Under the Hood

How Buffer Overflow Corrupts Memory?

When we talk about "overflowing a buffer," we're really talking about writing data into adjacent memory (memory that the programmer didn’t intend for us to touch.)

In C, local variables (like buffer and flag) are stored on the stack, and they’re placed one after another in memory. If your code doesn't check the size of input (like when using gets()), extra data spills beyond buffer and can overwrite whatever comes next in this case, the flag variable.

This is exactly why understanding how the stack is laid out during program execution is so important. Once you see the memory structure, the magic of buffer overflows starts making sense.

Here's a simplified view:

Higher Memory Addresses
+----------------------+
|  Return Address      | ← where the function should return after finishing
|----------------------|
|  Saved Frame Pointer |
|----------------------|
|  Local Variables     | ← this is where your `buffer` and `flag` live
+----------------------+
Lower Memory Addresses

Understanding the Stack Frame Layout

When a function is called in C (like main()), the system creates a stack frame a dedicated section of memory used to manage that function’s execution. This frame includes:

  • Return Address : This is the address of the instruction the CPU should return to after the function finishes.

  • Saved Frame Pointer : This helps the program restore the previous function’s stack state after finishing.It’s part of how the stack keeps track of "where it was" before the current function call.

  • Local Variables : These are the variables you define inside your function like char buffer[8]; and int flag = 0;

How Program Uses Memory :

When main() starts running, it creates a small space on the stack for local variables like buffer and flag. Here’s a visual:

Higher Memory Addresses
+----------------------+ ← Return Address
|  Return Address      |  
+----------------------+
|  Saved Frame Pointer |
+----------------------+
|     flag (4 bytes)   |  ← This should be 0 initially
+----------------------+
|   buffer[8] (8 bytes)|  ← User input goes here
+----------------------+
Lower Memory Addresses

🐞 Dissecting the Overflow with GDB

Now that we understand how the stack is laid out and how a buffer overflow can spill into adjacent memory, let’s observe it live using a debugger. We’ll use GDB (GNU Debugger) to watch how the memory changes before and after the overflow.

What Does disas main Mean in GDB?

When you run your program inside GDB (the GNU Debugger), you can use the command:

disas main

This stands for “disassemble main” and it tells GDB to show you the low-level machine instructions that your computer actually runs when it executes your main() function.

Think of it like this:

You're taking off the high-level "C programming glasses" and looking at what your program really becomes underneath raw CPU instructions.

Understanding the machine instructions

1. Stack Frame Setup (Standard Boilerplate)

0x401146 <+0>:   push   rbp
0x401147 <+1>:   mov    rbp, rsp
0x40114a <+4>:   sub    rsp, 0x10

What’s happening here?

This is just standard function setup. It creates a new stack frame by saving the previous base pointer and allocating 16 bytes of space for local variables.

That 16 bytes? That’s where our buffer[8] and flag live. 👀

It’s like opening a new notebook page to do a small task. You mark the old page to come back to later (push rbp), then leave space to write notes for this task (sub rsp, 0x10).

  1. Flag Initialization

0x40114e <+8>:   mov    DWORD PTR [rbp-0x4], 0x0

This line sets the variable flag = 0.

And now we can see exactly where in memory that flag is stored: [rbp-0x4] — just 4 bytes below the base pointer.

Lets set breakpoint at main and run the program and check the address of buffer and flag

The base address of the buffer is 0x7fffffffdc94, and the address of the flag variable is 0x7fffffffdc9c. The difference between them is exactly 8 bytes, which matches the size of our buffer. This confirms that flag is stored immediately after the buffer in memory meaning any input longer than 8 bytes will start to overwrite the flag variable.

This is the crux of how the overflow works. Since the gets() function does not perform bounds checking, it will continue writing whatever input it receives into memory past the end of the buffer and into any adjacent variables. In this case, that adjacent variable is our flag, which was initialized to 0. When we provide 8 characters to fill the buffer and 4 more bytes representing the integer value 1, those extra 4 bytes spill into the memory location reserved for flag.

This seemingly innocent mistake changes the logic of the program. The check if(flag == 1) is now true, not because we assigned 1 to flag, but because we overwrote it via a buffer overflow. The program behaves differently simply due to the layout of memory and lack of input validation.

Understanding this basic overflow lays the groundwork for more complex exploitation techniques, such as overwriting return addresses, injecting shellcode, or manipulating function pointers. But even in this simple example, we see how dangerous and powerful buffer overflows can be when proper safeguards are not in place.

Last updated