Buffer Overflows

Concepts you’re responsible for in this course:

  • High-level understanding of a buffer overflow attack
  • What a base pointer, stack pointer and instruction pointer are
  • What a stack frame is: How it saves the location of the previous stack frame and instruction pointer
  • How a buffer overflow can overwrite the return address of a previous stack frame
  • How ALSR and NX can protect against arbitrary code execution in the stack

Introduction

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. But for this to work, the attacker needs to know the memory address to write to.

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.

This is a problem for the attacker. If the address of the stack is randomly chosen at runtime. But if the stack location is assigned randomly at runtime, how can the attacker know beforehand where the stack is to return into to it? In the 32-bit address space, the attacker could select a fixed location to try run the exploit over and over and maybe eventually get lucky and hit the stack by random chance. But when we extend this to the 64-bit address space, the probability of success goes down by a factor of 2^32 (i.e., 4 billion times less likely).