BINARY EXPLOITATION: Stack Frames, RSP and RBP

Table of Contents

  1. Example code
  2. Stack Frames
  3. The role of RSP (Stack Pointer)
  4. The role of RBP (Base Pointer)
  5. Function Prologue
  6. Function Epilogue
  7. 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:

  • RSP
  • RBP

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 rbp Save 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,rsp Modify the base pointer with the stack pointer. This prepares the stage for creating the space for the new stack frame.

  • sub rsp,0x20 Create the space for the new stack frame. In this case a space of 32 bytes is created.

  • mov QWORD PTR [rbp-0x18],rdi Move 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
  • leave The 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, rbp Restore the stack pointer to the base pointer. This deallocates the stack frame used by the function.

    • pop rbp Restore the base pointer of the caller. This ensures that when returning to the caller, the stack frame structure is correct.

  • ret The ret instruction implements the following operation.

    pop rip
    
    • pop rip Restores 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