Last night I was trying to debug a fairly old network daemon we had running on a server. I had been lost for a couple of hours due to an unexpected crash every time I sent it a slightly longer-than-normal text string through the socket. The classic Segmentation Fault. And there I was, staring at a huge core dump file with my trusty GDB. It was then, seeing the registers scattered across the screen, that I remembered the chaotic beauty of buffer overflows. It's not just a silly programmer mistake; it's a window thrown wide open into the very guts of the processor.
What exactly is a buffer overflow? We talk about this a lot, especially when alarms go off on mailing lists like Bugtraq or Full Disclosure, but sometimes we lose perspective on how ridiculously mechanical these low-level flaws are. Basically, it happens when we stuff more data into a memory block (the buffer) than it can physically hold. Since C doesn't have automatic bounds checking, the program simply keeps writing into adjacent memory addresses, crushing everything in its path.
If you remember what I mentioned in the article about the x86 assembly call stack, you'll know that a function's local variables live together on the stack, right next to system-critical things like the return address (the EIP).
Imagine C code as innocent (and as dangerous) as this little practical example:
#include <stdio.h>
#include <string.h>
void funcion_vulnerable(char *entrada) {
char buffer[64];
/* Danger! strcpy does not check the length of 'entrada' */
strcpy(buffer, entrada);
printf("Procesado correctamente: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
funcion_vulnerable(argv[1]);
}
return 0;
}
If we pass this program a string of 40 or 50 characters from the command line, the strcpy function won't complain. Everything will run fast and smoothly. It will start copying our string into buffer[64]. But when it hits the limit and we overrun it, it will blindly keep writing over whatever is behind it. And what's right behind it, in the stack frame, is the saved base pointer (EBP) and, immediately after, the sacred function return address.
Writing our homemade exploit
If we calculate the exact distance between the start of our buffer and the memory position of that return address, we can put a carefully chosen hexadecimal value there. What value? The memory address where we have injected our own code (our shellcode).
To see it in action with our old friend the gcc compiler (and assuming you don't have any esoteric memory protection mechanisms enabled, the kind some experimental kernel patches are starting to bring):
# We compile the vulnerable code
gcc vulnerable.c -o vulnerable
# If we pass it 64 characters, everything is fine
./vulnerable $(perl -e 'print "A"x64')
# If we cross the line and hit the return address...
./vulnerable $(perl -e 'print "A"x80')
Segmentation fault (core dumped)
At this exact point, if we hook the executable to a debugger, we will see that the EIP (Instruction Pointer) register has attempted to jump to the address 0x41414141 (which is the hexadecimal value for four 'A's). We have just hijacked the program's execution flow. The processor is under our command.
Changing those last 'A's to the memory address of our shellcode, wrapped in a nice bed of NOPs (instructions that do absolutely nothing, 0x90 opcode, to have a good margin of error when jumping), is the logical next step for any hacker. It's fascinating to see how a small human oversight in variable handling can lead to someone gaining a remote superuser shell on our machine.
Future perspective
At this point in 2006, programming in C for network services exposed to the internet is almost an extreme sport. We rely entirely on the iron discipline of the developer to use safer versions of functions, like strncpy or snprintf, and manually check bounds.
I honestly believe that the computing industry cannot continue to sustain this rate of critical vulnerabilities. Sooner or later, operating systems and compilers themselves will have to shoulder this technical burden. You already hear about some interesting protections that randomize memory space or mark the stack as non-executable at the hardware level, but there's still a long way to go before that becomes a real standard. Until that happens in commercial distros, or until we massively adopt other types of languages that handle memory natively for us, the buffer overflow will remain the undisputed king of exploits. In the end, understanding it deeply makes you a much more aware, rigorous, and memory-respectful programmer.