tl;dr
- Jemalloc heap challenge
- A buggy implementation of
strncat
inmerge
allows for an overwrite onto the next region
Challenge Points: 540
No of Solves: 31
Challenge Author: Pwn-Solo
A beginner friendly pwn challenge I made for InCTFi 2021 as an introduction to jemalloc exploitation. A really helpful Article I found for jemalloc exploitation and internals
Jemalloc 101
Some terms that will help in understanding the writeup better
- Chunks - A chunk is used to describe big virtual memory
regions that the memory allocator conceptually divides available memory
into. - Arenas - Arenas are the central jemalloc data structures as they are used to manage the chunks
- Runs - Runs are further memory denominations of the memory divided by jemalloc into chunks , Runs are page aligned and are primarily used to track end-user allocations(regions).
Runs are divided into multiple size classes and hold regions of that particular size. - Regions - end user allocations, returned by
malloc()
(equivalent to chunks in Glibc). for small and medium sizes, the user allocations are contiguous. Unlike glibc the chunks dont have metadata, the state(allocated or free) of the regions are tracked by the run’s metadata
Bug
- The
merge
option essentialy lets you merge two regions if they are of the same size. The way this works is, itrealloc
‘s the first chunk tosize+size2
and then concats the data from the second region over to the realloced region
1 | char * buggy_strncat(char * dest, const char * src, size_t n){ |
The bug here is pretty clear, strlen()
can be tricked to return a value greater than the region size by allocating 2 regions and filling them end to end. This can then be leveraged to achieve an overflow when the merged region is created after the realloc()
Exploitation
To make the exploitation easy ,the 0x50 run contains a region holding a function ptr which is called during the exit option. There’s also system@plt
provided to further aid in exploitation.
There are actually a couple of ways in which you can go about exploiting this, one of which is to overwrite the name ptr of the enemy region to effectively gain an arbitrary free.This can be used to free the region holding the function ptr which we can then populate with system
by adding another chunk . This was how most teams solved it
but … we’re not here to talk about that
There’s another way , one which I find way more fun . The idea here is to corrupt the run header metadata to trick malloc into returning back an allocated chunk.
let’s take a look at the 0x50 run header (the run having our target funcion ptr)
1 | 0x7ffff7008000: 0x00000000384adf93 0x00007ffff7800d70 |
Struct members of the run header
1 | gef➤ p *(arena_run_t*)0x7ffff7008000 |
The magic field exists only for DEBUG builds of jemalloc but will cause a segfault if corrupted
The 0x0003fffffffffff0
is the bitmask of the 0x50 run, this bitmask is what tracks free and used regions. unlike Glibc , jemalloc does not use linked lists to track allocations. So, as an attacker ,overwriting this bitmask can get us control of the allocations in this particular run.
So, how do we set the bits ?
The state of each region is tracked by setting the bits according to a simple rule 0: in use , 1: free
since our target is right at the top and we have only 1 in use region, the last bit would be set to 0. But, since we need to get our new allocation to overlap with the previous one , we set the last bit back to 1 to essentialy fool jemalloc into thinking the run in unallocated.
Exploit
Battle
option does not check for negative indices, which lets us leak a bss pointer and at the same time overwrite the maximum number of allocations.
Now that there are plenty of allocations to play around with. We fill the 0x40 run ,which lies right before the 0x50 run, and trigger the overflow at the last allocation of the 0x40 run
1 | from pwn import * |
I had a lot of fun making this challenge , hope you guys did too.