Exploiting Vulnerabilities Part 1
You can access the slides 🖼️ for this lecture.
Here we are going to see how the memory safety violations we have discussed can constitute security vulnerabilities that can be exploited by attackers to subvert programs.
Memory Unsafety in C/C++
As we have seen, C and C++ are not memory safe. Programming mistakes may introduce memory errors and other bugs that are hard to detect and debug. When these bugs are present, there is often no error or warning reported at compile time, and the issue could further be completely silent at runtime. Beyond leading to program crashes or misbehaviour, these bugs can also constitute security vulnerabilities that can be exploited by attackers to break all aspects of the confidentiality/integrity/availability triad. Exploiting these bugs lets attackers leak and tamper with sensitive data, escalate privileges, take over the execution flow of programs, and disturb or crash applications and systems.
This represents a significant problem. In fact, a few years ago both Microsoft and Google reported that about 70% of their security bugs were due to memory safety violations.
Let’s have a look at a first example of a vulnerable program, where sensitive data is leaked.
Example 1: Infoleak
We assume the following scenario. We have a program that is distributed in binary-only form (that’s how most Windows proprietary applications are shipped). It contains some sensitive data: a password. An attacker has access to the binary only, not the source, and aims to figure out the password.
The program’s source originally is:
Original code:
char *welcome_message = "Hi there! How is it going?\n"; // 27 characters
char *password = "secret";
char entered_password[128];
int main(int argc, char **argv) {
for(int i=0; i<27; i++) // Print welcome message character by character
printf("%c", welcome_message[i]);
printf("Please input the password:\n");
scanf("%s", entered_password);
if(!strcmp(entered_password, password)) {
printf("Password ok!\n");
/* ... */
} else {
printf("Wrong password! aborting\n");
}
return 0;
}
The code prints a welcome message that says "Hi there! How is it going".
The message is printed character by character; it is not particularly optimal, but we need that for the sake of the demonstration.
Then it prompts the user for the password.
If the password is correct, it goes on to execute more code; if not, it prints an error message and exits.
You may already notice a very bad security practice: the password is hardcoded in plaintext in the binary.
Now imagine that the company making the program updates the code and shortens the welcome message to be "Hi there!".
The new version of the program is:
char *welcome_message = "Hi there!\n"; // shortened message, only 11 chars now
char *password = "secret";
char entered_password[128];
int main(int argc, char **argv) {
for(int i=0; i<27; i++) // Oopsie! forgot to update that bit of the code
printf("%c", welcome_message[i]);
printf("Please input the password:\n");
scanf("%s", entered_password);
if(!strcmp(entered_password, password)) {
printf("Password ok!\n");
/* ... */
} else {
printf("Wrong password! aborting\n");
}
return 0;
}
One issue here is that the programmer forgot to update the number of iterations of the loop printing the welcome message character by character. On a large and complex code base, that’s something that could happen. So now the printing loop is going to overflow the welcome message, and print on the standard output what is located in memory right after the welcome message:
At build time, when the compiler and the linker decide the layout for static memory, they will place all constant strings such as "Hi there!\n" and "secret" together in a read-only section.
The order in which constant strings are placed one after the other in that section is generally the order in which the compiler encounters them in the source code, so the chances of the password "secret" being located right after the overflowed buffer "Hi there!\n" are very high.
As a result, when the buffer is overflowed and what is past it gets printed on the command line, it is quite likely that the password itself will leak this way:
$ ./infoleak-updated
Hi there!
secretPlease inPlease input the password:
Example 2: Sensitive Data Tampering
To investigate a second example, we now assume a scenario with a similar program performing a password check, distributed as a binary only so the attacker does not have access to the sources. The attacker does not know the password and wants to bypass the password check.
Our vulnerable program is:
char user_input[32] = "00000000000";
char password[32] = "secret";
int main(int argc, char **argv) {
if(argc != 2) { printf("Usage: %s <password>\n", argv[0]); return 0; }
strcpy(user_input, argv[1]);
if(!strncmp(password, user_input, strlen(password))) {
printf("login success!\n");
/* do important stuff ... */
} else {
printf("wrong password!\n");
}
return 0;
}
This time the user passes the password attempt as a command line argument to the program.
That password attempt is copied into the user_input buffer with strcpy.
The content of that buffer is compared with strncmp to the correct password, and if they match the authentication succeeds.
Where is the vulnerability in this program?
Focus on the call to strcpy, as we saw previously that function will copy the entirety of the source string independently of its size.
So if the user passes to the program a password attempt whose size is larger than that of the destination buffer (32 bytes), strcpy will overflow user_input and start writing past that buffer in memory.
As in the previous program, because of how variables are declared, it is likely that the compiler will place the correct password right after the user_input buffer.
So the attacker has the ability to overwrite the correct password, by passing a string that is long enough:
This gives the attacker a write primitive in the program’s memory, i.e., the ability to set the value of both user_input and password.
If the attacker sets the content of the input it uses as a password attempt (we can call that the attack payload) in such a way that user_input and password end up having the same content, the password check will succeed:
This allows the attacker to bypass the password check:
$ ./tampering xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
login success!
Example 3: Stack Smashing
Let us see a third example, this time of a classic attack called stack smashing. Stack smashing lets the attacker divert the normal control flow of a program. That means the attacker can have the executed code follow paths that were not intended by the programmer. This attack was first described in 1996 in an article that has become quite famous since then. To understand stack smashing, we will first refresh our mind on how the CPU handles function calls and returns at runtime.
Assume we have the following scenario: a C program with a function f calling another function g.
In memory, when f runs, its local variables and parameters are located in its stack frame.
When f calls g, the CPU issues a call instruction.
That instruction pushes on the stack the return address of g in f: it is the address, in the code segment of the program, of the instruction that should be executed next when we will return from the call to g within f:
After the call g starts to run, and there is some space allocated on the stack for its own local variables and parameters.
When g returns, the CPU executes a ret instruction.
ret will pop the return address from the stack and jump to it: in this way, the execution in f resumes right after the call to g:
We now consider this vulnerable program:
char *password = "secret";
void security_critical_function() { printf("launching nukes!!\n"); }
void preprocess_input(char *string) {
char local_buffer[16];
strcpy(local_buffer, string);
/* work on local buffer ... */
return;
}
int main(int argc, char **argv) {
if (argc != 2) { printf("usage: %s <password>\n", argv[0]); return -1; }
preprocess_input(argv[1]);
if(!strncmp(password, argv[1], strlen(password)))
security_critical_function();
else
printf("Unauthorized user!\n");
return 0;
}
It is the same type of password checking application we have seen previously.
It takes the password attempt from the command line, and before performing the check it passes it to a function named preprocess_input.
preprocess_input copies the attempt into a local_buffer before working on it.
In the code you can also see the function executed when the authentication succeeds, it is called security_critical_function.
Here the goal of the attacker is to run this function without going through the password check.
As you can see the strcpy in preprocess_input takes as source something coming from the command line, so similarly to the previous example we have the capacity to overflow local_buffer.
How can we exploit that to bypass the password check?
When preprocess_input runs, the stack looks as follows:
We have main’s stack frame, next we have the return address where we should jump in main when preprocess_input returns, and then we have preprocess_input’s stack frame.
local_buffer is somewhere in preprocess_input’s frame, so when we overflow it with strcpy, we have the ability to overflow upwards in the stack, towards the high addresses:
If we craft the content of what we overflow local_buffer with carefully, we can make it in such a way that we overwrite the return address with the address of our target, which is security_critical_function.
Doing so, when preprocess_input returns, the CPU will pop our overwritten return address on the stack and jump to it:
In effect, the CPU will start executing security_critical_function without going through the password check.
See the complete program’s sources here for instructions on how to reproduce this attack. On the computer on which this code was tested, the payload (injected with echo -e and xargs to produce bytes and not ASCII characters) looks like this:
$ echo -e "\x11\x11\x11\... (24 bytes of \x11 padding) ... \x5e\x17\x40\x00\x00\x00\x00\x00" \
| xargs --null -t -n1 ./stack-smashing
./stack-smashing ''$'\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021
\021\021\021\021\021\021''U'$'\026''@'
launching nukes!!
xargs: ./stack-smashing: terminated by signal 11
Example 4: Use-After-Free
So far we have seen how spatial memory errors (buffer overflows) can be exploited to various effects. We now examine an example of exploitation of a temporal memory error: a use-after-free.
This is our vulnerable program:
typedef struct {
double member1; double member2;
void (*member3)(int);
} my_struct;
void print_hello(int x) {
printf("Hello, parameter: %d\n", x);
}
void security_critical_function() {
printf("Launching nukes!\n");
/* ... */
}
int main(int argc, char **argv) {
/* allocate and init ms */
my_struct *ms = malloc(sizeof(my_struct));
ms->member1 = 42.0; ms->member2 = 42.0;
ms->member3 = &print_hello;
/* call the function pointer */
ms->member3(12);
free(ms);
char *buffer = malloc(12);
strcpy(buffer, argv[1]);
ms->member3(12);
/* check a password, runs sec_crit_fn */
}
It declares a data structure my_struct for which one of the members, member3, is a function pointer.
In main a data structure object ms is allocated with malloc and initialised, with the function pointer set to point to a benign function that prints a welcome message.
The object is then freed, then there is another call to malloc and the buffer in question is filled with data from the command line arguments, with strcpy.
And then finally we have our use-after-free: the object ms, previously freed, is mistakenly accessed: the function pointer is dereferenced.
We also have a security-critical function, and the goal of the attacker is to redirect the execution of the program to that function without going through a password check.
We now examine how we can exploit this program.
When the object ms is initialised, the memory layout looks as follows:
The 3 members of the object are laid out contiguously in memory, and the function pointer points to the first byte of code of the print_hello function in the code segment.
When free is called that memory is discarded:
Due to the way malloc is implemented, it will try to reuse freed memory for future allocations as much as possible.
So it is likely that the space that previously held the ms data structure will be reused for the next allocation, which is filled with data coming from the command line parameter:
With strcpy the attacker can write in that space, and overflow the 12 bytes of buffer to overwrite the space that previously held the function pointer with the address of security_critical_function:
Later when the use-after-free happens, this in effect invokes the security_critical_function.
Please see the full source code for instructions on how to reproduce that attack. On the computer on which this code was tested, the payload looks like this:
$ echo -e "\x11\x11\x11\ ... (16 bytes of \x11 padding) ... \x7c\x16\x40\x00\x00\x00\x00\x00" \
| xargs --null -t -n1 ./use-after-free
./use-after-free ''$'\021\021\021\021\021\021\021\021\021\021\021\021\021\021\021
\021''|'$'\026''@'
Hello, parameter: 12
Launching nukes!
# program continues to misbehave after that
Advanced Control Flow Hijacking
The last two examples of attacks we have seen are named control flow hijacking attacks: the attacker diverts the control flow of the program and has the CPU run code paths that are different from what the programmer originally intended.
Concretely, our examples showed how the attacker can rewrite return addresses and function pointers to return and jump to security-critical pieces of code.
Other control flow hijacking attacks can attempt to jump to C standard library functions, for example jumping to the exec function while having the string "/bin/sh" in the register holding the first function parameter according to the ABI can lead to a remote attacker getting access to a shell on the victim machine.
Another relatively advanced attack is named return oriented programming (ROP). With ROP, the attacker has full control over what is on the stack, for example through an overflow. The attacker places on the stack a series of code addresses that point to small snippets of machine code. These are called gadgets, and represent sequences of just a few instructions ending with ret. In this way, the CPU executes the first sequence of instruction, returns to the second, executes it, then returns to the third, and so on:
In modern programs there is a very high number of gadgets in the code segment. In fact, on medium to large size programs, the attacker can generally achieve Turing complete computations through ROP, which makes it a particularly concerning attack vector.