Buffer Overflows

Created

Buffer Overflow

#include <stdio.h>
char *gets(char *s);

This code gets reads a single line of input from stdin and stores it in the character array pointed to by s

This code is dangerous because there is no way to ensure that the character array is large enough to store the input.

Given char buf[100], these statements can overflow the storage allocated to buf:

scabf("%", buf);
strcpy(buf, longstr);
snprintf(buf, 100, "%s", longstr);
fgets(buf, 100, stdin);

Memory Safety

C is a memory-unsafe language because it has

  • no bounds checking of array accesses
  • no restrictions on pointers. This is allowed:
     uint64_t x = 0xDEADBEEF;
     char *s = (char *) x;
     strcpy(s, "Hello, world!");
    

Invalid memory references are a big source of bugs in C program. So, what are the consequences?

segfaults

An invalid memory reference will crash the program with a segmentation violation (segfault).

  • Memory references outside a valid region of virtual memory, or which violate access permissions (e.g., store to read-only regions), result in a processor exception handled by the OS kernal. The OS sends a signal taht terminates the running program.

Memory corruption

A worse consequence of an invalid memory store is that data is corrupted.

This occurs when

  • A variable or array element is overwritten
  • A saved register value or temporary value is overwritten
  • A return address is overwritten (this is particularly bad, as we’ll see shortly)

Once a program makes an invalid memory reference, it cannot be trusted to behave correctly, which is why we must use valgrind.

  • valgrind knows which memory spaces is for variables, and it can tell when you are accessing somewhere its not supposed to.

Example:

#include <stdio.h>

void echo(void) {
  char buf[4];
  gets(buf);
  puts(buf);

}

int main(void) {
  printf("Enter a line of text:\n");
  echo();
  return 0;

}
  • char buf[4] is a small buffer, only safe for strings of length 3 or less.
    • Otherwise, it would overwrite the original %rbx value.
  • This program will run successfully, even if it is dangerous
    • The reason it works is because main wasn't using the overwritten %rbx value.
    • However, if the string requires 14 bytes, then the first 2 bytes of the return address would be overwritten. The control returns to a zeroed region of memory. No memory as mapped at address 14, so a segmentation fault occurs.

Vulnerabilities

Security compromises of computer systems cost the U.S. economy many billions of dollars anually

  • Buffer overflows are an important category of security vulnerability
  • But there are many other types of vulnerabilities!

Let's assume that the input sent to the program is untrusted, meaning it was made by a malicious user.

  • Untrusted data is common, especially for network application.

Because of the buffer overflow, the input sent to the program can change the echo function's return address to a arbitrary value. This means that the malicious user has control over which code executes when the function returns.

Executing arbitrary code from the stack

In previous (32-bit) x86 architecture, any region of memory marked as readable is also executable. This means that the attacker can send executable machine code that will be written onto the stack

  • The malicious data must overwrite the return address with the location of the exploit code (on the compromised stack)
  • This requires knowing (or guessing) the stack pointer’s value (so that control “returns” to the code on the stack)

Nop Sled To make arbitrary code execution more feasible, attacker can construct a “nop sled”: a long series of nop (do nothing) instructions leading to exploit code

  • As long as forged return address hits the nop sled, the exploit code will execute
  • This allows the exploit to work (with some probability) even if the exact stack pointer value isn’t known (the guess just has to be “close enough”)

Exploiting existing code

Another way of exploiting a buffer overflow is to overwrite the return address with the address of an instruction in the running program. If the target instruction is chosen carefully, it may be able to cause the execution of an arbitrary function with arbitrary arguments

For example, if the return address is overwritten with a code address leading to the execution of the system function, an arbitrary program could be executed. The exploit must somehow manage to forge argument(s): pop instructions are useful for this

Mitigations

To deal avoid buffer overflows:

  • Write code that doesn’t have bugs
  • Use memory-safe programming languages (Java, Rust). In these programs, these languages check memory references at compie time and/or runtime to ensure only valid memory locations are accessed.
  • Make stack non-executable
  • Address space randomization
  • Detect stack smashing

There are lots of things we can do (but they still can't catch all the bugs)

  • Thorough testing
  • Code reviews
  • Static analysis

Other security improvements:

Make stack non-executable.

  • x86-64 systems allow regions of memory to be marked as non-executable. This can eliminate the possibility of a buffer overflow resulting in arbitrary code execution from the stack.

Address space randomization

  • For exploits which depend on knowing the current (approximate) stack pointer value, the OS kernel can randomly choose where to place the stack in memory
  • Code and data in position-independent executables can be loaded into memory at arbitrary addresses
  • Exploits depending on a return address jumping to a specific instruction become less likely to succeed
  • Address space randomization techniques make exploits more difficult, but don’t make them impossible

Detecting stack smashing:

  • Compiler can generate code to detect improper modification of stack memory.

  • How it works:

    • On procedure entry, store a “stack canary” value near the return address
    • Prior to return, check the canary value
    • If canary was modified, terminate program
  • The canary value is generated randomly and cannot easily be guessed. The return address (in theory) cannot be overwritten

  • Return address (in theory) can’t be overwritten without also overwriting canary value

  • Small runtime overhead incurred on instrumented function calls

  • Enabled by default in recent Linux/gcc