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
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.
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.
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:
Looks like I was right about what the binary was doing. Let's proceed..
Hacking The Mainframe
We have a clear picture of what we want to accomplish. Let's run PwnDBG and get the functions.
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.
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
.
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
.
The 41
s are the A
s from the first command-line argument and as you can guess the 42
s are the B
s.
At this point let's go over the attack:
- We overflow with
A
/41
as padding until we're about to overwrite the value0x00405370
with the first command-line argument. - We then append the address the GOT
puts()
entry, still in the first command-line argument. - The first
strcpy()
will write the first command-line argument which will overwrite the second struct, specifically the pointer to0x405370
. - Since the second
strcpy()
dereferences the the address whatever we put in the second command-line argument will overwrite the GOT entry forputs
. - We overwrite it with the address to
winner()
andputs()
is called before exit in the program redirecting flow towinner
.
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')")
The second heap exercise is complete!