x86
Introduction
Letβs consider the following source code:
#include <unistd.h>
#include <stdio.h>
int secure(){
char buffer[200];
int input;
input = read(0, buffer, 200);
printf("\n[+] user supplied: %d-bytes!", input);
printf("\n[+] buffer content --> %s!", buffer);
return EXIT_SUCCESS;
}
int main(int argc, char *argv[]){
secure();
return EXIT_SUCCESS;
}This program is perfectly secure. We set up a buffer thatβs 200 bytes wide and when we take input from the user, we only allow 200 bytes to be inputted into that previously defined buffer!
We couldnβt even overflow this if we tried (I mean eventually, we would break the pipe but thatβs not important right now). Letβs see what happens if we try to sneak in a couple hundred more bytes than whatβs explicitly defined:

See, it only prints the buffer size, which is the expected and secure response from our program!
Exploitable Example
Now, letβs look at the following code:
This is where stuff gets bad. The only difference between this code and the secure code is the following line: input = read(0, buffer, 400);.
This time, the user is allowed to input more bytes than the buffer can handle. So, even when we try to compile this thing, we can see that the compiler screams at us telling us that what weβre doing something insecure:

Now, if we try to supply more bytes than the buffer space, we can see that we get a segmentation fault:

If you got a message saying *** stack smashing detected ***: terminated, this is normal as by default your OS protects your binaries using ASLR, NX, PIE and Stack Canary, so you have to compile with those flags:
gcc -m32 vuln.c -o vuln -fno-stack-protector -fno-pie -no-pie -z execstack -mno-accumulate-outgoing-args
-fno-stack-protector: Disables stack smashing protection (no canary values inserted).-fno-pie: Disables Position-Independent Executable (PIE) for non-relocatable code.-no-pie: Ensures the output is not a Position-Independent Executable.-z execstack: Makes the stack executable (enables execution of shellcode on the stack).-mno-accumulate-outgoing-args: Disables optimization for outgoing function arguments (x86-specific).
Also note that this binary is not stripped, meaning we can view raw function names (symbols), this is not the case always, stripped binaries are a pain so we have to read instructions to classify functions.
Note that -m32 is to compile the binary as 32-bit, its easier as a starter to debug and read its assembly, dont forget to run sudo apt install gcc-multilib to use the m32 flag.
Later, we will see techniques to bypass these counter measures, real life binary exploitation is not that easy!
Disassembly
Letβs take our python command and export it to a text file so that we can view whatβs happening inside a debugger.
Letβs take our python command and redirect it to a text file so that we can view whatβs happening inside a debugger.
I will use pwndbg a GDB plugin which makes it easier to debug the way to exploiting the binary.
We can examine the programβs insides a bit. First, letβs find all the functions (this would be a lot harder if the binary was stripped but lucky for us, itβs not!)

We can ignore most of the stuff above. The functions weβd like to focus on are main() and overflow(). If we go inside of main, weβll be able to see what the program does when it runs. Inside it, all it does is call our overflow() function.

If youβre having difficulty understanding this output, itβs not as intimidating as it looks. Letβs look at the source code so you can compare the assembly output to the pure source code:
Do you see? Inside of main, all itβs doing is calling our overflow function which is located at the address: 0x8049176. Letβs move on to something more interesting.
Now that we know (I mean we already knew since we compiled the god-damn program but letβs pretend that we were hacking blindly) whatβs inside of the main function, letβs actually disassemble the function that we found inside of it: overflow():

Before going any further, letβs bring back the source code so we can better make sense of the output:
If we examine the output of the disassembled function, we can see that the buffer variable is pushed onto the stack before the call to the read() function; that function is responsible for actually taking in our user input. This is done by moving the address of [ebp-0xd4] to the EAX register (overflow <+17>).
After this, the buffer variable (now the EAX register) is pushed on the stack as an argument for that aforementioned read() function. If we look carefully, we can see all of the arguments that are passed into read() being pushed on the stack. Observe:
So, the first argument of the read function is a 0. We can see this being pushed on the stack at:
Next, we see the buffer variable being passed into the function, which we already just covered:
Lastly, we have the 400 bytes weβre allowed to input into the buffer variable:
How is that the 400? Well, the thing being pushed (0x190) is hexadecimal for 400. We can verify this with:
And thus, we have reverse-engineered all the arguments to the read() function! Pretty fascinating stuff, right?
Stack Overflow
Letβs get even more in-depth and interact with the program. First, letβs create a text file to hold all of our Sβs:

I chose 800 bytes arbitrarily, you can choose whatever. Now, letβs run the program; supplying our input, of course:
Although the bytes I supplied are arbitrary, you should still be mindful of how much you initially supply, it's better to start small and iterate higher and higher.

As expected, weβve got a segmentation fault. Since this is going to be more in-depth than just a simple βdo this after crashing the programβ kind of article, letβs examine what happens to the program before it dies as a form of cyber-pseudo-still living autopsy. To do this, some extremely useful features called βbreakpointsβ are going to be used. These stop or βbreakβ the execution of the program when the program reaches it.
So, letβs set some breakpoints before the call to read(), one right after it, and lastly one on the return (ret) instruction.

Now, if we run the program, it should hit the first breakpoint right before the call to the read function.

We can see from the code section that weβre currently inside the overflow function. We can see this further by examining a couple of addresses at the EIP register:

Nice, this looks like the overflow function and if we recall, our input will be stored inside of the buffer variable which is located at the address of [ebp-0xd4]. We can find the address of this region with the following command:

If we view this address, we wonβt find our βSβs in there yet because remember, weβre at the breakpoint right before the program will take our input and toss it into the region weβre going to be examining (the buffer):

If we continue the execution of the program using c, we can then re-examine this block of memory and we should see our Sβs in there!

Weβre at the second breakpoint now, i.e., right after the call to read(). Which meansβ¦

Nice! We can see that Sβs are all up in here. The next thing we need to take a look at is why the program crashes.
Once the overflow() function is complete, the return address that was pushed onto the stack was meant to restore the rest of the main() function. However, if we hit continue again, we hit the last breakpoint set at the return instruction.
The stack pointer (ESP) holds the top of the stack. So, once we get to our last breakpoint, we can see:

Weβre at the RET instruction and since weβre at RET, whatβs going to happen here is that the ESP register is going to move whatever value is inside of it (i.e., at the top of the stack, normally, this would just be the normal return so that we could restore main()) into the EIP register and since itβs going to be a bunch of Sβs, itβs going to crash.
Letβs step inside the debugger and see if we can catch the moment the EIP gets filled with the value inside of ESP due to the RET instruction:

Itβs just like we thought, the ESP (although it once held a normal and perfectly usable address) took the value inside of it and put it in the EIP register.
Since our overflow had reached way down the stack, the ESP register took what was on top of the stack, a bunch of Sβs, and put that in the EIP register instead, and the EIP register told the program to run the instruction at 0x41414141 , which, as we all know, isnβt a proper memory address, so weβve crashed.

Finding the EIP Offset
The even more dangerous part now is that we can very obviously overflow the stack to the point that after the RET instruction is reached, it makes the ESP register move a completely useless address to the EIP register.
But what if we overflow the program just before we overwrite the return address and instead change the return address, not to a bunch of Sβs, but instead, to some actually useful code, like for instance, some code that we put on the stack.
In order to do that, we need to first figure out the offset until we reach the EIP. We need to generate a cyclic pattern:

Now, letβs run the program using the newly created pattern as our input:

It might be hard to see, but the value stored inside of the EIP register is: 0x63616165. Since we have this address now, we can find the offset using the following command:

Okay, perfect. We know that the EIP can be supplied up to 216 bytes before we overwrite it and destroy it. So, letβs see if we can overwrite the EIP address with a bunch of Bβs:
Itβs good to use the same amount of bytes as you started with and fill the unused bytes with a different character just to make good use of the space and to better see ourselves on the stack.
Letβs run this and if weβve overwritten values properly, we should see that our EIP register holds a value of 42424242 (Bβs in hex):


