Buffer Overflows

In order to understand how buffer overflows work, we must first understand how memory is laid out.

We’re going to work in the 32-bit x86 architecture because it’s a little simpler to learn about. However, the 64-bit x86-64 architecture is more common in computers today. This is also different that the ARM architecture used by many IoT devices. Nevertheless, the principles of buffer overflows remain the same across architectures.

Layout of memory

Programs are laid out in virtual memory and are organized by a memory map containing several key areas such as the stack and heap. Virtual memory is an abstraction managed by the OS that allows the program to occupy the entire addressable memory space as if it was the only process on the computer.

When a program executes, the OS loads everything it needs into memory and lays it out in following way:

(Highest)
0xffffffff  |------------|
            |   kernel   | holds environment variables,
            |            | command-line arguments, etc.
            |------------|
            |   stack    | holds local variables 
            |------------|          
            |            |          |
            |            |         \ /
            |            |          v
            |            |          direction of stack growth
            |            |
            |            |
            |            |          direction of heap growth
            |            |          ^
            |            |         / \
            |            |          |
            |------------|          
            |    heap    | holds dynamically allocated data
            |------------|
            |    data    | holds global and static variables
            |------------|
            |    text    | holds executable code (read only)
0x00000000  |------------|
(Lowest)

Stack Frame

A stack is a last-in first-out data structure. In this case you can think of the stack as being a little bit like a notebook that can be used to keep notes. A stack frame is a region of the stack which is dedicated to the execution of a particular function.

When a new function is called, things are added to the stack. When a function completes, all those things are removed from the stack. The program needs to keep track of a several important things:

  1. Where the current function’s stack frame begins
  2. Where the current function’s stack frame ends
  3. What instruction the current function is to execute next

To keep track of this, the system uses the following hardware registers:

  • ebp: the base pointer (also called the frame pointer), which points to the bottom of the stack frame. It is fixed within the current stack frame.
  • esp: the stack pointer, which points to the current top of the stack frame. It changes (downward) as the stack grows.
  • eip: the instruction pointer, which points points to the next instruction (in the text region) to be executed.

The relationship between the registers and memory looks like this:

CPU registers:              ebp  esp  eip
                              |    |    |
                              |    |    |
Memory address                |    |    |
(High)      |            |    |    |    |
            |            |    |    |    |
            |------------|<---/    |    |
            |  current   |         |    |
            |   stack    |         |    |
            |   frame    |         |    |
            |------------|<--------/    |
            |            |              |
            |            |              |
            |     .      |              |
            |     .      |              |
            |     .      |              |
            |            |              |
            |            |              |
            |------------|              |
            |            |              |
            |   text     |              |
            |   region   |              |
            |            |              |
            |            |<-------------/
(low)       |            |

Calling a Function

When a function is called, the system creates a new stack frame. The new function becomes the current function and the previous function is called the calling function.

The system must now do two things:

  1. Save the values of ebp, esp and eip, of the calling function:
    • Any arguments for the new function are pushed on the stack.
    • The calling function’s eip and ebp are then pushed to the stack (in fact all the CPU’s registers are pushed to the stack, but we’ve omitted them for brevity).
  2. Set the values of ebp, esp and eip of the current function:
    • The calling functions esp + 4 bytes becomes the current functions ebp
    • Space for current function’s local variables are allocated onto the stack, and current function’s esp grows downwards accordingly.

The memory now looks like this:

CPU registers:              ebp  esp  eip
                              |    |    |
                              |    |    |
Memory address                |    |    |
(High)                        |    |    |
            |            |    |    |    |
    Calling |   ...      |    |    |    |
 function's |   arg 3    |    |    |    |
      stack |   arg 2    |    |    |    |
      frame |   arg 1    |    |    |    |
            | eip (prev) |    |    |    |
            | ebp (prev) |<---/    |    |
    Current | local var1 |         |    |
      stack | local var2 |         |    |
      frame | local var3 |         |    |
            |------------|<--------/    |
            |            |              |
            |     .      |              |
            |     .      |              |
            |     .      |              |
            |            |              |
            |------------|              |
            |            |              |
            |            |<-------------/
            |   text     |              
            |   region   |              
            |            |              
            |            |
(low)       |            |

Writing strings to Memory

Suppose during the execution of the current function a string is copied into memory. Strings have the interesting property that they are copied from low to high addresses. If you combine this with a buffer overflow vulnerability (the ability to write beyond the bounds of allocated memory), you could potentially overwrite the calling function’s return address.

For example, if we had a C program that looked like this:

#include <string.h>

void foo()
{
  char buf[12];
  gets(buf);  // no bounds checking
}

int main (int argc, char **argv)
{
   foo();
   return 0;
}

Then when foo() is initially called, memory looks like this:

CPU registers:              ebp  esp  eip
                              |    |    |
                              |    |    |
