Greetings and welcome to a fresh series of articles. When I started on my expedition to explore the Information Security domain, there was this buzz termed “Buffer Overflow” that got me scared every time I tried to learn it. The result? I skipped the whole idea of learning it in the first place. After I took a very long break from my never-ending hacking journey, I spent time learning fundamental programming. In the concept of memory architecture and memory management I learned there, I figured out the problem that had been scaring me out for months, i.e “What is a buffer?” and “How does it overflow?”. To save time and effort for people starting in cyber, I have documented my learnings and prepared this “Buffer Overflow Bootcamp”. The target readers for the Boot Camp are Ethical Hackers and Security Professionals who are engaged in the Information Security Domain, but it is nonetheless helpful to Developers and Programmers who also want to understand the memory structure of their programs and how it all traces back to a buffer overflow.
While I do not expect you to be a Programming God, a basic understanding of a C program is essential to continue with the practical part of this Bootcamp. If you do not know at least how to print a hello world in C and read and print data, then I highly recommend you discontinue reading and learn basic programming. To understand Memory Architecture, I will explain every concept to a high intermediate level where everything will make sense. Like every other article I write, this one is also based on my principle - “Learn enough to be dangerous”. Having said that, we will not dive deep into the CPU architecture and how registers work in a Stack activation record, but we will give a high-level overview of what is happening at the low level, and of course why it is happening.
Furthermore, please understand that this Bootcamp orients upon understanding Buffer Overflow Vulnerabilities and watching them in action with simple programs. After reading this article series, please do not expect to become a Reverse Engineer or Binary Wizard.
- What is a Buffer?
- Execution Lifecycle of a C Program
- Memory layout of a C program
- Understanding the Stack
- The Call Stack - Overview
- Demystifying Buffer Overflow
Understanding the Memory Buffer
Irrespective of place and context, every one of us has heard the term “Buffer” before. In plain English, when we say buffer, we refer to something that is stored in stock and is available in excess, like food grains or eatables. Similarly, when we say “Buffer” in context to Memory (RAM), or simply computers, we are referring to a “stock of memory”, that stores data in a temporary and contiguous manner. The key factor is that this memory is contiguous, that is data items are stored adjacent to each other, like an array. You can visualize this with an egg tray. Every egg in a tray is stored adjacent to each other, in a contiguous manner. Mathematically speaking, consider the following instance:
- There exists a data item that consumes 2 bytes in the memory
- We need a buffer to store 10 such data items
- Assume that the base address of the first data item is 1000
- Contiguously, the second data item will be on the address 1002
- Similarly, the third data item will be on the address 1004, and so on.
Remember we visualized this structure with an egg tray? Now imagine if you want to pick an egg from this tray how would you pick one? You can pick any egg from the tray. The same logic is applicable in our buffer memory. If I want to access nth data item from my buffer, I do not have to traverse through the entire buffer to access this element, all I need is the base address of my buffer that points to its first element and I can calculate the required address since memory is contiguous!
Let’s summarize and put everything in a definition:
“A buffer is a contiguous and sequential block of memory allocated to store some data items combining for a fixed size in bytes”. Please note that the size of the buffer must be fixed, evaluating a constant size in bytes.
We have answered our first question, which is “What is a buffer”, but it’s a long way ahead before we can answer “How does it Overflow”. Let’s proceed!
Execution Lifecycle of a C Program
Our discussion on Buffer provided us with a very vague understanding of Memory, but we have to understand memory layout at a low level to understand buffer overflows. And even before we take an overview of memory layout, we need to understand the execution lifecycle.
When we execute a program, the first thing that happens is pre-processing. At this stage, the pre-processor removes comments from our program and expands all macros. Then it includes the header files that are requested by the user for compilation.
After pre-processing, the source code is compiled into assembly instructions by the compiler.
Once the source code is compiled, the assembly instructions are converted into object code without resolving the function calls.
The execution flow then moves to the “linker”. Linker resolves function calls at this stage and puts multiple object files into a single executable file.
At last, the “loader” loads this executable file into our main memory (RAM) to run.
Kew! You are already feeling tired right? Do not lose hope and continue reading, everything will make sense when we will put it all together.
Memory Layout of a C Program
During execution, the lifecycle marks its end with the loader loading the executable into the main memory. Let’s understand how the memory is organized when we load the program into it.
The organization of memory is done into the following segments:
- Text Segment - The space occupied by the source code itself
- Data Segment - Space occupied by static and global variables that are initialized.
- BSS Segment - Space occupied by static and global variables that are uninitialized yet.
- *The Stack - A linear data structure in the memory where function calls are resolved with their local variables.
- Heap - Memory allocated during runtime. (dynamically)
*While our point of focus is on the stack, please understand the following at a high level:
- Static variables are those variables that retain their values out of their scope.
- Local Variables are those variables that can only be accessed and modified in the function they are defined in.
- Global variables are such variables that can be accessed and modified from any function in the program.
The infographic is a visualization of memory structure, while it may have slight variations, the basic structure is the same in all architectures. The Text Segment allocates memory for source code. Global and Static variables are allocated to Data and BSS segments depending on whether they are initialized or uninitialized respectively. The Stack allocates space for resolving function calls and storing local variables of the called functions. Functions are pushed into the stack and popped after their execution. The heap allocates memory during runtime for dynamic data structures and data items.
There exists an empty memory buffer between the Stack and Heap since they grow as memory is allocated during compilation or runtime. The Stack grows from a higher memory address to a lower memory address. The Heap is exactly the opposite, it grows from a lower memory address to a higher memory address.
Understanding The Stack
Our discussion on Stack began with referring to it as a linear data structure. A stack is based on the LIFO principle, which is an abbreviation for “Last in, First Out”. Elements of a stack operate on a push-and-pop methodology. Let’s consider an example to visualize this structure. Have you ever played badminton? Do you remember a shuttlecock box? That is a stack. A box of shuttle boxes contains some shuttles. Every shuttle is pushed above the shuttle beneath it. When you want to play, you grab the shuttle that is at the top of the box, you cannot pick a random shuttle in between. That is last in, first out. The shuttle that is put inside the box, at last, is taken out first. While the first shuttle can only be taken out at last.
Let’s see how this helps in resolving function calls in memory.
// comicsid - buffer overflow bootcamp
int sum(int arg1, int arg2)
int a=5, b=5;
printf("Sum : %d", sum(a,b));
Let’s break down how the function calls will be resolved and this code will occupy space in the stack. The main() function is pushed in the stack. Local variables a and b occupy space in the stack frame of the main(). The main function calls add(). Function add() is pushed in the stack. Local variables arg1 and arg2 occupy space in the stack frame of add(). The addition is performed and the sum is returned to the main(). Function add() is popped out of the stack. Function main() prints the sum and then it is popped out of the stack. Please note that this is an oversimplification of what happens at the low level.
The Call Stack
A call stack is a stack data structure that stores information about an active procedure or function during a program’s execution. The call stack saves and maintains activation records of stack frames that are pushed into the stack during the execution of a program. The primary goal here is to manage return addresses to prevent stack overflow and memory corruption. Let’s understand the Call Stack of the program we have discussed above.
Let’s refer to the stack structure above. The first thing that happens during memory allocation is the pushing of the stack frame for the main() function. Immediately the stack pointer saves the address of the C runtime environment. The local variables a and b occupy their space in the main() function’s stack frame. When the main function calls add(), a new stack frame is pushed for the add() function. Immediately the stack pointer saves the return address of the main function. Then local variables of add function occupy space in the stack frame, addition is performed and the control flow moves back to the main function. The stack frame of add() is popped from the stack and memory is deallocated. The main function prints the sum of a and b and returns with an exit code. The stack frame of main() is popped from the stack and memory is deallocated. This process is a very high-level overview of what happens during execution in the Stack.
Again, please note that this is a very high-level overview of the process. At the lower level, this is done with multiple registers and pointers to manage the return addresses in the stack frame, but we are learning enough to understand the concept.
Our discussion on memory structure and the call stack has brought us back to our point of concern, which is What is Buffer Overflow and how it occurs. At this point, you might have understood on your own What is a buffer overflow. When the memory allocated to a stack frame exceeds its allocated space then it overwrites or sometimes corrupts the data held in the next adjacent memory address. While it generally occurs in the Stack, it could also happen in the Heap. These are the 2 prominent types of buffer overflow, that is:
- Stack Based Buffer Overflow
- Heap Based Buffer Overflow
Now you must be wondering if you could’ve understood this without understanding the memory, you certainly could’ve. But our discussion was to build a firm foundation that will help us understand real and deliberate buffer overflow attacks.
When an attacker discovers a buffer overflow vulnerability in a program, they attempt to exploit it by overwriting the return address in the call stack with malicious code. A classic attempt at buffer overflow includes the passing of arbitrary code consisting of these 3 parts:
- A chain of NOP bytes (No operation instructions)
- A new return address that points to the NOP bytes
- Arbitrary code in between NOP bytes
In the above call stack structure, a series of no-operation bytes insert an arbitrary code and the return address is overwritten to point to the arbitrary code that initiates a malicious action in the system. We have reached the end of our first article in the series, take your time to understand the concept. If it has driven your curiosity to understand low-level architecture, feel free to research more and understand the call stack in greater detail. If you liked this article, do reach out to me on my socials and share your feedback.
In the next article, we will see buffer overflow in action with some simple C programs. Meanwhile, here are some fun things you can do: