Exploit.edu Heap One - Heap Hell Part Two

Previous

Continued

Last walkthrough showed how to overflow the heap until a chunk that contained a function pointer was overwritten the address of the winner() function. In this walkthrough I'll explain my thought process and methodology as well, but first let's get into some stuff I believe I should have covered the first time.

Heap vs Stack

Pasted image 20250804112424.png
This is a visual representation of the binaries we will be hacking. Please note that the top left "writable; not executable" is not always the case as it depends on compiler protections (ours is since we use execstack when compiling). That's a bit off topic for the stack, but I want to show how the stack will grow "down" and the heap will grow "up". In theory you could smash right through the stack from the heap with 0x41 aka As for example. This would definitely break the program, still possible though without if buffer size isn't checked correctly.

Ghidra

Just like before I'll start by grabbing the code without looking at it (or trying not to). That way I can use Ghidra and start from a binary. I'll use the script https://github.com/pwnengine/binary-exploitation-ctf-helpers gcc-no-protect64.sh to compile without protections. You could just grab the binary from that repository and not compile at all. I'll open Ghidra on the binary and take a look.

Pasted image 20250806112119.png

At main I can see that it's looking a bit strange. When I see variables like this it's safe to assume it could be a struct. It's just something you notice while doing CTFs like this, or even reversing in general.

Pasted image 20250806113332.png

This is the decompiled code after I changed the two unidentified types to structs. I also changed the size representation to decimal (16 bytes) and the labels (struct1, struct2). I'm confident this is similar to what we'd see when viewing the source code. Now that there's a better representation what's going on here? Well, struct1 is allocated and it's first field is assigned 1 then another allocation is created on the heap with a size of 8 bytes. A second field of the struct is assigned to the memory address of the 8 byte allocation. The exact same thing happens with struct2 the difference is it's first field is 2 not 1. From this I can get an idea of what the author wants us to do. The first strcpy() will NOT check size of buffer so we could write to it until we've overwritten the field that is getting dereferenced in the second strcpy(). If we overwrite it to another address when that second strcpy() is called anything in the second command-line argument will be written to it. I want to show the original code now before we start debugging with PwnDBG:
Pasted image 20250806114529.png
Looks like I was right about what the binary was doing. Let's proceed..

Hacking The Mainframe

Pasted image 20250808092606.png
We have a clear picture of what we want to accomplish. Let's run PwnDBG and get the functions.
Pasted image 20250806115620.png
I'll grab the address for winner() as that's where we want to end up. Next, disassemble main() and set a breakpoint somewhere that makes sense to calculate the padding needed for the payload.
Pasted image 20250806115757.png
main+20 is where the first malloc() is called so I'll break there and get the RAX register for the address returned. I'll also set a breakpoint at main+160 the last strcpy to examine the allocated chunks after being filled.

Next just run the program r AAAAAAAA BBBBBBBB.
Pasted image 20250806205302.png

Take note of RAX that's the address returned by malloc().

c then on the next break ni and examine the memory at the address noted down from above using x/128x 0x405310.
Pasted image 20250806205915.png
The 41s are the As from the first command-line argument and as you can guess the 42s are the Bs.

At this point let's go over the attack:

  1. We overflow with A/41 as padding until we're about to overwrite the value 0x00405370 with the first command-line argument.
  2. We then append the address the GOT puts() entry, still in the first command-line argument.
  3. The first strcpy() will write the first command-line argument which will overwrite the second struct, specifically the pointer to 0x405370.
  4. Since the second strcpy() dereferences the the address whatever we put in the second command-line argument will overwrite the GOT entry for puts.
  5. We overwrite it with the address to winner() and puts() is called before exit in the program redirecting flow to winner.

Counting how many bytes it would take right before overwriting 0x00405370 from 0x00405330 we get 40 bytes. We can use readelf -all heap1 | grep puts to get the address of the puts GOT address. Putting that all together the final payload is:

r $(python2 -c "print('A'*40 + '\x08\x40\x40')") $(python2 -c "print('\x21\x12\x40')")

Pasted image 20250807161616.png
Pasted image 20250808092738.png
The second heap exercise is complete!