Memory address                |    |    |
(High)                        |    |    |
            |            |    |    |    |
            |     .      |    |    |    |
     main's |     .      |    |    |    |
      stack |     .      |    |    |    |
      frame |     .      |    |    |    |
            | eip (prev) |    |    |    |
            | ebp (prev) |<---/    |    |
      foo's | buf[8-12]  |         |    |
      stack | buf[4-7]   |         |    |
      frame | buf[0-3]   |         |    |
            |------------|<--------/    |
            |            |              |
            |     .      |              |
            |     .      |              |
            |     .      |              |
            |            |              |
            |------------|              |
            |            |              |
            |            |<-------------/
            |   text     |              
            |   region   |              
            |            |              
            |            |
(low)       |            |

Suppose we were to input the string AAAABBBBCCCCDDDDEEEE. Since there is no bounds checking with the gets() function, the eip and ebp of main will be overwritten!

CPU registers:              ebp  esp  eip
                              |    |    |
                              |    |    |
Memory address                |    |    |
(High)                        |    |    |
            |            |    |    |    |
            |     .      |    |    |    |
     main's |     .      |    |    |    |
      stack |     .      |    |    |    |
      frame |     .      |    |    |    |
            |    EEEE    |    |    |    |
            |    DDDD    |<---/    |    |
      foo's |    CCCC    |         |    |
      stack |    BBBB    |         |    |
      frame |    AAAA    |         |    |
            |------------|<--------/    |
            |            |              |
            |     .      |              |
            |     .      |              |
            |     .      |              |
            |            |              |
            |------------|              |
            |            |              |
            |            |<-------------/
            |   text     |              
            |   region   |              
            |            |              
            |            |
(low)       |            |

When the function foo returns, the current stack frame is unallocated, and the contents of the previous eip are loaded into the eip register, in this case is the string “EEEE” (i.e., \x69\x69\x69\x69), meaning the next instruction the program will execute is whatever lives at address 0x69696969.

Ok so here’s an idea. What if we created a buffer overflow that overwrites the return address of the instruction pointer such that it returned into the stack? It would start to execute whatever was in the stack! If you loaded the stack with malicious code, you cause this code to run!

Buffer Overflow Attacks

Buffer overflows on the stack are a common exploitation method. Learn the basics in this buffer overflow attack video

A typical objective of such an attack is to inject malicious code to provide the attacker with a shell. This code is called shell code.

Why stack overflows work:

  • Stack fills variables down (high to low)
  • Strings fill up (low to high)
  • Return address at highest memory
  • Buffer overrun fills up to overwrite return address

Protections: Non-executable Memory

Basic buffer overflow exploits arise from the unintended execution of (read-only) data. One protection is to explicitly designate data as non executable (NX), however even with this protection there are ways to boostrap “gadgets” out of pieces of executable code to overcome this, including return-oriented programming and return-to-libc attacks.

Protections: Address Space Layout Randomization

Recall that our buffer overlflow attack was exploited by getting the program to jump to an address of the attacker’s choosing. Recall in the passcode challenge the attacker exploits that fact that they can write to any memory address, and they use this ability to overwrite the address of fflush in the Procedure Linkage Table. So when the program tries to call fflush, it actually ends up calling the cat flag system instruction instead. Uh oh!

But for this to work, the attacker needs to know the memory address to write to. Recall we found this by running:

(gdb) disas fflush
Dump of assembler code for function fflush@plt:
=> 0x08048430 <+0>:	jmp    *0x804a004
   0x08048436 <+6>:	push   $0x8
   0x0804843b <+11>:	jmp    0x8048410
End of assembler dump.

The attacker needs to place the address of the cat flag instruction into address 0x804a004. Similarly we can find the address of this system call:

(gdb) disas login
...
0x080485ea <+134>:	call   0x8048460 <system@plt>
...

So as you see the attack is highly dependent on the specific memory address. So if we could somehow make these addresses random, we could make such attacks much harder. Address Space Layout Randomization (ASLR) provides such protection against buffer overflows.

You can check if your system supports ASLR by typing:

cat /proc/sys/kernel/randomize_va_space

A value of 0 means ASLR is not present, whereas 1 and 2 are respectively support for “conservative” and “full” randomization. In order to exploit ASLR you must compile your program to as a “position independent executable” (PIE) using the following compiler flags:

gcc -pie -fPIE <yourprogram.c>

We can test this out on the username.c program from assignment 1:

gcc -pie -fPIE username.c -g
chmod +x a.out
gdb a.out

By default GDB turns off ASLR for debugging, however you can re-enable it as follows:

(gdb) set disable-randomization off

Now let’s set a breakpoint in the main() function. Run the program and disassemble the pin function and look for the <system@plt> call. We get something that looks like this:

0x8005d76e <+94>: 	call	0x8005d510 <system@plt>

But now let’s stop the debug and run it again. We get something different, e.g.:

0x8004976e <+94>: 	call	0x80049510 <system@plt>

And if we run it again:

0x800eb76e <+94>: 	call	0x800eb510 <system@plt>

This is a problem for the attacker. If the address of the system call in memory is randomly chosen at runtime, how can the attacker know where it will be to jump to it?