Points: 800
Description:
The glibc heap is too insecure. I took matters into my own hands and swapped efficiency for security.
The mound is a custom heap exploitation challenge, also featuring seccomp :) .
We are given a binary, mound
, and the libc.so
(2.31).
In short this is what i did:
- Reverse the custom heap implementation
- Find vulns in the binary
- Code execution + seccomp bypass
1 - Reverse the custom heap implementation
The binary mmaps two region of memory:
mmap(0xdead0000000, 0x8018, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
mmap(0xbeef0000000, 0x400000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
0xdead0000000
will contain the following heap metadata:
struct heap_data {
void* beefmap; // = 0xbeef0000000
unsigned long ID_LIST[4096];
cache* cache; // = 0xbeef0000010
chunk* top_chunk;
};
0xbeef0000000
will be the actual heap containing the chunks.
The chunks are structured like this:
--------------------------------------
| (8 byte) random id | (8 byte) size | # <- chunk metadata, the id is used to
-------------------------------------- keep track of freed chunks.
| data |
--------------------------------------
When the heap is initalized a chunk of size 0xd0 is allocated at 0xbeef0000010
.
This chunk will hold a cache that works like the classic tcache from glibc:
struct cache {
uint8_t counts[24];
chunk* freelist[24]; // LIFO linked list
};
Each freelist will contain chunks of different sizes, from 0x10 to 0x100.
After the allocated chunks there is a top chunk of inital size 0x400000
.
So, after the initialization of the heap, 0xbeef0000000
looks like this:
0xbeef0000000: 0x27ae1e1f53167faf 0x00000000000000f0 <- cache chunk
0xbeef0000010: 0x0000000000000000 0x0000000000000000 <- counts
0xbeef0000020: 0x0000000000000000 0x0000000000000000 |
0xbeef0000030: 0x0000000000000000 0x0000000000000000 |
0xbeef0000040: 0x0000000000000000 0x0000000000000000 |
0xbeef0000050: 0x0000000000000000 0x0000000000000000 |
0xbeef0000060: 0x0000000000000000 0x0000000000000000 |
0xbeef0000070: 0x0000000000000000 0x0000000000000000 | freelists
0xbeef0000080: 0x0000000000000000 0x0000000000000000 |
0xbeef0000090: 0x0000000000000000 0x0000000000000000 |
0xbeef00000a0: 0x0000000000000000 0x0000000000000000 |
0xbeef00000b0: 0x0000000000000000 0x0000000000000000 |
0xbeef00000c0: 0x0000000000000000 0x0000000000000000 |
0xbeef00000d0: 0x0000000000000000 0x0000000000000000 |
0xbeef00000e0: 0x0000000000000000 0x0000000000000000 |
0xbeef00000f0: 0x706396411d7177ac 0x00000000003fff10 <- top chunk
0xbeef0000100: 0x0000000000000000 0x0000000000000000
...
Now we can analyze the functions used to allocate and free memory.
The program allows to free only chunks that fall inside the cache range.
A freed chunk looks like this:
--------------------------------------
| (8 byte) random id | (8 byte) size | # <- chunk metadata
--------------------------------------
| cache* verify | chunk* next |
--------------------------------------
verify
is a pointer to the cache = 0xbeef0000010
next
is a pointer to the next free chunk in the freelist.
In short the allocation works like this:
mmalloc(size)
| |------------------------------|
| |-----------------------------------| | cache contains a freed chunk |
|--> if | size falls inside the cache range | and | of the requested size |
| |-----------------------------------| |------------------------------|
|
| ---yes--> return mcache_alloc(size)
| ---no---> return top_chunk_alloc(size)
top_chunk_alloc(size)
|
|--> subtract size from heap_data->top_chunk
|--> move the top_chunk down (copy ID and size)
|
|--> set new_chunk->size and new_chunk->id = rand64bit()
|--> return chunk
mcache_alloc(size)
|
|--> take off chunk from freelist, freelist head is now chunk->next
|--> if chunk->verify != heap_data->cache: exit(1) # "Mcache: Invalid verify"
|
|--> remove chunk->id from heap_data->ID_LIST
|--> clear chunk->verify and chunk->next
|--> return chunk
And this is the freeing process:
mfree(chunk)
|
|--> search chunk->id in heap_data->ID_LIST, if found: exit() # "Mound: Double free detected"
|
---> mcache_free()
|
|--> if chunk size not in cache range: exit() # "Mound does not support
| such large freeing yet."
|--> add chunk to freelist:
| chunk->next = freelist[index]
| freelist[index] = chunk
|
|--> set chunk->verify
|
|--> add chunk id to heap_data->ID_LIST
2 - Find vulns in the binary
Checksec of the binary:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Run the binary and we get the following output:
1. Add sand
2. Add dirt
3. Replace dirt
4. Remove dirt
5. Go home
“Add sand”: allocate on the classic heap using strdup
with max 0x100 chars.
“Add dirt”: allocate on the custom heap using mmalloc()
, max size 0x10000.
“Replace dirt”: edit an allocation. ( we can only edit allocations from strdup )
“Remove dirt”: free an allocation.
“Go home”: exit.
We’ll have to work with two global arrays:
char* arr[16];
unsigned long sizes[16];
In the binary we also have a win
function:
void win(void) {
char buf [64];
puts("Exploiting BOF is simple right? ;)");
read(0, buf, 0x1000);
return;
}
The plan is to:
- get a double free and get an allocation on
heap_data
( in the0xdead0000000
map ) - overwrite
heap_data->top_chunk
and point it to the got - next
top_chunk_alloc
will end up in the got.
We can get a double free like this:
- Create two allocations using
strdup()
, the first allocation will write over theprev_size
field of the second allocation. - free the second allocation (using
mfree
on a chunk from the normal heap!!), theprev_size
field (that we overwrote before) will be treated as anid
and inserted inID_LIST
. - edit the first allocation and overwrite
prev_size
again but with a different value. (id
is changed, we bypassed the double-free check! ) - free the second allocation again, double-free !
- create one allocation using
mmalloc
and poison the freelist.
Side note:
In order to bypass the “Mcache: Invalid verify” check we need to make sure that the verify
field of the fake chunk we put in the freelist matches heap_data->cache
.
We can put in the freelist the address of ID_LIST[4095]
(0xdead0007ff8
).
At 0xdead0007ff8+8
we have heap_data->cache
(which will bypass the check) and at 0xdead0007ff8+16
we have heap_data->top_chunk
(which we will overwrite).
- create one more allocation to consume the freelist
- Now we request one more allocation and this time
mmalloc
will return0xdead0007ff8
thus letting us overwrite thetop_chunk
ptr. - Set
top_chunk
tosetvbuf@got.plt
- Request one allocation (coming from
top_chunk_alloc
) and overwrite__isoc99_scanf@got.plt
with the address ofwin
.
3 - Code execution + seccomp bypass
Now that win
is called we have a simple buffer overflow.
However we still have to deal with seccomp.
$ seccomp-tools dump ./mound
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014
0004: 0x15 0x09 0x00 0x0000003b if (A == execve) goto 0014
0005: 0x15 0x08 0x00 0x00000142 if (A == execveat) goto 0014
0006: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0014
0007: 0x15 0x06 0x00 0x00000003 if (A == close) goto 0014
0008: 0x15 0x05 0x00 0x00000055 if (A == creat) goto 0014
0009: 0x15 0x04 0x00 0x00000086 if (A == uselib) goto 0014
0010: 0x15 0x03 0x00 0x00000039 if (A == fork) goto 0014
0011: 0x15 0x02 0x00 0x0000003a if (A == vfork) goto 0014
0012: 0x15 0x01 0x00 0x00000038 if (A == clone) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL
From the provided Dockerfile we see that the following script is called:
#!/bin/sh
mv /pwn/flag.txt /pwn/$(xxd -l 16 -p /dev/urandom).txt
service xinetd start
sleep infinity
So we also need to find the flag filename.
Steps:
- Leak libc base
- Use
getdents64
to get/pwn
directory entries andwrite
to print the filenames. (the output will also contain a lot of gargbage, see getdents64)
Once the filename is known just use openat
and sendfile
to print the flag.
Leak filename:
$ ./solve.py REMOTE
[!] libc base: 0x7ff34a0a1000
...
pwn/716b228a42da0c8b248c9e2f801f2c6f.txt
...
Print flag:
$ ./solve.py REMOTE
[!] libc base: 0x7fb3ed074000
rarctf{all0c4t0rs_d0_n0t_m1x_e45a1bf0b2}
link to solve.py