Stack Based Buffer Overflow

Previous Reading:

  • Stack Frames, Calling A Function, and Function Prologue

Cause:

  • Unbounded input

Key Terminology:

  • Unbounded Check

  • Return Address

  • Stack Frame

  • Instruction Pointer

What Is A Stack Based Buffer Overflow

A buffer overflow is a general name for any type of exploit that writes data past the room that it's been given. The simplest of these to understand is a 'stack based buffer overflow', and is what this tutorial will focus on. A buffer overflow is caused by a program accepting more input than it can handle. Often times a program that is vulnerable to a buffer overflow is vulnerable because it accepts 'unbounded input', meaning that there is no (practical) limit on the amount of input the program accepts, however in other cases there is a limit, but the limit is not stringent enough to stop the exploit.

In this tutorial, we will learn about an overflow that happens on the stack, as part of a function's local array.

Overwriting Data On The Stack

A very common thing for programs to do is to accept user input, and store it in a local array. For example, a scanf() might be used in someone's code to store user input in a local array, for example in something like buffer[8] = { 0 }. What the author is intending is for the user to be able to give 8 bytes of input, for the program to accept and store that input in buffer[], and then for the program to use that input as it continues to run. But what happens if we give 9 bytes of input? 10? etc?

In a well written program, nothing happens, there’s is a ‘bounds check’ and when the input surpases the bounds of the expected input the rest of the input is ignored. In an insecure program, that data will continue to get  written to the stack. However, that means we are writing (theoretically) infinite input into the 8 byte array buffer[], so where does the 9th byte get placed? 10th? etc? It overwrites other values on the stack.

If the author does not specify when or how the input function will stop taking user input, it will just continue to do so. Now, even though there is only an 8 byte array, the input will start writing to:

buffer[8];

buffer[9];

buffer[10];

etc.

We are now indexing outside of our array, which is not something you are supposed to do. Realistically, you are just reaching other values at this point. As you continue to write, you will continue to overwrite more and more values in the function's stack frame. If you vomit in a crap ton of input, you will overwrite local variables, the base pointer, the return address, arguments, and whatever is past that. If you input something along the lines of 10,000 characters, you will eventually overwritten so much stuff that the program will not be able to function properly and crash. But in this case what specifically is causing the program to crash? If we can overwrite data on the stack, but still keep the program running, that'd be pretty sweet; and we certainly can.


If you just vomit garbage data to overwrite what is currently on the stack, most likely your target program is crashing because the stack frame's return address was replaced with garbage. (Note that 'garbage' is both slang and a pseudo technical term to denote placeholder characters whose value doesn't matter.) What is happening is that the function continues to execute, and then attempts to return to it's calling function by referencing its return address on the stack. But if it's return address has been replaced with '0x41 0x41 0x41 0x41' ('AAAA'), code execution will be transferred to that address, where it is not set up to transfer code execution to, and because of this the program will crash.

That's interesting though. So we actually forced the program to execute code at a different location that it's author had intended. If we can just overwrite the return address with anything we want, we can point it anywhere we want, and run anything we want! Hijacking the return address is a common beginners level exploit and CTF challenge, however there are other goals that can be achieved with a stack based buffer overflow as well.

In order to tactfully implement a stack based overflow, we need to visualize and count out the target function's stack frame. As was discussed in Stack Frames, Calling a Function, and the Function Prologue, when a function is called and it's prologue executed, it's stack frame looks as follows:

  • arguments

  • return address

  • base pointer

  • local variables

On a 32 bit system that's an unknown amount of arguments, a 4 byte return address, a 4 byte base pointer, and an unknown amount of local variables. In an actually example, we could just use a debugger to actually look at how many arguments and local variables there are. In fact, the following is a full example of what you might expect to see on the stack, keep in mind the little endianness of it reverses the value's byte's order:

  • 0x01 0x00 0x00 0x00    <- Argument of '1'

  • 0xf8 0x55 0x55 0x56    <- Return Address

  • 0xa2 0x80 0xff 0xff    <- Base Pointer

  • 0x04 0x00 0x00 0x00    <- int a = 4;

  • 0x02 0x00 0x00 0x00    <- int b = 2;

  • 0x00 0x00 0x00 0x00

  • 0x00 0x00 0x00 0x00    <- char buffer[8] = { 0 };

