Mitigations
Previously Read:
Memory Management (NOT WRITTEN YET)
Key Terminology:
Stack Canary
Data Execution Prevention (DEP)
Return Oriented Programming (ROP)
Address Space Layout Randomization (ASLR)
Relocation Read-Only (RELRO)
Common Mitigations
Over the years engineers have invented and added protections to their programs to prevent exploitation, or at the very least to make it much more difficult. While most beginer level CTF challenge sets won't include most of these, intermediate and advanced CTFs likely do (and real world targets absolutely do). The most common of these mitigations is discussed below.
Stack Canary
A value that changes every time the program is run is placed on the stack as part of a function's stack frame, and is then checked if it was changed before the function returns. This value is placed directly between the return address and the base pointer. Recalling from **Stack Frames, Calling A Function, and Function Prologues**, this means that any overwrite coming from a local variable will have to overwrite the stack canary before it overwrites the return address. If unaccounted for, an overwrite will have changed the value of the stack canary, and then when it is checked right before the function returns, the program instead errors out.
The most common way to defeat a stack canary is to get an 'information leak' into the program, and ensure that your overwrite keeps the stack canary's value unchanged, while still updating the return address that is being targeted (or to just brute force it). Throughout all of this websites earlier practice examples, there is always a 'compiler options' snipet for learners to copy and paste so that they can ensure they are compiling a program with the same exact layout that we intended. The '-fno-stack-protector' flag specifically opts out of implementing stack canaries. If you are curious to see what this looks like, compile a previous program without that one option and observe the stack canary for yourself.
Data Execution Prevention
This enforces code sections of memory to be executable but ensuring that any other section is not. Specifically, when the computer's CPU sees that it is being asked to execute a 'not executable' page of memory, it throws an access violation and errors out. Among many things, this prevents the stack from being executable. This prevents an exploit from writing their own code into memory, and then manipulating the program to run it. Without any other options, this would mean that even f an exploit had a well crafted shellcode payload, it would not have permission to be executed and would not work.
The work around for DEP is Return Oriented Programing (ROP). Instead of using your own code, ROP works by calling code snipets that already exist within the program. Since it's pretty unreasonable to expect the precise full code that you want in one centralized spot, what this often means is calling different small snipets of code all throughout the program. These small snippets are called 'ROP gadgets', and to defeat ROP you have to string together multiple gadgets to eventually call all of the needed instructions to achieve the exploit you want. For example you might call a POP EAX from address 0x56555540, and then right after a INT 0x80 from 0x56555420. In actuality ROP chains are often much longer and more complex than this. Working around ROP using Python's MONA tool is an excelent topic to look into if this interests you.
Address Space Layout Randomization
ASLR simply randomizes where a program is going to be laid out in memory every time it's run. This prevents someone from looking in a debugger and finding the precise address of a program that they are targeting, and then hard coding that component into their exploit. In Stack Based Buffer Overflow, we had done just this, we found the address we wanted to redirect our function return to, and used it as part of our exploit to overwrite the return address. This would not have been possible if ASLR had been turned on.
ASLR is defeated using offsets. While yes, the program is laid out in a randomized location in memory every time, it's components still have to be laid out in the same order with relation to each other. You can park your car in a different spot every day for a year just fine, but you cant just rip out the engine and put it in the trunk, it would cease to function. Following this logic we might not know what address some target function is at specifically, but we can look in a debugger and know for sure that it will always be at 0x64 bytes above a vulnerable functions base pointer. This is useful because even if we cant hard code the address we'd like to go to, we do know how long a payload needs to be, and we can redirect program execution (or whatever the exploit requires) using offsets as opposed to hard coded addresses. To visualize this, turn on ASLR on your current system if it isn't on already (you'll have to google this for your own system) run the following code:
#include <stdio.h>
void offset(void)
{
int hello_there = 0x41414141
}
int main()
{
buffer[8] = { 0 }
printf("Buffer is located at address: %x\n", buffer);
return 0;
}
If you pick this apart in GDB, you will notice that the location of this program always changes. What you will also notice however though, it that offset() and hello_there are always the same amount of bytes away from main(), no matter where in memory everything is laid out.
Relocation Read-Only
As the name implies, this mitigation forces certain sections of a binary to be read only, this is a Unix system mitigation. There are 2 types of RELRO, ‘partial’ and ‘full’, with partial being the default setting for GCC, but full being the setting that really affects us as vulnerability researchers. In order to understand RELRO you must understand what the Program Linkage Table (PLT) and Global Offset Table (GOT) are. A fair warning that these next few paragraphs are rather challenging, they also use multiple references to ‘the linker’. The intricacies of how the linker works and what it’s role is in running a binary are beyond the scope of this tutorial, but if you are curious and determined to understand, feel free to look up “linking and loading” to beter understand how binaries are run on different systems (in this case Unix systems). These next few paragraphs describe their contents in broadstroakes and aren’t meant to walk you thoroughly through the discussed mechanism, but instead just to give you a feel for the underlying technology, so that we can than return to Full RELRO and understand its significance.
If you’ve worked your way through some of the previous tutorials on this site, you’ll by now have spent sometime sitting in GDB inspecting various simple programs. What you may or may not have noticed by now is that every time a library function is called the disassembly specifies “@plt”, the Program Linkage Table. This is because when a program uses external library functions, they aren’t actually compiled into the binary. So for example, if you have a program that uses printf(), you compile that program into a binary, and email it to a friend, printf() and its library (along with any library the program requires) aren’t actually sent with it. Instead, a placeholder in the Program Linkage Table is set that says “if this function actually gets used, then we’ll have the linker on whatever computer we’re running on deal with it then”. This might seem overly complicated, but it’s useful. If binaries had references to external libraries hard coded in, it would mean every time we update a library, we would have to recompile that program (and if all of our binaries worked like that, we’d be recompiling everything all the time and it would just suck). it also allows for programs to be smaller, as long as the computer it’s running on has the libraries it requires to run. So, the first time that any external library function (like printf()) is called, the linker steps in, and begins to connect that bookmarked function call to the linker, which in a bit will take a few more steps to connect that to where that function actually is in the external library. This only has to happen on the first time the function is called, after it is linked, there’s no need to redo it.
However there is another level of ‘indirection’, meaning the linker doesn’t just connect the PLT to the function in the library directly, it connects it to… yet another table! The linker connects a PLT entry to the Global Offset Table (GOT). Imagining the linker as a ‘black box’ mechanism and just assuming it works, this means that on one end the program is giving the linker a PLT stub for an external library function that is being called, and on the other end it has a Global Offset Table for precisely what addresses these functions exist at within the library being used on this computer. As the name implies, the linker’s job here is to link those entries together. Note that the linke ronly has to do this once, after the first time a function’s PLT entry is connected to the GOT, from then on out throughout the program’s execution, that function call is already resolved to just go to that address.
So that was probably more intense and confusing than you had expected given the simpler explinations before it, you’re probably asking yourself, “why use the PLT and the GOT”? We’ve justified why the PLT exists, because if a function was used in writing the code, but is never actually called, than we never have to bother loading it up and linking it’s PLT stub, and therefore save time and resources. We need the PLT and the GOT because the PLT is what the linker requires, it is the program’s way of telling the linker “ah yes, we’re actually using this function now, get to work”, but it still doesn’t tell us the actual address of where the external function is in the library. While the GOT is exactly that, a list of the addresses of external library functions that are used by the program. Infact, once an external function is used for the first time and its PLT stub is linked to the GOT, subsequent function calls for that function go straight to the GOT and skip that redundancy as it is only needed in that initial call. While it will probably take you as a new learner a momentous effort to read and understand all this, this whole process of only linking when needed is called “lazy binding”.
Once more, you’re encouraged to go look up how the PLT, GOT, and linker all work together to achieve lazy binding, there are certainly better and more in depth explanations on line, but for us right now, we are going to turn our attention back to RELRO. If nothing else, from the last few paragraph you must have at least understood that the GOT is a table of addresses to functions that the program and linker use to locate and call those functions. Well, wouldn’t it be convenient if we could overwrite that and trick the program into calling a different function instead? That’s where full RELRO comes in. Full RELRO makes the GOT read only. Partial RELRO, the default gcc setting, does not do this and would not prevent this attack, although it does ensure the GOT comes before the .bss section of a binary to avoid simple overflow attacks.
If you read the above and felt it did more harm than good and feel uterly confused. We suggest you go do some independent research on the PLT and GOT. Then, consider compiling this code and inspecting and running it in GDB. You’ll notice printf() in the disassembly of main() has @plt followed by it. Step through it handling it’s PLT stub and inspect where that is set to point to, inspect the GOT.
#include <stdio.h>
int main()
{
printf(“Hello there.\n”);
return 0;
}
Summary: Mitigations
Several mitigations are commonplace in challenge problems and real world examples. Stack canaries place a random value on the stack and check before a function returns to ensure a buffer overflow did not happen. DEP prevents certain pages of memory from being executable and can be beaten using ROP, which calls ROP gadgets from within the program’s own executable code. ASLR randomizes where programs are laid out in memory making harcoded exploits more difficult. FULL RELRO forces a program’s GOT to be read only, preventing GOT overwrites. As you get better at vulnerability research you’ll be required to understand and gain a familiarity with these mitigations and how to work around them. However at a beginners level, you are probably just fine knowing of them and the basic concepts, keeping an eye out for if you encounter them in the future.
Practice Exercise:
Compile this C code yourself using GCC and inspect the stack_canary_example’ binray yourself in GDB. This is a super simbple program, accept this time we’ve removed the ‘-fno-stack-protector’ flag from the compiler options. Inspect and run this code in GDB, fin and inspect the stack canary in this example. If you want to go for bonus points, compile and run this code in GDB, break before the call to scanf() and inspect main()’s stack canary. Then, overwrite the return address to instead transfer execution to win() just like we did in the Stack Based Buffer Overflow tutorial, but ensure the stack canary’s value is unchanged (don’t forget to account for endianess).
Compiler options:
gcc buffer_overflow_example.c -o buffer_overflow_example -m32 -O0
Code:
#include <stdio.h>
void win(void)
{
printf("You win!\n");
}
int main()
{
char buffer[8] = { 0 };
/*Take user input*/
printf(“Give me your small amoutn of input.”);
scanf(“%s”, buffer);
return 0;
}