ASU CSE 466

Module: Introduction to Binary Exploitation

Because of the lack of memory safety in low-level languages, such as C, memory corruption vulnerabilities manifest quite frequently, to brutal effect. This module will explore a number of different exploitation scenarios, using different types of flaws to achieve control over software.

Slides

The slides for this module are:

Practice

The goal of the challenge sets in this module is to get the flag. There is a /flag file, and you get to choose one binary on which the SUID flag will be set. The binaries that you are allowed to choose are all in the /pwn directory.

There are a number of difficulty levels, but the programs are structured similarly. Each program takes user input on stdin and contains at least one (intentional) vulnerability. If you exploit it, you can get it to read the flag and print it out to you.

The up-shot is this: to read the /flag for a binary, you will have to understand how to exploit it.

CONCEPT: Abusing NULL-termination in strings

One concept we didn’t have time to fully explore in classs non-terminated strings. Consider the following code:

int main()
{
	char name[16] = {0};

	printf("Name: ");
	read(0, name, 128);
	printf("Hello %s!\n", name);

	char color[16] = {0};
	printf("Favorite color: ");
	read(0, color, 128);
}

Obviously, this code is insecure, because we read in way more bytes into name and color than they can hold. However, stack canaries offer some protection against this: because we have to clobber the canary on the way to overwriting the return address, we cannot actually exploit without knowing the canary (so that we overwrite it with the exact value that is currently there).

Luckily, we can leak the canary! In C, strings are null-terminated. This means that, rather than explictly storing a size (such as what is done by Python strings, C++ strings, and so on), they are simply assumed to keep going until the first NULL byte (i.e., 0x00). If the user input in this example is less than 16, there is no problem, because char name[16] = {0}; initializes the array to all NULL bytes, and if the read grabs fewer than 16 characters, the remaining bytes will remain NULL, and the string will be NULL terminated.

What if the user inputs 16 bytes or more? In this case, whatever is after name on the stack (such as the canary value) will be leaked, because printf will just keep printing until it hits a NULL byte to terminate what it thinks is name. We can see an example of this below. In this example, we omit the frame pointer, because that tends to be the default nowadays.

$ gcc -fomit-frame-pointer no-null.c -o no-null
$ echo -n "AAAABBBBCCCCDDDD111122223333444455556666X" | stdbuf -o0 ./no-null | hd
00000000  4e 61 6d 65 3a 20 48 65  6c 6c 6f 20 41 41 41 41  |Name: Hello AAAA|
00000010  42 42 42 42 43 43 43 43  44 44 44 44 31 31 31 31  |BBBBCCCCDDDD1111|
00000020  32 32 32 32 33 33 33 33  34 34 34 34 35 35 35 35  |2222333344445555|
00000030  36 36 36 36 58 74 e5 73  cb 81 15 b5 21 0a 46 61  |6666Xt.s....!.Fa|
00000040  76 6f 72 69 74 65 20 63  6f 6c 6f 72 3a 20        |vorite color: |

A few things:

  1. We use echo -n so that echo does not implicitly print a newline. It is always better to explicitly understand exactly what input you are providing to a program while trying to exploit it.
  2. We use stdbuf to disable output buffering. This lets us use hd without waiting for the program to explicitly flush its output buffer.
  3. We use hd to interpret the hex, rather than have the program output to us directly.
  4. We might expect to only wrote 32 bytes (the size of name plus the size of color) before hitting the canary, but stack padding (the stack is always aligned to 0x10) necessitates a further write. It takes 40 bytes to reach the canary.

Even with this, why do we write that extra “X” bytes? This is necessary because the least significant byte of the canary (which is the leftmost byte in memory) is a NULL byte, specifically to evade these sorts of leaks via non-terminated string output. In this case, we have a powerful enough vulnerability to overwrite just that NULL byte, which will lead to the rest of the canary being printed. In the above case, the canary is 0xb51581cb73e57400 (in little endian)! Now, for the color buffer overflow, we can write the canary back into its place and fully control the return address!

CONCEPT: brute-forcing static memory

Sometimes, there is static memory that you need to leak, and you don’t have a direct leak. A specific application of this is forking programs. Some classes of network applications fork (i.e., copy the process) on every connection. Interestingly, canaries are randomized only when the process starts, but not when it forks. Because forked processes are independent of each other, you can experiment with vulnerabilities in the child process (and overwrite the canary with incorrect values, leading to process termination) without adversely affecting the parent.

Aside from network services, this also happens with Android applications. On Android, every process is forked off of a common process called the Zygote. This weird setup causes all the canaries to be the same, so if you can leak one, you will know them all.

Homework 4 has some customized challenges that use part of the flag for the canary, meaning that the canary is static per binary, allowing you to treat it like one of these cases and brute-force the canary.

So how do you do it? If you overwite the whole canary, you have a 1 out of 72057594037927936 chance of randomly guessing the right value (1/2^56, rather than 1/2^64, because the LSB is always NULL). But, you can do it byte by byte! Consider the leftmost byte (which we know is NULL, or 0x00): if you overwrite it with an incorrect value, the canary check will fail. If you overwrite it with 0, it will succeed. Since you know the leftmost/LSB byte, you can begin to attack the next byte. You can guess a value, see if the process aborts with a canary fail, and if it does, try another value. Since you are brute-forcing one byte, this should take at most 256 tries. Then you move on to the next one. The entire canary can be brute-forced byte-by-byte in 256*7 (1792) tries.

Of course, this only works with static canaries (or other static data that you need to leak without actual output).