Notice how the return address points to a location very far away from where the base pointer points on the stack, because the return address points to the .text section of the running program, while the base pointer points slightly back on the stack. Also notice how these ints are given 4 bytes to store their values in, however they only needed to modify one, despite this they still are given the full amount of bytes they need. Also notice how the 'start' of the array, it's 0th element, is closer to the top of the stack than it's final element, it grows upwards back up the stack. This is actually the exact mechanism that allows us to perform a stack based buffer overflow. Since the array grows back upwards, as we keep continuing to write we can overwrite additional portions of the stack frame.

Overwriting Specific Values On The Stack

In theory we now have this cool tool, a buffer overflow, with which we can wreak havoc with! But so far all we've managed to do is crash the program by mashing the 'A' key. What specifically is being overwritten with the 9th byte of our overflow, the 20th? We will count this out. Let's recycle the previous example, but this time input 9 'A's (one more than expected) bytes into our target program.

  • 0x01 0x00 0x00 0x00    <- Argument of '1'

  • 0xf8 0x55 0x55 0x56    <- Return Address

  • 0xa2 0x80 0xff 0xff    <- Base Pointer

  • 0x04 0x00 0x00 0x00    <- int a = 4;

  • 0x02 0x00 0x00 0x41    <- int b = 1090519042;

  • 0x41 0x41 0x41 0x41

  • 0x41 0x41 0x41 0x41    <- char buffer[8] = { 0 };

Our 'A's are visible as 0x41's (because they are stored as hex values) on the stack. We can see we filled buffer[] all with A's,  but then we've started to overwrite another local variable, the int b. In fact, b used to equal 0x00000002 (decimal 2), but now equals 0x41000002 (decimal 1090519042).

Continuing to count from the bottom right back up the stack (Since it grows downwards like an icicle), we can see that the 9th, 10th, 11th, and 12th bytes will all overwrite int b. Then the next 4 will overwrite int a. Let's do this one more time, this time with 16 A's.

  • 0x01 0x00 0x00 0x00    <- Argument of '1'

  • 0xf8 0x55 0x55 0x56    <- Return Address

  • 0xa2 0x80 0xff 0xff    <- Base Pointer

  • 0x41 0x41 0x41 0x41    <- int a = 1094795585;

  • 0x41 0x41 0x41 0x41    <- int b = 1094795585;

  • 0x41 0x41 0x41 0x41

  • 0x41 0x41 0x41 0x41    <- char buffer[8] = { 0 };

We've overwritten all of our local variables. After 8 + 4 + 4 = 16 bytes of input that we just provided, we will start overwriting pointers. The 17th, 18th, 19th, and 20th byte will overwrite our base pointer. Then, finally the 21st, 22nd, 23rd, and 24th bytes will replace our return address. (And if we continue, the function's arguments.)

Hijacking The Return Address

We've now calculated that the return address is 24 bytes out from where buffer[] starts. In other words, we need to input 20 'garbage' characters (characters whose value doesn't matter, they are just placeholders) and then 4 bytes to overwrite the 4 bytes of the return address on the stack. Before we get into what value we actually want to overwrite the return address with, we will want to test that our math is correct. Using the previous example, we will input 24 'A's and 4 'B's:

  • 0x01 0x00 0x00 0x00    <- Argument of '1'

  • 0x42 0x42 0x42 0x42    <- Return Address

  • 0x41 0x41 0x41 0x41    <- Base Pointer

  • 0x41 0x41 0x41 0x41    <- int a = 1094795585;

  • 0x41 0x41 0x41 0x41    <- int b = 1094795585;

  • 0x41 0x41 0x41 0x41

  • 0x41 0x41 0x41 0x41    <- char buffer[8] = { 0 };

That is aligned exactly as we want. Now we need to figure out what to change the return address to. While we haven't really shown you any code or given a realy example program here so that is difficult, but a common thing to do is to make the return address point at a different function. For example, if main() called function_a(), it's return address will resume execution in main(). However we can overwrite that with, for example the address of function_b(), so instead of returning back to the calling function main(), it just transfers execution to function_b(). If we want to overwrite the return address with the address of another function, by definition it will be pointing to the .text section. Below is a new simple example:

#include <stdio.h>
void win(void)
{
    printf("You win!\n");
}
void lose(void)
{
    buffer[8];
    scanf(“%s”, buffer);
    printf("You suck.\n");
}
int main()
{
    lose();
    return 0;
}

If you were to compile and run this code, it would call lose(), tell you "You suck." and then exit. However if you overwrote the return address of lose() to point to win(). The program would (still) tell you "You suck.", but then it would return to win() and tell you "You win!".

Payload Injection And Endianness