Ret2Function
Now, all we need to do is supply our own shellcode to abuse this or find a function thatβs stupidly overpowered to hack the program for us. Letβs examine a case where we could populate the EIP with the address of a function left inside of the program to hack it for us.
First, letβs edit our source code:
The only difference between this program and the previous one is the inclusion of the hackme() function which will create a file called hacked.txt.
Now, it wonβt ever get the chance to actually run that function since inside of main(), we never call the function, so this is just dead code inside of the program. Letβs compile this:

So, we can crash the program, crash the program just enough to supply the instruction pointer with a bunch of Bβs, but what else can we do? and more importantly, how much more malicious can we get?
Well, friends, allow me to introduce the concept of EIP control or execution control.
So, the EIP register, what does it do? Basically, the instruction pointer can be summarized in the following:
βThe Program Counter, also known as the Instruction pointer, is a processor register that indicates the current address of the program being executed.β - Program Counter, Embedded Artistry
So if you could imagine, when we filled the EIP register of the four Bβs, those were just junk bytes. 0x42424242 doesnβt mean anything in the context of a usable memory address.
However, what if instead of supplying Bβs or any other letter, we supplied the address of a function, specifically, the function that wouldnβt otherwise get executed.
There lies the beauty of this technique. We get to use our program against itself. First, letβs go ahead and disassemble the function we added:

In this output, we can clearly see that inside the hackme() function, thereβs a call to system() with a push of an address right above it.
That address being pushed, is going to be the argument passed to system. In this case, itβs going to create a text file called hacked.txt. Letβs see if thatβs the value of the argument by examining that address as a string:

Itβs just as we thought! Perfect. Now, letβs move on and actually exploit this program such that we overflow the binary, redirect the EIP to hold the address of the hackme() function, the culmination of which will create a text file in our directory.
So, letβs start off by getting the address of this function:
Remember that this binary is in little-endian. This means that we canβt just supply the address above as is, we need to reverse the byte order which means instead of 0x8049186 , the address is going to be:
See how much more useful this is when we compare it to our βBBBBβ inside of the EIP register? Now, when the EIP gets this value, itβll run the hackme() function, instead of crashing because it doesnβt know what to do with 0x42424242. Letβs replace our payloads:
This turns into:

Now, if we finally run this exploit input, we should see that the exploit forces the binary to run that hackme() function and thus, a file will be created.
First, letβs list our current directory:

See, thereβs no hacked.txt file. Now, letβs open the program up inside of GDB and run the exploit:

Awesome, we can see new processes/programs were spawned which shouldβve created our file:

Now, obviously, creating a file is not that special but could you imagine the devastation if instead of:
There was a function like:
That wouldnβt be good. And thereβs a nice little trick we can use (called the double-cat trick due to STDIN being open in a weird way but thatβs a talk for a different time) to keep the shell open because if we were to redirect the EIP to that ez() function, it wouldnβt be stable enough for us to use the shell.
I hope youβll excuse me for blabbering on and nerding out, but you can see how cool this is. From the basic reverse engineering to delving down deep and hacking this program, I hope you learned/got something from this and I sincerely thank you for reading this!
Extras
Cyclic Pattern
A cyclic pattern generates a unique, non-repeating sequence (e.g., aaaabaaacaaad...) so when a program crashes, the crash value in $eip or $rip points to a specific part of the pattern, allowing you to calculate the exact offset using cyclic -l <value>.
For example, If $eip = 0x6161616c ('laaa' in cyclic pattern), cyclic -l 0x6161616c gives the offset.
GDB/pwndbg Commands
Memory Inspection:
x/[NUM][FORMAT][UNIT] [ADDRESS]:x/8x $eip: 8 hex values at$eip.x/12i $eip: Disassemble 12 instructions.x/s $eax: Show string at$eax.
telescope $rsp: Inspect stack in readable format (pwndbg).
Practice buffer overflow more:
Last updated