Phoenix64 Writeup - heap{n}

Introduction

This post documents my learning journey through the phoenix challenges from Exploit Educationarrow-up-right, 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-zeroarrow-up-right 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:

  1. Allocate 2 heap objects:

    1. struct data --> contains 64-bytes character buffer.

    2. struct fp --> contains a function pointer.

  2. Initializes f->fp to nowinner

  3. Copies user-controlled input (argv[1]) into d->name without bounds checking.

  4. calls f->fp()

Intended Control Flow

Step 2: Spotting the Vulnerability

🚨 Vulnerable Line

Why This is Dangerous

  • d->name is 64 bytes.

  • strcpy performs 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 constant

  • No 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-onearrow-up-right 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:

  1. i1 is allocated → i1->name gets an 8‑byte heap buffer

  2. i2 is allocated → i2->name gets another 8‑byte heap buffer

  3. User input (argv[1], argv[2]) is copied with strcpy into each buffer

  4. A 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:

    • i1 with priority = 1 and an 8-byte name buffer

    • i2 with 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:

  • strcpy does 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->priority

  • i2->name pointer (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:

  1. Overflow i1->name to overwrite the pointer i2->name

  2. Set i2->name to a location the attacker wants to write to

  3. Use the second strcpy() to write a new value (e.g., an address) into that location

  4. Influence 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