Phoenix64 Writeup - heap{n}
Introduction
This post documents my learning journey through the phoenix challenges from Exploit Education, focusing on memory corruption in x64 binaries.
Rather than rushing to an exploit, the goal is to digest the concepts:
How to review code for memory safety issues
How vulnerabilities arise in heap layouts
How exploitation differs on x64
How to reason about exploitation step-by-step
How to fix the vulnerability correctly
Heap - 0 : Function Pointer Overwrite
Code Under Review
struct data {
char name[64];
};
struct fp {
void (*fp)();
char __pad[64 - sizeof(unsigned long)];
};Step 1: High-Level Program Behaviour
At runtime, the program:
Allocate 2 heap objects:
struct data--> contains 64-bytes character buffer.struct fp--> contains a function pointer.
Initializes
f->fptonowinnerCopies user-controlled input (
argv[1]) intod->namewithout bounds checking.calls
f->fp()
Intended Control Flow
Step 2: Spotting the Vulnerability
🚨 Vulnerable Line
Why This is Dangerous
d->nameis 64 bytes.strcpyperforms no bounds checking.Any input longer then 64 bytes will overflow into adjacent heap memory.
This is a heap-based overflow.
Step 3: Heap Layout Reasoning
Heap allocation are usually contiguous, malloc(sizeof(struct data)) followed by malloc malloc(sizeof(struct f)) often results in :
[Metadata 16 byes][d->name (64 bytes)][Metadata 16 byes][f->fp (8 bytes)] [...]
That means overflowing d->name can overwrite f->fp.
we can confirm that using a debugger after setting a breakpoint on strcpy() :
We examine the values of d (at rbp-0x10) and f (at rbp-0x8), as well as the distance between them.
f we proceed to the next line of code (using 'n'), we arrive right after the return of strcpy.
At the target memory location, we find exactly what we expected:
We can observe that our "X"s have been copied to the address stored in 0x7ffff7ef6010. The data structure was 64 bytes in size. After the 64-byte mark, there are 16 bytes of metadata, followed by the start of the next chunk, where the second structure is allocated.
Additionally, we notice the value 0x00400ace at byte 0x7ffff7ef6060. This corresponds to the address of nowinner.
We could simply overflow the buffer with 64 + 16 bytes, reach the function pointer, and overwrite it with our winner function. However, there's an issue in this case: the problem is that our winner function is located at the address:
This address contains not only a null byte (actually, 5 null bytes, but since the memory is conveniently set to 0, this isn't an issue), but also a byte 0x0a. When strcpy copies the string, it will replace this byte with a null byte, which will terminate the string copy prematurely. As a result, the string won't be fully copied, and the overflow won't work as expected.
To reach the winner function, we introduce an extra layer of indirection. We place a small shellcode at the start of our buffer that jumps to the winner function (using push ADDR; ret).
Step 4: Exploitation
Before attempting exploitation, it’s good practice to inspect the binary’s security mitigations. This helps determine what kind of exploit is feasible and what complexity to expect.
Running checksec on the Phoenix heap-zero binary:
Interpreting Each Mitigation (x64 Context)
Arch: amd64-64-little
64-bit little-endian binary
Function pointers and addresses are 8 bytes
Endianness matters when overwriting pointers (
little-endian)
This directly impacts how we craft the overflow payload.
RELRO
Global Offset Table (GOT) is writable
Stack Canaries
Stack-based overflows would not be detected
Not-directly relevant here (heap overflow), but confirms
The binary is intentionally weak
No runtime detection of memory corruption
NX (Non-Executable Memory)
Heap and stack memory is executable
Shellcode injection would be possible
PIE
Binary loads at fixed address
Function addresses (like
winner()) are constantNo ASLR for the main function
RWX Segments
Momory segments are readable, writable, and executable
Extemly insecure by modern standards
By exploiting the heap buffer overflow, we can overwrite the function pointer fp with the address of our buffer, which doesn't include any null bytes. This is the core of the exploit:
And triggering the exploit:
Last updated