We want to replace the return address that currently transfers execution back to main(), to instead transfer execution to win(), so we need to replace it with the address of win. The address of this function can be located in a debugger, if the program is already loaded up to begin running, a quick disassembly of the win() function should provide the exact address you need.

The common way to inject payloads with CTFs is with Python, and often using the pwntools library, we will not get into the later right now. The type of input the program expects changes how you inject your payload. For example, stdin versus an expected argument, and local or remote payload injects will all require different variants of python scripts. This Stack Overflow guide is an excellent start and working reference to which type you should rely on and when (https://reverseengineering.stackexchange.com/questions/13928/managing-inputs-for-payload-injection).

Endianness is important with payload injection. It doesn't alter the order in which values are ordered, but it does mean each of those individual values are written backwards in their own spot. Using that Stack overflow example, and this knowledge of endianness, lets say we want o redirect our previous example with all of the 'A's and 'B's to address 0x56555540, and lets say the program take input in the for of an argument that is later written to a local variable, that would look like this.

./function_name $(python -c "print 'A'*20 + '\x40\x55\x55\x56'")

Notice a few things here:

  • The script is being provided as an argument as we are using the './' operator to run our binary.

  • Python's -c command for making those hex value literal hex values (or else they'll be input as ASCII and converted to hex and the values will be wrong)

  • Python uses '\x' to denote a hexadecimal value

  • The address we are writing is backwards because of endianness

Vulnerable Functions

Alright, so you are now aware of the absolute basics of how a stack based overflow works. What has not been explained is how to find them. Understanding a stack based buffer overflow is more or less useless if you can't audit code and point out yourself the potential for this to take place. Within the C coding community, there are multiple potentially vulnerable functions that are generally handled with care because they otherwise leave the potential for a buffer overflow to take place, even if only by a single byte. The below list is in no way a comprehensive list, but a good place to start for considering functions used in a potentially vulnerable way:

  • gets() - Has no bounds checking whatsoever. Many people recommend never using this function.

  • scanf() - If no bound is specified by the user, it will not perform a bounds check.

  • printf() - Yes, this also allows for input using the %n (not ‘\n’) specifier. If there is no format specifier, printf()’s can be dangerous. ‘Vulnerable printf()’s’ are a topic for another day though.

  • strcpy() - When the source string is copied to the destination string, so is the string’s null terminating byte. This means that the destination needs to be able to hold the anticipated max string length + 1, and is a common mistake.

Not Stack Based Buffer Overflows

We won’t be diving into this topic here, but you should note that this whole tutorial has been on ‘stack based buffer overflows’. That first caveat of the title, being ‘stack based’, is because there are in fact other types as well. Another obvious common overflow type that occurs in CTFs is a ‘heap based buffer overflow’, just keep in mind that buffer overflows are a more general topic, even though this article teaches about them using ‘stack based’ examples.

Summary: Stack Based Buffer Overflow

Unbounded user input writes over other important data once it goes beyond its designated alloted destination space. When this happens on the stack, it allows the opportunity any part of a function’s stack frame to be overwritten. A common technique is to overwrite the return address with the address of a different function and therefore hijak a program’s execution to that different function instead upon the vulnerable functions return. There are a variety of common functions in C that can be used in vulnerable ways. In general, especially during CTFs, opportunities for user input should be scrutinized thoroughly for the potential for exploitation.

Practice Exercise:

Compile this C code yourself using GCC and inspect the buffer_overflow_example’ binay yourself in GDB. Inspect overflow_me()’s stack frame and count out how many bytes it will take to hijak the return address. Hijak the return address to transfer code execution to win(), writing your injection mechanism in python. You should use GDB to do this, to get you started. some useful gdb commands for this will be:

    Commands                                                               Examples
  • disass [FUNCTION NAME]                                  disass main
  • start                                                                         start
  • info registers                                                         i r
  • x/[INSERT NUMBER]xb $[INSERT REGISTER]     x/4xb $ebp
  • si                                                                               si
  • c                                                                               c
  • bp                                                                            bp

Compiler options:

gcc buffer_overflow_example.c -o buffer_overflow_example -m32 -fno-stack-protector -O0

Code:

#include <stdio.h>
void win(void)

{
    printf("You win!\n");
}
void overflow_me(void)
{
    char buffer[8] = { 0 };
    scanf(“%s”, buffer);
    
    printf(“overflow_me is vulnerable as heck!\n”);
}
int main()
{
    overflow_me();
return 0;
}