The Basics of Hacking: Part 3
So we’ve all compiled programs before, but do you know how your computer divided up and saved the different parts of the program? Be patient, this kind of overwhelmed me at first. Let’s jump in.
A compiled program is broken into five segments: text, data, bss, heap, and stack.
The text segment is where the machine language instructions of the program are located. When a program begins executing, the RIP (the register that points to the currently executing instruction), is set to the first machine language instruction in the text segment. The processor than follows a execution loop as it steps through the instructions:
- Reads the instruction that RIP is pointing to
- Adds the byte length of the instruction to RIP
- Executes the instruction that was read in step 1
- Goes back to step 1
You cannot write to the text segment of memory. Any attempt to do so will result in the program being killed. The text memory segment is of fixed size.
- The data section is used to store initialized global and static variables.
- The bss section is used to store uninitialized global and static variables.
Both these sections of memory are writable although they are fixed in size. Global and static variables are able to persist — regardless of function context — because they are stored in their own memory segments.
The heap section of memory is directly under the programmers control. In C, programmers can use the function malloc() to dynamically allocate memory on the heap. The heap is not of fixed size and can grow larger or smaller. The growth of the heap “moves downward toward higher memory addresses” (Hacking: The Art of Exploitation, Jon Erickson). Let’s illustrate this with a program that makes subsequent memory allocations on the heap:
Let’s execute the program to see the memory addresses:
The first address printed is 0x21c5010 and the second one is below it (visually) at 0x21c51b0 (a higher address).
The stack section of memory is used to store local function variables and context during function calls. The stack is not of fixed size. When a function is called,
“that function will have its own set of passed variables, and the function’s code will be at a different memory location in the text segment. Since the context and the RIP must change when a function is called, the stack is used to remember all of the passed variables, the location the RIP should return to after the function is finished, and all the local variables used by that function,” (Erickson).
This information is stored on a stack frame. The stack (the memory stack) is made up of many different stack frames. Opposite to the heap, the stack grows upward (visually) towards lower memory addresses.
Since the stack and heap are both dynamic, that is, their size changes depending on how much memory the programmer is using, it makes sense that they grow in opposite directions towards one another. This “minimizes wasted space, allowing the stack to be larger if the heap is small and vice-versa,” (Erickson).
The RSP register holds the address of the end of the stack. It’s important to note that the RSP register is manipulated implicitly by several CPU instructions: PUSH, POP, CALL RET, etc. This is why we don’t see assembly instructions resetting the value of the RSP register after a CALL instruction.
When a function is called, several things are pushed onto the stack frame: the RBP register (also known as the framer pointer (FP)) which
“is used to reference local function variables in the current stack frame, […] the parameters to the function, its local variables, and two pointers that are necessary to put things back the way they were: the saved frame pointer (SFP) and the return address. The SFP is used to restore EBP to its previous value, and the return address is used to restore RIP to the next instruction found after the function call,” (Erickson).
The RBP register is manipulated only explicitly.
Let’s illustrate what’s happening on the stack when we run a simple function:
Let’s disassemble the main() function:
You can see that each of the parameters for test_function() are pushed onto the stack. When the CALL instruction is executed,
“the return address is pushed onto the stack and the execution flow jumps to the start of the test_function() at 0x40055d,” (Erickson).
The return address in this case will be the instruction following the CALL instruction. After the test_function() returns, the RIP will point to 0x4005be.
Let’s disassemble test_function():
Notice that RBP is pushed onto the stack. This is referred to as the saved frame pointer (SFP) and is later used to restore RBP back to a previous frame. RSP is than copied into RBP to set the new frame pointer. This makes sense as RBP is used to refer to local function variables in the current stack frame. Also notice that RBP is being manipulated explicitly. Finally, we see that 40 is subtracted from the value of RSP, this is to save memory for the local variables buffer and flag.
We can observe how the stack changes using GDB. First let’s add some breakpoints:
Let’s jump to the next break point:
You can see that the RSP, RBP, and RIP registers moved down the address space to lower addresses. We also see that the difference between RSP and RBP is 40 which makes sense because of the “SUB RSP, 0X40” instruction we saw in the test_functions disassembly.
In the end, the stack frame looks like this:
After the test_function() terminates the stack frame gets popped off the stack and the RIP gets set to the return address so the program can continue executing. RBP also gets set to the value of the saved frame pointer so it can reference local variables in the previous stack frame. This cycle of stack frames being created and terminated continues until the program finishes executing.
Subscribe to get your daily round-up of top tech stories!