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:

  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:

Last updated