There are three classes of protection we will be discussing in this section.
- No eXecute Bit
- Address Space Layout Randomisation
- Stack Canaries
We will discuss this using visualisations of how a classic exploitation technique attempt is stopped by the properties of each of the protections.
First let's visualise how the stack looks like before a buffer is read into:
For clarification, the value of the saved base pointer is 0xbfff0030 and the value of the return address is 0x080484f0 (an address within the binary). The numbers are reversed in the visualisation because x86 is a little endian architecture.
On a valid run of the program, the buffer is filled within its bounds. Here we have 15 As and a null byte written to the 16 length buffer.
However, since the read allows for the program to read more than 16 bytes into the buffer, we can overflow it and overwrite the saved return pointer.
When the function returns, the program will crash since the instruction pointer is set to 0x41414141, an invalid address.
To complete the technique, the attacker will fill the first part of the buffer with the shellcode, append the appropriate padding and overwrite the saved return pointer with the address of the buffer.
Now, when the function returns, the program will begin executing the shellcode contained in the buffer since the saved return pointer was overwritten by the buffer address (0xbfff0000). From this point onwards, the attacker has achieved arbitrary code execution.
Now that we understand how the classic exploitation technique works, let us start introducing protections and observing how they prevent the technique from working.
Also known as Data Execution Prevention (DEP), this protection marks writable regions of memory as non-executable. This prevents the processor from executing in these marked regions of memory.
If we look at the memory map of a program compiled with NX protection, the stack and heap are typically marked non-executable.
In the following diagrams, we will be introducing a new indicator colour for the memory regions to denote 'writable and non-executable' mapped regions. Firstly, the stack before the read occurs looks like this:
When we perform the same attack, the buffer is overrun and the saved pointers are overwritten once again.
After the function returns, the program will set the instruction pointer to 0xbfff0000 and attempt to execute the instructions at that address. However, since the region of memory mapped at that address has no execution permissions, the program will crash.
Thus, the attacker's exploit is thwarted.
This protection randomises the addresses of the memory regions where the shared libraries, stack, and heap are mapped at. The reason for this is to frustrate an attacker since they cannot predict with certainty where their payload is located at and the exploit will not work reliably.
On the first run of the program, the stack looks like this just before the read:
If we terminate the program and run it again, the stack might look like this before the read:
Notice how the stack addresses do not stay constant and now have their base values randomised. Now, the attacker attempts to re-use their payload from the classic technique.
Notice that the saved return pointer is overwritten with a pointer into the stack at an unknown location where the data is unknown and non-user controlled. When the function returns, the program will begin executing unknown instructions at that address (0xbfff0000) and will most likely crash.
Thus, it is impossible for an attacker to be able to reliably trigger the exploit using the standard payload.
This protection places a randomised guard value after a stack frame's local variables and before the saved return address. When a function returns, this guard value is checked and if it differs from the value provided by a secure source, then the program is terminated.
In the following stack diagram, an additional stack canary is added right after the buffer. The valid value of this stack canary is 0x01efcdab.
Now, the attacker attempts their exploit with the standard payload again. The stack diagram looks like this after the read:
Notice that the stack canary has been overwritten and corrupted by the padding of 'A's (0x41). The value of the canary is now 0x41414141. Before the function returns, the canary is xored against the value of the 'master' canary. If the result is 0, implying equality, then the function is allowed to return. Otherwise, the program terminates itself. In this case, the program fails the check, prints a warning message, and exits.
Thus, the attacker is not even able to redirect control flow and the exploit fails.