In this post I will write about C/C++ code vulnerabilities because avoiding insecure coding practices in the initial stages can minimize the time and effort spent on finding and fixing them in later stages. More specifically, I will talk about implementation errors relating to memory-safety because they are still very common vulnerabilities used by attackers to gain control over the execution-flow of an application. These vulnerabilities are included in the list of most dangerous software errors. By carefully crafting an exploit for these vulnerabilities, attackers can make an application transfer execution-flow to code that they have injected. Such code injection attacks are among the most powerful and common attacks against software applications, i.e. code injection attacks allow an attacker to execute foreign code with the privileges of the vulnerable program. To exploit a vulnerability and execute a code injection attack, an attacker must: find a bug that can allow an attacker to overwrite interesting memory locations, find such an interesting memory location (stored code addresses, function pointers, data pointers), copy target code in binary form into the memory of a program (can be done easily, by giving it as input to the program) and use the vulnerability to modify the location so that the program will execute the injected code. Security vulnerabilities in C/C++ programming languages that will be described in this post and that can directly or indirectly cause code injection attacks are:
Buffer overflow
Format string vulnerability
Integer errors
In C/C++ programming languages memory is allocated in multiple ways: automatic (local variables in function), static (global variables) and dynamic (malloc or new). Programmer is responsible for correct allocation and deallocation in the case of dynamic memory and appropriate use of the allocated memory (bounds and type checks). It is important to say that memory management is very error prone. Typical bugs are: writing past the boundaries of the allocated memory, dangling pointers (pointers to deallocated memory), double free (deallocating memory twice) and memory leaks (never deallocating memory). Figure 1 show segments of process memory, that is important because different attacks exploit different segments.
Figure 1. Process memory layout
Buffer overflow happens when the data gets written beyond the boundaries of an array (buffer). This way data gets written to a portion of memory which does not belong to the program variable that references the array. Buffer overflow can occur by using an unsafe copying function (e.g. strcpy), through integer errors or by looping over an array using an index which may be too high. Code that contains of buffer overflow:
void function(char *input)
{
char str[80];
strcpy(str, input);
}
int main(int argc, char **argv)
{
function(argv[i])
}
Problem in this example is strcpy function. Strcpy has no way of knowing how large the destination buffer is (i.e. there is no length parameter) so using it can lead to overrunning the buffer and corrupting other memory.
Most popular buffer overflows are:
Stack-based buffer overflow
Heap-based buffer overflow
Main difference is where the buffer being overwritten is allocated and techniques for gaining control of execution-flow. Stack-based buffer overflow attacks buffer on the stack and is exploitable by return address, frame pointer or indirect pointer overwriting. Heap-based buffer overflow attacks buffer on the heap and is exploitable by function pointer, data pointer, virtual function pointer overwriting or more generally overwriting memory management information. It is worth noting that other overflows exist as well like overflows in the data and bss segments. Data segment contains global or static compile-time initialized data and bss contains global or static uninitialized data. Overflows in these segments can overwrite function and data pointers stored in the same segment or data in other segments. Functions that can cause buffer overflow are: gets, strcpy, strcat, sprintf.
Format string vulnerability occurs if an attacker is able to specify the format string to a format function. If the format string that is received differs from that which is expected, such as being longer or shorter than the allocated data space, the program may crash, quit or make up for the missing information by reading extra data from the stack; allowing the execution of malicious code. Format functions are functions that have a variable amount of arguments and expect a format string as argument. This format string will specify how the format function will format its output. The format string is a character string that is literally copied to the output stream unless a % character is encountered. This character is followed by format specifiers that will manipulate the way the output is generated (format specifier particularly interesting to attackers is %n). When a format specifier requires an argument, the format function expects to find this argument on the stack. Some examples of format function, which if not treated, can expose the application to the format string attack are: fprint, printf, sprintf, snprintf, vfprintf, vprintf.Example code of format string vulnerability:
void foo(char *val) { printf(val); } int main(int argc, char **argv) { char buf[100] = "Hi %x %x "; foo(buf); return 0; } Problem with code is that printf in foo will print content of stack. This can happen because printf accept variable number of arguments and does not check for mismatch of number of format specifiers and arguments passed after format string.
Integer errors are not exploitable vulnerabilities by themselves, but exploitation of these errors could lead to a situation where the program becomes vulnerable to one of the previously described vulnerabilities. Two kinds of integer errors that can lead to exploitable vulnerabilities exist: integer overflowsand integer signedness errors. Since an integer is a fixed size, there is a fixed maximum value it can store. When an attempt is made to store a value greater than this maximum value it is known as an integer overflow. This can cause a program that does not expect this to fail or become vulnerable: if used in conjunction with memory allocation, too little memory might be allocated causing a possible heap overflow. Signedness errors occur when an unsigned variable is interpreted as signed, or when a signed variable is interpreted as unsigned. This type of behavior can happen because internally to the computer, there is no distinction between the way signed and unsigned variables are stored. This can lead to a situation where a negative argument passes a maximum size test but is used as a large unsigned value afterwards, possibly causing a overflow if used in conjunction with a copy operation (e.g. memcpyexpects an unsigned integer as size argument and when passed a negative signed integer, it will assume this is a large unsigned value). Example code of integer overflow:
int main(int argc, char **argv) { int val = 0x7fffffff; /*2147483647*/ print("%d ", val); val = val + 1; print("%d , val"); }
Variable val is initialized with the highest positive value a signed long integer can hold (2147483647). Adding 1 to the hex value of 0x7fffffff the value of the integer overflows and goes to a negative number (0x7fffffff + 1 = 80000000), in decimal this is -2147483648. Problem is that compilers will not detect this and the application will not notice this issue.