In VERT Vuln School: Stack Overflow 101 we reviewed a contrived example of a simple stack-based buffer overflow vulnerability in a binary wrapper for the nMap scanning tool. With this example, I showed how crafted command line parameters could be trigger an overflow of user-controlled data onto the stack. The synscan binary performed no checking on the length of a user-supplied buffer before copying it into a fixed-length buffer on the stack. Analysis in the debugger revealed that the program attempted to access memory at the address we specified (0xdeadbeef).
How did this happen? Before answering that question it is necessary to understand a little about how the call stack works.
Each time a function is called, the stack grows downward (toward lower addresses) adding pertinent context information from the caller, which must be restored when the function returns. Additionally, with each function there is some function prologue, which reserves space on the stack for automatic variables, such as ‘char buf’ from the sample code.
This means that buf is a pointer to the stack with a lower address than the saved return address, which is to be restored as EIP when main() returns. The location of this buffer combined with the use of sprintf() rather than its bounded cousin snprintf() is what makes it possible to not only crash the process but also to completely subvert it to run new instructions. Since the target is suid-root, this vulnerability can be exploited to elevate privilege and spawn a root shell.
In order to execute arbitrary code through this bug, the return address can be replaced with the address of suitable machine instructions residing in an executable portion of memory. The set of instructions used to spawn a shell is what is known as shellcode. In order to demonstrate exploitation of this example vulnerability, I feel that it makes the most sense to reuse the shellcode, which is derived in AlephOne’s article in Phrack #49 (Google ‘Smashing the Stack For Fun and Profit’ for a complete explanation of how this was developed).
Now that the background is covered, the only thing left is some basic exploit code. The role of the exploit code is to put the shellcode on the stack, locate the address where the instructions start, build a buffer to repeat the shellcode address, and then finally to execute the vulnerable program with the crafted buffer.
The call to execle() begins execution of synscan with the payload buffer set as an environment variable. The address of the shellcode is found by subtracting its length and the length of the invoked program’s name from the known starting address for BASH variables (0xbffffffa) on line 14.
The resulting address is used to build a buffer for the command line argument on lines 17 & 18. Since the return address should be 4-byte aligned, it is sufficient to simply repeat the calculated return address and see that the saved EIP gets perfectly replaced by the target. The only thing left to do is compile and test the exploit as shown below.
If you are a developer concerned about this type of attack, I encourage you to perform some static analysis of your code. Unbounded string manipulation has no place in secure software so any instances of strcpy, strcat, or sprintf (as well as a few others) should stand out like a sore thumb.