CyCTF25PWNWrite-Ups
Omar Mohamed
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

Long time no see, folks! I'm back with some fresh goods from the the recent CyCTF Quals. I was able to clear pwn alhamdulillah so I will be covering all the challenges in this write-up. All will be in details with sources for everything! Let's get started!
All challenges can be found here.
pwn1 (481 pts)
This was a classic FSOP (File Stream Oriented Programming) challenge. Let's dive into the source code to see what's going on.
Source Code Analysis
The decompiled code shows a simple menu-driven program.
Here's a breakdown of the interesting parts:
load_flag(): This function reads the flag fromflag.txtinto a global buffer. The address of this buffer is0x404300, which we will need later. (You can get it from gdb or by simply double clicking it in the decompiler.)fh = fopen("/dev/null", "w"): The program opens/dev/nulland stores theFILEpointer in a global variablefh. AFILEstructure is allocated to manage this stream.- The Vulnerability: There's a hidden menu option,
1337(0x539). When chosen, the program executesread(0, fh, 307). This is the bug! It reads 307 bytes from our input directly into the memory location pointed to byfh. Sincefhpoints to aFILEstruct on the heap, we can overwrite its contents entirely.
This is a textbook setup for an FSOP attack. By controlling the
FILE struct, we can manipulate how I/O functions like fwrite and fflush behave.FILE Structure?
So.. what the hell is this?
The
FILE structure is an internal representation used by the C standard library (libc) to manage file streams. It contains various fields that track the state of the stream, including buffer pointers, flags, and function pointers for I/O operations.The struct looks like this (simplified, full struct here):
When we call functions like
fwrite or fflush, they rely on the information in the FILE struct to determine how to handle the data.And this is exactly what we will be doing, we will overwrite the
FILE struct to make fwrite read from the flag's memory location and print it out. And can you guess what is this technique called? yes, FSOP!I will be covering only the required knowledge to solve this chall, for more info on FILE struct you can search
What is FSOP?
FSOP, or File Stream Oriented Programming, is a technique that allows you to corrupt
FILE structure.By overwriting a
FILE struct, we can gain control over the program's execution flow. For example, when fflush(fh) or fwrite(..., fh) is called, libc will use the function pointers within our corrupted struct, allowing us to redirect execution to arbitrary code or, in this case, trigger specific behaviors beneficial to us.A resource for understanding the FILE struct exploitation is pwn college, niftic.ca or any other FSOP resources.
The Exploit Plan
Our goal is to read the flag from memory at address
0x404300. Here's the step-by-step plan:- Choose the hidden menu option
1337. - Send a specially crafted payload that will overwrite the
FILEstruct in memory. - This new, fake
FILEstruct will be configured to make the nextfwritecall leak the flag for us.
So, how do we make
fwrite, a function for writing, read arbitrary data for us?Think about how
fwrite works internally. It reads data from a buffer and writes it to a destination. The location and size of this buffer are stored inside the FILE struct itself, using pointers like _IO_write_base and _IO_write_ptr.Our attack is to overwrite the
FILE struct and change these pointers. We will set _IO_write_base to point to the beginning of the flag's memory location (0x404300), _IO_write_ptr to _IO_write_base + length and fileno to 1 (stdout).filenois the file descriptor number,1corresponds to standard output (stdout). By setting this, we tellfwriteto write to the console.
There is also a quirk where we need to set
_IO_read_end to _IO_write_base, I don't know why but it has got to do with fwrite's implementation, it was mentioned in pwn.college.
Of course pwntools write function handles all of that!

