This weekend, me and 0x4a47 attempted the flea_attack challenge from Harekaze CTF, but we were unable to solve it in the time allocated. Still, this was a good lesson in modern heap exploitation, and therefore, I think it’s worth writing up.
At the end of this post, I will disclose the exploit-in-progress at the time the CTF ended, as well as two modified exploits, one of which “works” (but both prove the concept).
This challenge was presented as a 64-bit Linux binary, which you can download here: flea_attack. The flow of the application is as follows:
- Open /home/flea_attack/flag, drop this into 0x204080
- Read a comment, of 0x60 (via original_fgets: 95 characters) in length into 0x204000.
- Present the user with a menu, allowing:
- Adding a note (_malloc)
- Removing a note (_free)
Based on the challenge title and the way the add and remove functions were structured as malloc and free primitives, we figured this would be a heap challenge.
My initial, naive approach was to attempt your standard unlink macro corruption, but inspecting memory, I noticed that this challenge required a different approach:
As you can see, there is no fd/bk pointer to overwrite to exploit unlink with, just a size. Furthermore, there’s not really any “overwrite” per se: while we entirely control a chunk, there’s no way provided for us to actually write outside an allocated chunk.
The madness of GLIBC
This is due to the glibc’s heap implementation, which is much, much more complicated than glibc. In glibc, memory allocations are stored in “bins”, in which memory blobs of a single size can be stored one chunk after another / as efficiently as possible by glibc. For example, if I request a malloc of 2 bytes, malloc is likely to return me a chunk of 16 bytes (the smallest chunk size), as part of a bin (as per the behaviour above).
Each chunk allocation looks like this:
When I free a chunk, two things happen:
- Perform some sanity checking around the chunk I want to free – it’s structures must be valid. Look here for more information.
- The chunk’s “fd” ptr is zeroed. This indicates the memory is ready for re-use.
- The chunk address is added to a “freelist”, which operates like a FIFO stack. If I want to allocate 2 bytes, glibc will traverse it’s freelist, to determine if there’s a chunk of memory it can re-use. If not, it will allocate me a new chunk.
When I allocate memory to a previously freed chunk (from the freelist):
- If the “fd” ptr is zeroed, return a pointer here and let the user write.
- If the “fd” ptr is not zeroed, return a pointer here and let the user write, but next time you allocate memory, follow the pointer to get the next free chunk.
Note that none of this is magic: glibc malloc simply relies on some flimsy-ass local structures to determine if you’ve got a valid chunk and what that looks like so it’s entirely possible to forge heap allocations for yourself, which was my approach.
Forging a chunk
We can then forge two chunks with a single malloc call: if we allocate a large chunk of memory, we can write a smaller chunk header inside it, creating a second “fake allocation”. This second allocation can then be freed, writing it’s location to the free list.
Now, when we reallocate a chunk of the same size, glibc will fetch our fake block from the freelist – if the fd pointer happens to be written at this point, glibc will fuck up the next allocation, pointing it to whatever we filled fd with.
We can attempt this to overwrite the flag directly. Firstly, we forge a fake heap chunk:
Our fake heap chunk is at 0x7bb260 (with malloc returning 0x7bb270). Note that it’s size is 0x20 (the 1 is a flag, indicating the previous chunk is in use).
Now, we allocate memory of size < 20, and we should overwrite this with no error:
Note that 0x7bb270 is now replaced by our userdata. Where is our pointer, I wonder?
There it is! The error indicates that glibc tried to give us memory from that location, but it doesn’t have a valid header. Fair enough, it’s the flag, and there’s nothing before it: that’s not a valid header.
Our flag is stored at 0x204080, but we’re only able to write until 0x204060 (based on the original_fgets function). Still, if I’m able to allocate memory in the 0x204000 to 0x204060 range, this should be good enough: I can size this allocation to cover until the flag (by writing 95 bytes into the comment: normally, original_fgets will add 0x0a after your comment, but will fail at doing so if you write as many characters as it allows, giving us a clean fastbin length.
So, we modify our exploit as follows:
- In the initial comment, forge a chunk at 0x204056 (by sending 94 “A”‘s followed by one character of length). Let the size of this chunk be X.
- Make an allocation of size >X. Let this be allocation A
- Within allocation A, forge a second chunk of size X. Let this be allocation B.
- Free B (on the freelist)
- Free A.
- Make an allocation of size >X, re-using A. During this allocation, recreate B, with a fd pointer aiming at 0x204056.
- Make an allocation of size X, re-using B, and storing B’s “fd” pointer as the next free chunk.
- Make a second allocation of size B. This will allocate memory at 0x204066 (taking into account headers), letting is write and leak memory from there (by filling it with enough characters such that it reaches the flag at 0x204080).
While we did not finish this exercise in time, you can find the original script we arrived at at the conclusion of the CTF here.
A working script which demonstrates the concept above is here. Note when you make the second allocation of size <0x20, you get an address within the comment section, demonstrating the attack is possible. Tweak the field size to leak the flag. Here’s the script in action, allocating an address within the comment section:
I also followed someone else’ writeup, which talked about exploiting this through a double free. This is also doable, and the script for that is here. Note that when double freeing, you can’t free the same chunk twice in a row: thus the extraneous free operation.
If only there were more hours in the day…