BINARY EXPLOITATION: Stack Frames, RSP and RBP
Table of Contents
- Example code
- Stack Frames
- The role of RSP (Stack Pointer)
- The role of RBP (Base Pointer)
- Function Prologue
- Function Epilogue
- Call Stack
1. Example code
Consider the following C code:
#include <stdio.h>
void hello(char *name) {
int age = 42;
printf("Hello %s, your age is: %d
", name, age);
}
int factorial(n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n-1);
}
}
int main(void) {
char *name = "Leonardo";
hello(name);
return 0;
}
When this code is compiled into an ELF binary, the following x86-64 instructions are generated by the compiler.
gcc -ggdb hello.c -o hello
main()
Dump of assembler code for function main:
0x000000000000116d <+0>: push rbp
0x000000000000116e <+1>: mov rbp,rsp
0x0000000000001171 <+4>: sub rsp,0x10
0x0000000000001175 <+8>: lea rax,[rip+0xea3] # 0x201f
0x000000000000117c <+15>: mov QWORD PTR [rbp-0x8],rax
0x0000000000001180 <+19>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000001184 <+23>: mov rdi,rax
0x0000000000001187 <+26>: call 0x1139 <hello>
0x000000000000118c <+31>: mov eax,0x0
0x0000000000001191 <+36>: leave
0x0000000000001192 <+37>: ret
End of assembler dump.
hello()
Dump of assembler code for function hello:
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x20
0x0000000000001141 <+8>: mov QWORD PTR [rbp-0x18],rdi
0x0000000000001145 <+12>: mov DWORD PTR [rbp-0x4],0x2a
0x000000000000114c <+19>: mov edx,DWORD PTR [rbp-0x4]
0x000000000000114f <+22>: mov rax,QWORD PTR [rbp-0x18]
0x0000000000001153 <+26>: mov rsi,rax
0x0000000000001156 <+29>: lea rax,[rip+0xea7] # 0x2004
0x000000000000115d <+36>: mov rdi,rax
0x0000000000001160 <+39>: mov eax,0x0
0x0000000000001165 <+44>: call 0x1030 <printf@plt>
0x000000000000116a <+49>: nop
0x000000000000116b <+50>: leave
0x000000000000116c <+51>: ret
End of assembler dump.
As you can see, each function starts and ends with the similar instructions.
Function Prologue for hello()
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x20
0x0000000000001141 <+8>: mov QWORD PTR [rbp-0x18],rdi
Function Epilogue for hello()
0x000000000000116b <+50>: leave
0x000000000000116c <+51>: ret
These instructions are used to manage the stack frames of the various functions.
2. Stack Frames
A stack frame is a data structure used to store information about a specific function call and it is used by the processor during the execution of a program.
A stack frame for a given function typically contains the following information:
- Return Address: The address of the instruction to execute after the function returns.
- Function Arguments: The arguments passed to the function call.
- Local Variables: The local variables defined within the function.
To manage the stacks, two registers are used:
RSPRBP
3. The role of RSP (Stack Pointer)
The stack pointer is used to keep track of the top of the stack, which is the area of memory that represents the boundary between the used stack and the space still available.
Remember that in x86-64 the stack grows downwards. That it, it grows towards lower addresses. We thus have the following situation:
0x0x7ffffff0e410 <-- lower address
...
0x0x7fffffffe410
...
0x0x7fffffffffff <-- higher address
For this reason, when we want to “grow the stack”, we perform a sub instruction to reduce the value of the RSP, which represents the top of the stack.
Consider the following example:
Before sub rsp,0x20
0x7ffffff0e410 <-- lower address
...
0x7fffffffe410 <-- RSP
...
0x7fffffffffff <-- higher address
After sub rsp,0x20
0x7ffffff0e410 <-- lower address
...
0x7fffffffe3f0 <-- RSP
0x7fffffffe3f1
...
0x7fffffffe40f
0x7fffffffe410 <-- previous RSP
...
0x7fffffffffff <-- higher address
4. The role of RBP (Base Pointer)
The base pointer is used to access the stack frame during the execution of a function call. The presence of the base pointer is useful because the stack might grow or shrink depending on the different execution paths taken.
Consider a more complex example:
void hello(char *name) {
int age = 42;
if (age < 30) {
int young = 1;
printf("Hello %s, your age is: %d and your young flag is:
", name, age, young);
} else {
printf("Hello %s, your age is: %d and your young flag is:
", name, age, 0);
}
}
And consider the disassembled code.
Dump of assembler code for function hello:
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x20
0x0000000000001141 <+8>: mov QWORD PTR [rbp-0x18],rdi
0x0000000000001145 <+12>: mov DWORD PTR [rbp-0x8],0x2a
0x000000000000114c <+19>: cmp DWORD PTR [rbp-0x8],0x1d
0x0000000000001150 <+23>: jg 0x117c <hello+67>
0x0000000000001152 <+25>: mov DWORD PTR [rbp-0x4],0x1
0x0000000000001159 <+32>: mov ecx,DWORD PTR [rbp-0x4]
0x000000000000115c <+35>: mov edx,DWORD PTR [rbp-0x8]
0x000000000000115f <+38>: mov rax,QWORD PTR [rbp-0x18]
0x0000000000001163 <+42>: mov rsi,rax
0x0000000000001166 <+45>: lea rax,[rip+0xe9b] # 0x2008
0x000000000000116d <+52>: mov rdi,rax
0x0000000000001170 <+55>: mov eax,0x0
0x0000000000001175 <+60>: call 0x1030 <printf@plt>
0x000000000000117a <+65>: jmp 0x119f <hello+102>
0x000000000000117c <+67>: mov edx,DWORD PTR [rbp-0x8]
0x000000000000117f <+70>: mov rax,QWORD PTR [rbp-0x18]
0x0000000000001183 <+74>: mov ecx,0x0
0x0000000000001188 <+79>: mov rsi,rax
0x000000000000118b <+82>: lea rax,[rip+0xe76] # 0x2008
0x0000000000001192 <+89>: mov rdi,rax
0x0000000000001195 <+92>: mov eax,0x0
0x000000000000119a <+97>: call 0x1030 <printf@plt>
0x000000000000119f <+102>: nop
0x00000000000011a0 <+103>: leave
0x00000000000011a1 <+104>: ret
Within the code it is possible to see the common pattern of access to the stack frame memory using the rbp register.
QWORD PTR [rbp-0x18] -> argument *name
DWORD PTR [rbp-0x8] -> local variable int age = 42
DWORD PTR [rbp-0x4] -> local variable int young = 1
5. Function Prologue
We can now understand how the function prologue is implemented.
0x0000000000001139 <+0>: push rbp
0x000000000000113a <+1>: mov rbp,rsp
0x000000000000113d <+4>: sub rsp,0x20
0x0000000000001141 <+8>: mov QWORD PTR [rbp-0x18],rdi
-
push rbpSave the old base pointer to the stack so that later it can be recovered. This is key, as at this point the base pointer value is used to determine the stack frame of the caller. -
mov rbp,rspModify the base pointer with the stack pointer. This prepares the stage for creating the space for the new stack frame. -
sub rsp,0x20Create the space for the new stack frame. In this case a space of 32 bytes is created. -
mov QWORD PTR [rbp-0x18],rdiMove the argument that was passed to the function within the stack frame of the new function. To access this value you can offset the base pointer.
6. Function Epilogue
We can now understand how the function epilogue is implemented.
0x000000000000116b <+50>: leave
0x000000000000116c <+51>: ret
-
leaveThe leave instruction implements the following operations and it is used to restore the base pointer of the caller to the original value.mov rsp, rbp pop rbp-
mov rsp, rbpRestore the stack pointer to the base pointer. This deallocates the stack frame used by the function. -
pop rbpRestore the base pointer of the caller. This ensures that when returning to the caller, the stack frame structure is correct.
-
-
retThe ret instruction implements the following operation.pop rippop ripRestores the value of the instruction pointer by using the one saved in the stack frame.
7. Call Stack
The call stack is obtained by having a sequence of stack frames one on top of another. It is managed in a LIFO approach.
LIFO -> Last-In, First-Out
In gdb you can print out the call stack as follows:
(gdb) bt
#0 functionC (z=32767) at hello.c:5
#1 0x00005555555551af in functionB (y=15) at hello.c:13
#2 0x00005555555551e9 in functionA (x=20) at hello.c:19
#3 0x000055555555525c in main () at hello.c:32
If you’re interested in specific stack frames instead you can use the info frame command.
(gdb) info frame 0
Stack frame at 0x7fffffffe3f0:
rip = 0x555555555149 in functionC (hello.c:5); saved rip = 0x5555555551af
called by frame at 0x7fffffffe420
source language c.
Arglist at 0x7fffffffe3e0, args: z=32767
Locals at 0x7fffffffe3e0, Previous frame's sp is 0x7fffffffe3f0
Saved registers:
rip at 0x7fffffffe3e8