Heap-Hop
Solves: 31 Medium
Heap exploitation is cool, and the best is when no free is used. >Try to pwn the challenge and get the flag remotely.
Note:
- You must spawn an instance to solve this challenge. You can connect to it with netcat: nc IP PORT
Author: Express#8049
Remote service at : nc 51.254.39.184 1336
Heap-hop is a heap exploitation challenge I did during the pwnme CTF. It involved classic tricks like tcache poisoning and GOT hiijacking. You can find the related files here.
TL;DR
- Setup heap layout
- fill tcachebin for 0x400 sized chunks
- free large 0x400 sized chunk to get libc addresses
- oob read onto the chunk right before the large freed chunk => libc leak
- request a small 0x20 sized chunk that gets free right after, it falls at the begin of the chunk in the unsortedbin, oob read like just before => heap leak.
- tcache poisoning (we’re able to deal with safe-linking given we leaked heap)
- With the help of tcache poisoning, overwrite
realloc@got
to write&system
realloc("/bin/sh")
is thensystem("/binb/sh")
What we have
1 | $ checksec --file ./heap-hop |
What we can see is that a recent libc is provided (which means with safe-linking) and that the binary isn’t PIE.
Code review
Here is basically the main logic of the binary:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
Basic layout for a heap exploitation challenge, we’re allowed to create, read and edit a given track. As we already read in the initial statement we apparently cannot free a track.
Let’s first take a look at the create function:
1 | unsigned __int64 handle_create() |
It crafts a chunk, and then allocates a chunk for a given size (< 0x480). The read function is very basic:
1 | unsigned __int64 handle_read() |
It prints tracks[v1]->size
bytes from tracks[v1]->track
. Which means no need to worry about badchars for the leak.
The bug lies in the handle_edit
function:
1 | unsigned __int64 handle_edit() |
There are two bugs, or at least interesting behaviours around realloc. First there is an out of bound (oob) read / write, indeed if we give a size smaller than tracks[idx]->size
, then v0->track
could be changed to a smaller chunk and thus read(0, (void *)tracks[idx]->track, tracks[idx]->size);
could write over the end of the chunk. Secondly we can free a chunk by giving zero to the size.
Exploitation
Given tcache poisoning seems to be pretty easy to achieve, we need to find where we could use our arbitrary write. If you remind well, the binary isn’t PIE based and has only partial RELRO, which means we could easily hiijack the GOT entry of a function (like realloc) to replace it with system and then call realloc("/bin/sh")
. This way we need to get a heap and a libc leak.
libc leak
To get a libc leak we can fill the tcache and free a large chunk to make appear libc addresses on the heap and then read it through the oob read. Which gives:
1 | create(0, b"", 5, b"0") |
The heap looks like this:
1 | 0x1d83120 0x0000000000000000 0x0000000000000041 ........A....... <= chunk used to get the oob r/w |
I advice you to take a look at the heap layout if you do not understand the exploit script.
Heap leak
Now we got a libc leak we’re looking for a heap leak, it is basically the same thing as above, but instead of freeing a large chunk, we free a small 0x20
sized chunk. To understand the defeat of safe-linking I advice you to read this. Which gives:
1 | # leak heap to craft pointers |
tcache poisoning
To achieve tcache poisoning we just need to get the 0x20
sized chunk right after the out of bound chunk. Then we free it and we use the out of bound chunk to overwrite the forward pointer of the victim chunk to &realloc@GOT
. Given we leaked the heap we can easily bypass the safe-linking protection.
1 | #== tcache poisoning |
PROFIT
Then we just have to do:
1 | # edit => realloc("/bin/sh") => system("/bin/sh") |
Which gives:
1 | nasm@off:~/Documents/pwn/pwnme/heap$ python3 exploit.py REMOTE HOST=51.254.39.184 PORT=1336 |
Final exploit
Here is the final exploit:
1 | #!/usr/bin/env python |