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
Code Under Review
The challenge comes from phoenix/heap-zero in exploit.education.
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:
Heap - 1
Code Under Review
The challenge comes from phoenix/heap-one in exploit.education. The program allocates two structures on the heap, each containing an integer and a pointer to a dynamically allocated name buffer:
Then:
i1is allocated →i1->namegets an 8‑byte heap bufferi2is allocated →i2->namegets another 8‑byte heap bufferUser input (
argv[1],argv[2]) is copied withstrcpyinto each bufferA hidden function
winner()exists and prints a success message
Because strcpy performs no bounds checking, an overflow into the heap is possible.
Step 1: High‑Level Program Behaviour
At a high level, the program:
Allocates two structures:
i1with priority = 1 and an 8-byte name bufferi2with priority = 2 and another 8-byte name buffer
Copies user input directly into both buffers
Does nothing with the data afterward, the program ends
What matters here is not what the program does, but how it uses memory.
Because the heap buffers are fixed-size (8 bytes) but the user-provided strings can be longer, the program allows an attacker to overflow the first buffer (i1->name) and overwrite adjacent heap memory, including fields belonging to i2.
Step 2: Spotting the Vulnerability
🚨 The vulnerable lines are:
Problems:
strcpydoes not check length.The buffers are only 8 bytes long, including the null terminator.
Any input longer than 7 characters (even a simple string) will overflow.
Since heap allocations are typically sequential, overflowing i1->name allows an attacker to overwrite:
The pointer
i2->name
Once i2->name is overwritten, the second strcpy will attempt to write user-controlled data to an attacker-controlled pointer, enabling arbitrary writes.
This is the core heap exploitation primitive for this level.
Step 3: Heap Layout Reasoning
The exact layout looks like:

Because i1->name (0x7ffff7ef6030)comes before the i2 structure, overflowing i1->name lets you overwrite:
i2->priorityi2->namepointer (0x7ffff7ef6070)
If an attacker overwrites i2->name, then the second strcpy(i2->name, argv[2]) will copy the attacker’s second argument into whatever memory address they choose.
That is the crucial exploitation vector.
Step 4: Exploitation
The goal is to redirect program flow to the hidden function:
Since the program does not call winner() normally, the exploit must:
Overflow
i1->nameto overwrite the pointeri2->nameSet
i2->nameto a location the attacker wants to write toUse the second
strcpy()to write a new value (e.g., an address) into that locationInfluence a code path so that execution eventually jumps to
winner()
In this level, the intended target is typically:
The saved return pointer on the stack, or
A Global Offset Table entry (GOT overwrite)
By controlling where the second strcpy() writes data, the attacker achieves a controlled write, enabling a redirect to winner().
Before sharing the working exploit I developed, I ran into a few constraints, most notably the presence of null bytes, since strcpy stops at the first null. This was one of the reasons I couldn’t rely on a GOT‑based approach:

To work around this problem, I opted for a different strategy: instead of overwriting a GOT entry, I chose to overwrite the saved return address on the stack

As you can see, the return address of main() is 0x7fffffffe478. For context, ASLR is disabled for this binary, yet this value still changes when I’m not using pwndbg. I was ultimately able to solve the challenge using the hardcoded return address with pwndbg context.

I wanted an exploit that didn’t depend on pwndbg, so I examined the stack layout and noticed a consistent address pattern at 0x7fffffffe4xx. This allowed me to brute‑force the stack address by iterating through that range until I found a value that successfully redirected control flow:

Exploit
Last updated