Introduction
Greetings amazing readers, welcome to the second part of our Buffer Overflow Boot Camp. We started with understanding the memory layout and the structure of the call stack in a program, which laid our foundation for continuing our journey of the Buffer Overflow realm. In this article, we linger upon analyzing Stack-based Buffer Overflow in some simple C programs, to watch it happen in action. If you haven’t already read the previous article of this boot camp series, read it before continuing. Well if you are all set to go, get your IDE and Compiler ready to get your hands dirty and overflow that Stack!
Index
- Setting Environment
- Stack Overflow in Action - Program 1
- More Action - Program 2
- End Notes
Setting Environment
Since we are starting with our practical part, you require an IDE to write your C program and a Compiler to compile the code. I am using Microsoft VS Code as my IDE and the standard GCC compiler to compile my code on a Windows computer. Additionally, no changes are made to the environment. If you are finished setting up your environment, let us continue.
Stack Overflow in Action
Let’s start with a very simple program. We will initialize 2 character arrays and then we will try to overwrite the second array by overflowing the stack buffer. We will understand the source code step-by-step.
- Declare 2-character arrays. A character array is simply a string terminated by a null character.
#include<stdio.h>
void main()
{
char array2[] = "overflow_me";
char array1[8];
printf("Address of Array 1 : %d", array1);
printf("\nAddress of Array 2 : %d", array2);
}
You must be wondering why I initialized the second array first. That is because the stack grows from a higher memory address to a lower memory address. I explained this in the previous article. Let’s confirm this by compiling and executing the program at this stage.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc stackoverflow.c -o stackoverflow } ; if ($?) { .\stackoverflow }
Address of Array 1 : 6422284
Address of Array 2 : 6422292
PS F:\C Scratch\user_defined_data>
Since we declared the second array first, it occupies a higher address in the memory, while the first array occupies a lower memory address.
- Let’s print the value at these addresses now.
#include<stdio.h>
void main()
{
char array2[] = "overflow_me";
char array1[8];
printf("Address of Array 1 : %d", array1);
printf("\nAddress of Array 2 : %d", array2);
printf("\nValue at Array 1 : %s", array1);
printf("\nValue at Array 2 : %s", array2);
}
Since we have not initialized any value to array 1 at this stage, we will get some garbage value as output. Let’s confirm this by compiling the code.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc stackoverflow.c -o stackoverflow } ; if ($?) { .\stackoverflow }
Address of Array 1 : 6422284
Address of Array 2 : 6422292
Value at Array 1 : δ↓@
Value at Array 2 : overflow_me
PS F:\C Scratch\user_defined_data>
- Now we will use a vulnerable function to read input for array 1. I will use the gets() function, which is vulnerable to buffer overflow since it does not perform any array bound checking while reading user input.
#include<stdio.h>
void main()
{
char array2[] = "overflow_me";
char array1[8];
printf("Address of Array 1 : %d", array1);
printf("\nAddress of Array 2 : %d", array2);
printf("\nEnter value to store in Array 1 : ");
gets(array1);
printf("\nValue at Array 1 : %s", array1);
printf("\nValue at Array 2 : %s", array2);
}
We prompt the user to enter some value to store at array 1. Now the maximum value in bytes that can be stored in the buffer is n-1 bytes since a character array(string) terminates itself by adding a null character at the end of input. Here, we have allocated 8 bytes to array1, so we can store data up to 7 bytes. Let’s input a legitimate value.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc stackoverflow.c -o stackoverflow } ; if ($?) { .\stackoverflow }
Address of Array 1 : 6422284
Address of Array 2 : 6422292
Enter value to store in Array 1 : 1234567
Value at Array 1 : 1234567
Value at Array 2 : overflow_me
PS F:\C Scratch\user_defined_data>
We inputted 7 bytes of data and everything worked as expected without any errors. Now we will increment one more character that will possibly overflow the buffer. Let’s confirm this with a longer input.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc stackoverflow.c -o stackoverflow } ; if ($?) { .\stackoverflow }
Address of Array 1 : 6422284
Address of Array 2 : 6422292
Enter value to store in Array 1 : 12345678
Value at Array 1 : 12345678
Value at Array 2 :
PS F:\C Scratch\user_defined_data>
Voila! Value at the second array is gone! We actually overflowed the stack with a longer input. When we inputted 8 characters in array1, the null character was moved to the buffer of array2, and since it terminated the string in the beginning, we got an empty buffer in the output.
Amazing right? Sure it is. But what will happen if we input more than 8 bytes? Let’s see.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc stackoverflow.c -o stackoverflow } ; if ($?) { .\stackoverflow }
Address of Array 1 : 6422284
Address of Array 2 : 6422292
Enter value to store in Array 1 : 123456789
Value at Array 1 : 123456789
Value at Array 2 : 9
PS F:\C Scratch\user_defined_data>
The value at array2 became null at the 8th byte. And after that, the 9th byte got placed in the buffer. We have successfully pulled off our first Stack Based Buffer Overflow!! But guess what? We are not done yet. Let’s get our hands even dirtier and get a taste of a more real stack overflow attack.
More Action!
We will get close to a more realistic scenario and chain a buffer overflow vector to bad logic. We are going to abuse an authentication mechanism with stack overflow. Let’s begin. Again, I’ll break down and explain the code step-by-step.
- Declare an integer to verify authentication status. Declare two character arrays, the first array will store a hardcoded password, the second will read the user password to authenticate.
#include<stdio.h>
#include<string.h>
void main()
{
int status=0;
char systempass[] = "letmein";
char userpass[10];
}
The status variable verifies the authentication status. For a non-successful authentication attempt, it will have an integer value of 0. While any non-zero integer value leads to successful authentication. (We will try to abuse this business logic with stack overflow).
- Get the memory address of systempass, userpass, and status variable.
printf("Address of user pass : %d", userpass);
printf("\nAddress of system pass : %d", systempass);
printf("\nAddress of status variable : %d", &status);
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc authentication_abuse.c -o authentication_abuse } ; if ($?) { .\authentication_abuse }
Address of user pass : 6422282
Address of system pass : 6422292
Address of status variable : 6422300
PS F:\C Scratch\user_defined_data>
The systempass is 10 bytes ahead of userpass in the stack. Now, if we use a vulnerable function while reading systempass from the user, we should be able to overwrite the hardcoded password by inputting a longer value than 10 bytes.
Similarly, the status variable is 18 bytes ahead of the user pass in the stack. If we input an even longer value, we should be able to overwrite this value as well.
- Use the scanf() function to read userpass
printf("Enter User Password : ");
scanf("%s", userpass);
Here we have used the scanf() function to read input. This is to demonstrate that the scanf() is also vulnerable to buffer overflow vulnerability.
- Writing a logic to authenticate passwords with strcmp() function.
if(strcmp(userpass, systempass) == 0)
status++;
else
printf("\nWrong Password!!");
if(status)
printf("\nLogin Successful!!");
We are using the string compare function to compare the userpass with systempass. The strcmp() function returns 0 when both strings are found the same, else it returns a non-zero value. Now, if both strings are the same, we increment the value of the status variable, else we output that the user has inputted the wrong password.
The second if statement allows successful login when the status variable is not zero. Now recall that the status variable is 18 bytes ahead of userpass in the stack. If we input a value longer than 18 bytes in userpass, we should be able to overwrite the status variable and possibly get a login. Let’s try this out. Please note that we need to include the string.h header file to use the strcmp() function.
But first, let’s confirm that our authentication logic is working with legitimate input values. For sake of understanding, we will also print the value of the status variable.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc authentication_abuse.c -o authentication_abuse } ; if ($?) { .\authentication_abuse }
Enter User Password : letmein
Value of status variable 1
Login Successful!!
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc authentication_abuse.c -o authentication_abuse } ; if ($?) { .\authentication_abuse }
Wrong Password!!
Value of status variable 0
Everything seems to be working as expected. All that’s left now is to overflow that stack again. Let’s pull the trigger and bypass this authentication.
PS F:\C Scratch\user_defined_data> cd "f:\C Scratch\user_defined_data\" ; if ($?) { gcc authentication_abuse.c -o authentication_abuse } ; if ($?) { .\authentication_abuse }
Enter User Password : 12345678901234567890
Wrong Password!!
Value of status variable 12345
Login Successful!!
PS F:\C Scratch\user_defined_data>
Voila! We logged in with the wrong password!! We overflowed the stack and overwrite the status variable. And since the program logic allows us to log in for the any-zero value of the status variable, we abused the functionality to log in.
End Notes
You must be feeling like a god already for abusing that logic and bypassing the authentication mechanism. Attack vectors like this do exist in production applications due to the poor logic of how the user authenticates to the application. To avoid possible buffer overflow and memory corruption in your program, never use functions like scanf() and gets() since they do not perform any bound checking while reading user input. Also, put emphasis on better development practices to avoid bad logic that could be abused to chain vulnerabilities in systems and programs.
I put down a lot of research in my buffer overflow boot camp and there is more to come. Honestly, it is one of my best article series, since it involved a lot of research and challenges for me as well. While I enjoy writing content and transcending knowledge through my writings, I also expect my readers to react and provide feedback. If you love what I do, do share my content and follow me on my socials. You can reach out to me on LinkedIn or Twitter. If this boot camp gets the acknowledgment and appreciation that is at least equivalent to the effort I put into preparing it, we will continue this further with offensive stack overflow exploitation and popping reverse shells. Till then, enjoy reading my other articles, and have a good day.