When the program calls
fwrite again, it will look at our malicious FILE struct and think that the flag's memory is its data buffer. It will then proceed to "write" the contents of that buffer (the flag) to its destination, which in our case is standard output. We have successfully turned a write function into an arbitrary memory read gadget!You can write the
FILE struct yourself, but pwntools makes it easy with its pwnlib.filepointer.FileStructure utility. We can simply tell it what address we want to read from, and it will generate the entire payload for us.The Exploit
The final exploit is surprisingly short, thanks to the power of
pwntools.pay = fp.write(FLAG_ADDR, 0x100): This is the core of the exploit. It generates a byte string representing aFILEstructure. This fake struct is configured such that any write operation on it will actually read0x100bytes fromFLAG_ADDRand write them to the stream's output (which isstdoutby default).- The program's
read(0, fh, 307)overwrites the realFILEstruct with our fake one. - The subsequent
fwriteandfflushcalls operate on our malicious struct, read the flag from memory, and print it out.
What a ride! Though this was the first pwn challenge, it was the one with the fewest solves. Now let's hop to the next one!
pwn2 (460 pts)
This next challenge was a fun one involving a buffer overflow that we escalate into a Sigreturn Oriented Programming (SROP) attack to get a shell.
Source Code Analysis
Let's look at the decompiled code.
You can immediately spot the bof in
check_password function. It allocates a 32-byte buffer (local_28) on the stack but then uses read to write 0x200 (512) bytes into it.The Exploit Plan
You must know what SROP and stack pivoting is before continuing, some good resources are: ir0nstone and pwn.college
In a typical SROP challenge will be using
execv with /bin/sh, but we need an address pointing /bin/sh first, that's that we don't have here. Although username could hold the string, there's no stack leak to discover its exact addr. So.. my plan was to use read to put the string /bin/sh in .bss, then do another SROP for execv.- Goal
get a shell by abusing the
check_password stack overflow to perform two chained SROP frames:- First SROP frame will
read(0, .bss, 0x200)to write/bin/shand a second SROP frame into.bss, and setrspto that area. - Second SROP frame does
execve("/bin/sh", 0, 0).
Why SROP here: Since there is no gadget for setting
rdi, SROP lets us set all registers from a crafted sigcontext frame and call syscall, only a gadget to set rax and a syscall/ret gadget are required.For some reason I couldn't make the first SROP return directly to the second one, so I pivoted the stack instead.
- High-level flow
- Overflow
passwordbuffer and overwrite return to trigger a small ROP that:- sets
raxto0xF(sigreturn trigger value), callssyscallgadget to invokesigreturn.
- sets
- First sigreturn frame (lets call it frame A) sets registers to call
read(0, bss, 0x200)and crucially setsrsptobss + 0x100(stack pivot).rip=syscallgadget soreadexecutes. - Send payload for
read:/bin/sh\x00padded up tobss + 0x100, followed by second sigreturn frame (frame B). - When
readreturns, execution continues at the pivotedrspin.bss, now pointing at frame B;sigreturnis invoked again (via thesyscallgadget) to populate registers forexecve. - Frame B sets
rax=59,rdi=bss(pointer to/bin/sh),rsi=0,rdx=0,rip=syscall.syscallexecutesexecve("/bin/sh", 0, 0)-> shell.
All syscall numbers are for x86_64 Linux can be found here
The Exploit
First let's choose a writable free space in memory

We have got
0x4040b0.- Call read(...) and pivot stack
Now we want
read(0, bss_addr, 0x200) and rsp = bss_addr + 0x100:- Second-stage layout (what we send to read())
The data we send after frame A (via the
read) must contain, in order:- the string
b"/bin/sh\x00"(placed atbss_addr), - padding up to
bss_addr + 0x100, - gadgets to re-trigger
sigreturn(pop rax; 0xf+syscall), - frame B (
SigreturnFrame).
Concretely in your exploit you build
second_stage as:- Frame B: execve("/bin/sh", 0, 0)
Frame B sets registers for
execve (syscall 59) and points rdi at bss_addr where /bin/sh is stored:The final exploit looks like this:
And just like that, we get a shell!
pwn3 (464 pts)
The final pwn challenge was a neat one that involved a buffer overflow to manipulate the arguments of an
mmap call, allowing us to execute shellcode.Source Code Analysis
You can immediately spot the bof in
fgets(buf, 160, stdin), where buf is only 128 bytes long. There is an intger mmap_prot right after it on the stack, which is used as the third argument to mmap.What is mmap()?
mmap is a system call that can also be used to allocate memory regions with specific permissions (read, write, execute). The function signature is:The important arguments we need to know for this challenge are:
addr: Desired starting address for the mapping.length: Size of the mapping in bytes.prot: Memory protection flags (e.g.,PROT_READ,PROT_WRITE,PROT_EXEC).
Source Code Analysis Cont.
mmap is used to allocate a memory region at a fixed address 0x500000 with size 0x40000 and permissions defined by mmap_prot. Initially, mmap_prot is set to 3, which means the region will be readable and writable, but not executable.However, because
mmap_prot is on the stack right after our buf, we can overwrite it with our buffer overflow!The Exploit Plan
The plan is to get shellcode into an executable memory region and then jump to it.
- Overwrite
mmap_prot: We'll use the buffer overflow to change the value ofmmap_protfrom3(RW-) to7(RWX), which isPROT_READ | PROT_WRITE | PROT_EXEC. This will make the memory region allocated bymmapexecutable. - Place Shellcode: The program copies our input from
bufinto the newly mapped region withmemcpy. This means our shellcode, placed at the beginning of our input, will be copied into the executable memory region. - Overwrite Return Address: We'll continue the overflow past
mmap_protto overwrite the saved return address with0x500000, the address of our now-executable shellcode.
The Exploit
Running the exploit sends the carefully crafted payload. The program overflows, maps an executable memory region, copies our shellcode into it, and then jumps straight to the shellcode upon returning.
That's it for today! Hope you enjoyed this write-up as much as I enjoyed solving the challenges. You can Dox me on X/Twitter from here. See you next time! 🍄🍄

