Part 1 of 'VERT Vuln School: Stack Buffer Overflows 101' introduced an example program containing a common programming error known as a buffer overflow. Specifically as outlined in part 1, this program fails to provide bounds checking when processing user-input and enables an overflow of user-controlled data onto the stack. Now that we have explored what a buffer overflow is and how to avoid creating them, it is important to really elaborate on how this mistake can be exploited to completely compromise the integrity of the system it is running on. The first step in this process is to look at what the stack does and how it is contents are arranged in memory.
In this context, the 'stack' refers to a portion of memory used by the operating system to store contextual details regarding the execution of a program. If you have ever reviewed the assembly generated by a C compiler, you might have noticed that it prepends a few extra lines of assembly which don't correspond to lines in the source. This is because the beginning and end of your functions require housekeeping in the form of (for x86) push and pop instructions. These instructions are what preserve aspects of the execution state prior to entering a function call and restores it when the function returns. The following illustration provides a high-level view of what the stack might look like for the vulnerable synscan program running on an x86 based Linux operating system.
On an x86 architecture like the one we are examining, the stack grows toward lower addresses with each push. Memory is reserved for local variables such as ‘char buf’ from the sample code. This means that buf is going to be at a lower memory address than the saved return pointer. Since this saved return address will be restored as the CPU's instruction pointer (EIP) when main() returns, it becomes possible to actually load an arbitrary address into the EIP register. The location of this buffer combined with the use of sprintf() to copy a user-supplied buffer is what makes it possible to not only crash the process but also to completely lead execution into user-supplied code. Since the target is suid-root, this code can be exploited to elevate privilege and spawn a root shell.
Various exploitation techniques have been devised over the years which can take advantage of this type of bug to hijack control of a program. The earliest such techniques involved overflowing the buffer with executable instructions and replacing the instruction pointer with an address somewhere in these instructions. Since the stack arrangement can vary slightly between environments, a tool is employed which is known as the 'nop sled'. NOP, short of no operation, is an instruction which is implemented on most architectures to spend a clock cycle doing nothing. The advantage of this is that if an attacker's payload follows a series of NOP instructions, the attacker just needs to get EIP pointing to one of the NOP bytes and then the CPU will slide down the NOP sled until it reaches the payload instructions. The third and final segment of 'Stack Buffer Overflows 101' will use the synscan example to demonstrate an alternate method attackers have been using to exploit this class of vulnerability.