This is the second post on buffer overflow attacks! Feel free to check out Buffer Overflow Attacks (Part 1) as well.

In this post, we’ll have to deal with “undefined behavior” in the C programming language. It cannot be stressed enough that “undefined behavior” is not “implementation defined behavior” (or some other similar term). Undefined behavior gives license to the implementation to do very dangerous things, and is a potential source of many security vulnerabilities.

A lot of seemingly reasonable reasonable things in C have undefined behavior, and the compiler may not generate any warnings. For an excellent explanation of this topic, I strongly recommend “What Every C Programmer Should Know About Undefined Behavior” by Chris Lattner.

Anyway. Let’s start with some code. Here we have overflow.c:

#include <stdio.h>

void f() {
  puts("greetings from f()!");
}

int main() {
  void * volatile buffer[1];
  buffer[3] = (void *) f;

  return 0;
}

You might be wondering about the volatile keyword. Its goal is to avoid certain compiler optimizations (in this case, dead code elimination because buffer written to, but never read). Alternatively, we can disable most optimizations with the -O0 compiler flag. But why rely on certain compiler flags, when can just add volatile to the source code (to ensure the behavior that we want in this post)?

Let’s compile and execute:

gcc -Wall -O3 overflow.c -o overflow
./overflow

On my computer this code compiles (but not without some warnings). It crashes when executed, but not before outputting “greetings from f()!”.

Huh?

Before explaining this, let’s be fair and admit that you may have seen a different output. Overflowing a buffer is undefined behavior in C, so the output depends on what the compiler did with our source code. If you see a different output, changing buffer[3] to another value may help.

To understand the behavior of the implementation, we’ll use GDB debugger to step through its CPU instructions. Although GDB is the standard debugger for Linux, unfortunately it’s not intuitive nor user-friendly. Its main shortcoming, it turns out, is that GDB is really bad at visualizing what’s going on.

Luckily there is GEF, which is one single GDB script file that fixes all these problems (and a lot more). Credit should be given its predecessor PEDA, which is another fantastic tool. Anyway, let’s install GEF:

bash -c "$(curl -fsSL http://gef.blah.cat/sh)"

Let’s start the debugger and run a few commands:

gdb -q overflow

disass main
break main
run

The command stepi will advance one instruction, and we can just keep hitting “enter” to repeat the last command.

At this point, it may be necessary to get a bit familiar with registers and assembler instructions. We’ll only see a few here, actually. They include arithmetic and logic operations (add, sub, xor), load and store of either values (mov) or addresses (lea for “load effective address”), and conditional jumps (jne for “jump if not equal”). The Intel syntax is used (thank you GEF!), so the first operand is the destination, and the second operand is the source. (The same convention used by most C library functions like strcpy().)

We also see the most interesting instruction of all (at least for this post): ret (for “return”). This instruction jumps to whatever address is at the top of the stack. (The rsp register points to the top of the stack.) To be precise, the ret instruction also removes the top element of the stack, so ret is actually a combination of jmp and pop.

Due to a confluence of unfortunate circumstances, we see that the address that the main() function is supposed to return to, can be accessed (and overwritten!) using buffer[3]

It’s a good idea to play around with the value 3 in buffer[3] and see what happens. Interestingly, gcc generates stack protection code (known as a stack canary or a stack cookie) that terminates the program if buffer[1] is overwritten…

Let’s see what happens when we use clang instead of gcc:

clang -Wall -O3 overflow.c -o overflow
./overflow

No output apparently! (Although -O0 would have given the same output.) Maybe clang provides better protection (with default compiler flags) against buffer overflow attacks?

Alas, it turns out that clang doesn’t generate stack protection code (with the default compiler flags). The next post will show a buffer overflow (with shellcode) that works on my system when compiled with clang. Stay tuned!

Buffer Overflow Attacks (Part 2)