Through the years, we have seen many kinds of breaches happen on digital systems. Attacks such as stack-based buffer overflows proved to be a first step in the direction of many more attacks to come. These attacks, prevalent in vulnerable code, allowed attackers access to a lot of tooling they should not have access to. Today, the question we pose is:
“What are the most common memory-based exploits and how can we identify these?”
Common exploits
As mentioned before, memory-manipulation is not a one-size-fits-all term. There are a lot of attacks that would fit under this term. In the past, we have seen the Morris Worm pop up. It was one of the earlier computer worms that exploited a memory vulnerability. In particular, a buffer overflow vulnerability that existed in the “fingerd” network service on Unix systems, from back in the day. Utilizing this vulnerability to make the execution of arbitrary code execution possible.
Another example that is similar in nature, but still widely different, is an integer overflow. Integer overflows are most easily explained when we think about the mileage counter on cars. We have always wondered what would happen if we hit the maximum and exceeded it. If we were to watch and find out, we’d see that either the counter just simply breaks or switches back to all 0’s, in most cases. This happens in computers, as well. When this happens, data can become corrupted, or we are perhaps even able to exploit this and run our own code.
These attacks are both less rampant than they were in the past. Buffer overflows have largely been fixed with protections like ASLR (Address-Space Layout Randomization) and DEP (Data Execution Prevention), both of which are usually built-into OSes, system-wide. Where developers had to opt-in and perhaps optimize their code, now the OS handles this by default. Of course, these solutions are not always able to alleviate the problem. And nowadays, where buffer overflows were a major exploit, the introduction of these protections has caused it to evolve into ROP (Return-Oriented Programming) attacks.
Another form of modern-day attacks, is DLL injection. Windows largely depends on these DLL’s, short for Dynamically Linked Libraries, which allow various functionality to be loaded into application and services. DLL injection takes adventage of these functionalities in programs that aren’t built as well. What it does, is inject a DLL, made to run our own code, and inject it into a running process. We can use debugging software to figure out which DLL’s a program loads and instead have it load our own, so that we can manipulate the program to run code it wasn’t meant to.
However, the majority of modern day exploits has grown from the exploits of the past. And the best way to form an understanding, is to see how these were originally created and then understand how these have evolved through the protection that were created. And that is what we’ll be focusing on.
Identification
The identification of vulnerable code is not a one-and-done process. After all, not all code is the same. Neither are all programming languages the same and neither do they have the same protections in place for compiled code. For example, the identification of a buffer overflow can be performed in a variety of ways. But of course, the most fun method is getting hands on with a program and seeing for ourselves.
Oftentimes, there’s a fault in vulnerable software. For example, in the way the code is written and what functions are used, since some functions are vulnerable by nature due to these being deprecated and used in legacy software. It also just is this way because the way the functions are put together. Usually, there is a modern, safer version available, which you should use, regardless. Functions such as strcpy, gets, or scanf in code are vulnerable, for example.
Let’s take a look at an example with a bit of C code using the strcpy function to see what happens.
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *input) {
char buffer[50];
// Vulnerable: no bounds checking for `input`
strcpy(buffer, input);
printf("You entered: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]);
printf("Program completed successfully!\n");
return 0;
}
We have a simple C program called “vulnapp”. This program looks for arguments passed when executing it. If there is less than 2 characters passed as an argument, we assume that the user doesn’t understand how to use it and return an instruction on its usage. However, if the user passes an argument, such as “Hello”, it will be passed into the buffer, and then printed to the console, similar to how the “echo” command would function. (Note that the buffer is set to hold a maximum of 50 bytes.)
This is an unsafe app, because the function strcpy() is used to pass data into the buffer. We can define the size as being a maximum of 50 bytes, but the issue is that this function does not do bounds checking. What this means is that the app does no checking of its own and simply assumes that the data we pass into it is below the buffer’s maximum size. So if this data is not filtered by some means, we can fill it however much data we please and cause a buffer overflow on the application.
An example of a remediation of this, is the function strncpy() – (Note the “n” in the function). Instead of only taking a “destination” and “source” parameter, strncpy() also takes the “n” parameter. This is the number of bytes until we cut off anything else written to the buffer, and limit what data can enter it. Which means it checks for the amount of data written to it, and by doing so, reduces the chance of a buffer overflow attack being performed.
Demonstration
Of course, the best way to understand how this works is by exploiting this application! We don’t have any tools to use at the moment, so we will have to manually explore this application. We will compile the binary with the following command, so that we can run it as an executable on a Linux machine:
gcc -fno-stack-protector -z execstack -o vulnerable_program vulnerable_program.c
We want “vulnerable_program” to be the name of the output, which I named “vulnapp”. While we want “vulnerable_program.c” to be the name you gave to the C file with the code we saw above.
A short explanation of the flags we use, which aren’t important to understand, yet, but are handy to know about:
- “-fno-stack-protector” disables stack protection mechanisms like StackGuard, which would not allow us to run this exploit (We will look into this, later)
- “-z execstack” marks the stack as executable (though modern systems often disable this by default).
Following this, we can proceed with the execution of the program.
shore@LAPTOP-AULF4TJG:~$ ./vulnapp HelloWorld
You entered: HelloWorld
Program completed successfully!
We can see that the program shows us what we have entered into the buffer and then exits properly.
Now, we have seen that the buffer is able to contain a maximum of 50 bytes in itself. What would happen if we passed more than 50 bytes into the program?
We will pass a bunch of “A”s into the program, roughly 60, and see what happens when we run the program with this data.
shore@LAPTOP-AULF4TJG:~$ ./vulnapp AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
You entered: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
We receive a segmentation fault, and the program crashes before it can run to completion! The data we have entered has overflown the size of the buffer and caused the program to crash, because it doesn’t know what to do, next.
Now, this is not a sure-fire way to find and detect the chance of a buffer overflow being possible. But if a program crashes or shows errors upon entering a string that is too long, we can assume there is something of a vulnerability at place.
This was, of course, a short demonstration of how the most basic of buffer overflows functions. We overflow the vulnerable buffer with data, which eventually overwrites the data is has stored. Now, all we have done is simply cause the program to crash through the means of a buffer overflow, but we can do a lot more with this vulnerability than meets the eye.
A few examples of things we can do with this are:
- Running our own code, usually shellcode of some sort.
- Jumping to a different place in the program, to bypass a license check, for example.
- Gain more privileges on a system.
- Cause a DoS if the process exploited is important.
- … And much, much more!
Finding overflows
Most of the time, we can assume that even if an unsafe function, such as strcpy() is used, that there is protections around this, implented by the developer of the software. Strange input that we do not expect or what to be input could be blocked, for example. And doing all of this testing by hand is a lot of work. And we certainly don’t want to spend too much time messing around with a program and editing random, long strings of text.
So what we will be doing is using software that will make the difficult less difficult. For the next post in the series, if you want to follow along, make sure to have the following ready on your Windows machine (Sorry Linux people, you’ll get a chance later, as well!):
- The x96dbg debugger suite, which includes x32dbg and x64dbg.
- The ERC plugin for the x96dbg suite (we’ll walk through the installation in the next post, don’t worry if it’s too difficult).
- Visual Studio Code, so we can compile our code. (VS Codium might work too, but I don’t use it, personally.)
- Python3.10(or later), so we can write code for our exploit.
Thank you for reading, I hope you found this explanation useful and the instructions helpful.
If you have any feedback, please don’t hesitate to leave a comment and I will reply as soon as I can!
One thought on “Identification of Memory-Based Attacks”