I already hinted at what the problem is. The idea is that we have a blatant heap overwrite. To add on to that, we can create up to 32 heap chunks (we don't need that), and can overwrite any of them from the one previous. We can print out all the values. Finally, we can cause the program to delete any given entry.
The key point to add to all of this is the fact that we have a pointer to our heap address in a known static place. Specifically, the array named "reqlist". This becomes important because it opens up the opportunity for us to utilize the unsafe unlink attack.
The idea behind the unsafe unlink attack is that we can overwrite a pointer somewhere by abusing the unlink method of the free call. This method is called when we want to merge two adjacent, now free, memory chunks together so that we can have one large one instead of two small ones. There was an unlink vulnerability in earlier versions of libc that allowed almost arbitrary write what where. However, modern versions of libc added a specific check to defeat this.
At its core, dlmalloc can be thought of similar to a doubly linked list. Each free malloc chunk contains a pointer to the next chunk, as well as to the previous chunk. The check that libc implemented to stop the previous unlink attack was simply to verify that the current chunk's forward pointer had a corresponding backwards pointer that pointed to the original chunk. The same concept with the backwards pointer. In code, it looks like (P->fd->bk != P || P->bk->fd != P) != False.
While this check does stop a write-what-where, clever people discovered a new way to attack it. The new attack utilizes the fact that malloc doesn't inherently know where the malloc'd blocks are. It utilizes pointers and sizes to discover this. That means, we can create our own fake malloc structures that malloc will believe are real, because it has no way of really determining they're fabricated. The only real requirement for this attack, aside from the ability to overwrite, is that we know where a pointer to our malloc chunk is. This is needed so that we can satisfy the unlink checks as described previously.
In our case, we do happen to know where our pointer is because it's being held in a global array on a non Position Independent Executable. Specifically, it resides at 0x609e80. That means, our game plan is:
- Allocate two blocks (don't overflow them)
- Edit the first block, creating a fake malloc structure and overwriting the control structure of the second
- Delete the second, causing the unlink
- We now have a pointer directly before our global array of pointers. We have arbitrary read/write ability.
The Fake Chunk
The fake chunk we are creating will look something like this:
- 8-bytes of 0 - <-- Previous Size
- 8-bytes of 0 - <-- Size and AMP flags
- Forward ptr - <-- Global Pointer Address - (8 * 3)
- Backward ptr - <-- Global Pointer Address - (8 * 2)
- Padding - <-- Up to next chunk header
- 0x30 - <-- Previous size (back to our fake chunk)
- 0x42 - <-- Original chunk size minux in-use flag
Now when we attempt to free the second block, malloc will believe that our first block is free. It will read our fake malloc header structure, discover that the forward and back pointers point to a valid place, and update it for us. The net effect is that the next time we choose the menu option to edit our first block, we will instead be editing directly before it. If we're careful about how we write, we can leverage this for arbitrary writes and reads.