Party Cat0%

CyCTF25PWNWrite-Ups

Omar Mohamed
Thanks for sharing!

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

CyCTF 25 PWN Write-Ups
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:
  1. load_flag(): This function reads the flag from flag.txt into a global buffer. The address of this buffer is 0x404300, which we will need later. (You can get it from gdb or by simply double clicking it in the decompiler.)
  2. fh = fopen("/dev/null", "w"): The program opens /dev/null and stores the FILE pointer in a global variable fh. A FILE structure is allocated to manage this stream.
  3. The Vulnerability: There's a hidden menu option, 1337 (0x539). When chosen, the program executes read(0, fh, 307). This is the bug! It reads 307 bytes from our input directly into the memory location pointed to by fh. Since fh points to a FILE struct 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:
  1. Choose the hidden menu option 1337.
  2. Send a specially crafted payload that will overwrite the FILE struct in memory.
  3. This new, fake FILE struct will be configured to make the next fwrite call 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).
fileno is the file descriptor number, 1 corresponds to standard output (stdout). By setting this, we tell fwrite to 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.
pwn college
Of course pwntools write function handles all of that!
love pwntools
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 a FILE structure. This fake struct is configured such that any write operation on it will actually read 0x100 bytes from FLAG_ADDR and write them to the stream's output (which is stdout by default).
  • The program's read(0, fh, 307) overwrites the real FILE struct with our fake one.
  • The subsequent fwrite and fflush calls 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:
  1. First SROP frame will read(0, .bss, 0x200) to write /bin/sh and a second SROP frame into .bss, and set rsp to that area.
  2. 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

  1. Overflow password buffer and overwrite return to trigger a small ROP that:
    • sets rax to 0xF (sigreturn trigger value), calls syscall gadget to invoke sigreturn.
  2. First sigreturn frame (lets call it frame A) sets registers to call read(0, bss, 0x200) and crucially sets rsp to bss + 0x100 (stack pivot). rip = syscall gadget so read executes.
  3. Send payload for read: /bin/sh\x00 padded up to bss + 0x100, followed by second sigreturn frame (frame B).
  4. When read returns, execution continues at the pivoted rsp in .bss, now pointing at frame B; sigreturn is invoked again (via the syscall gadget) to populate registers for execve.
  5. Frame B sets rax=59, rdi=bss (pointer to /bin/sh), rsi=0, rdx=0, rip=syscall. syscall executes execve("/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
vmmap
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:
  1. the string b"/bin/sh\x00" (placed at bss_addr),
  2. padding up to bss_addr + 0x100,
  3. gadgets to re-trigger sigreturn (pop rax; 0xf + syscall),
  4. 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.
  1. Overwrite mmap_prot: We'll use the buffer overflow to change the value of mmap_prot from 3 (RW-) to 7 (RWX), which is PROT_READ | PROT_WRITE | PROT_EXEC. This will make the memory region allocated by mmap executable.
  2. Place Shellcode: The program copies our input from buf into the newly mapped region with memcpy. This means our shellcode, placed at the beginning of our input, will be copied into the executable memory region.
  3. Overwrite Return Address: We'll continue the overflow past mmap_prot to overwrite the saved return address with 0x500000, 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! 🍄🍄

You might also like