---
description: A method of arbitrary writing
---

# Controlling rbp

Once again, let's go back to the demo program.

```c
// gcc demo.c -o demo -no-pie -fno-stack-protector
#include <stdio.h>

int main() {
	char buf[0x20];
	puts("ROP me if you can!");
	gets(buf);
}
```

The main idea we'd have when overflowing this to just control the return address to get RCE. And in most cases that's sufficient, however with the lack of `pop rdi ; ret`, we're forced to look for alternatives.

There's another value that's overwritten when we overflow, and that is the saved base pointer. While not as exciting as the return address, it can also be very useful.

### What is the saved base pointer?

In our case, the stack is managed using 2 registers: `rsp` and `rbp`.

* `rsp` is the stack pointer, and is used when popping and pushing values.
* `rbp` is the base pointer, and is used to determine where the variables on the stack are located.

{% hint style="info" %}
Sometimes In optimized code, only `rsp` is needed, and `rbp` is instead used as a scratch variable. This is common to see in glibc code. However, this isn't relevant here.
{% endhint %}

`rsp` will typically point to the bottom of the stack (address-wise), and `rbp` to the top of the current stack frame.

<figure><img src="https://281452579-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FO4GaVx6h6D06xlxKSULj%2Fuploads%2FF3tT4aXs6yYewB8qqTCB%2Fimage.png?alt=media&#x26;token=35d6a141-44e7-4a68-aef8-51704a5cc335" alt=""><figcaption><p>Disassembly of <code>main</code></p></figcaption></figure>

When `main` is called:

1. `call main` will push `rip` (the address of the instruction after `call`), which is the return address.\
   Then it jumps to the function.
2. `push rbp` will push the previous value of `rbp` to the stack. This becomes the saved base pointer.
3. `mov rbp, rsp` makes `rbp` now point to the recently pushed "saved base pointer". This is important to recognise, that `rbp` points to the saved base pointer of the _previous function._
4. `sub rsp, 0x20` allocates the space for the variables (the space used for variables is between `rsp` and `rbp`)
5. The code of the function
6.  `leave` will restore the previous values of `rbp` and `rsp` before `push rbp` (i.e. at the very start of the function). It does this by effectively doing

    ```nasm
    mov rsp, rbp
    pop rbp
    ```
7. `ret` now jumps back to the previous function, with `rsp` and `rbp` back to their original values.

And we also see that when `gets` is called, the argument is loaded using `lea rax, [rbp-0x20]`, showing that the `buf` buffer is relative to `rbp`.

### Arbitrary write

However, in the process above, when we overflow, things start going wrong. The obvious one is changing the return address to jump somewhere else, but right before the return address, is the saved base pointer. When we overwrite that, we end up controlling the value of `rbp` when we return.

Combining this with the fact that we have a gadget that does `gets(rbp-0x20)`, we can return back to this with a controlled value of `rbp`, and get an arbitrary write!

### Overwriting `GOT`

The scope of this write is limited when we don't have any leaks, but one example of a useful target is `GOT`. In fact, I wrote a challenge for [HTB Business CTF 2024](https://www.hackthebox.com/events/htb-business-ctf-2024) which used this exact technique, and if you want to see an example of how you could use this, check out [my writeup](../../ctf-writeups/htb-business-2024/no-gadgets).

### Other targets

Overwriting `GOT` can be useful in some cases, but what about if there are no good targets in `GOT`, or `FULL RELRO` is enabled?

Aside from that, your targets for a write could be global variables used by the program for example.

Another idea is to create fake objects in a known location, which can be useful for certain things, such as `ret2dlresolve` (covered [here](dlrop)).

Another idea is to forge a fake stack frame, and to return to a certain section of a function. Since all variables are relative to `rbp`, by pointing `rbp` to a controlled buffer, you could "set" certain variables to controlled values, such as pointers to buffers, size variables, and so on.

Unfortunately both of these are dependent on what the binary does, so there aren't many universal approaches for when `FULL RELRO` is enabled, but this can still remain to be a useful trick which may aid exploitation in some cases.
