Controlling rbp
A method of arbitrary writing
Last updated
A method of arbitrary writing
Last updated
Once again, let's go back to the demo program.
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.
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.
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.
rsp
will typically point to the bottom of the stack (address-wise), and rbp
to the top of the current stack frame.
When main
is called:
call main
will push rip
(the address of the instruction after call
), which is the return address.
Then it jumps to the function.
push rbp
will push the previous value of rbp
to the stack. This becomes the saved base pointer.
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.
sub rsp, 0x20
allocates the space for the variables (the space used for variables is between rsp
and rbp
)
The code of the function
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
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
.
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!
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 which used this exact technique, and if you want to see an example of how you could use this, check out my writeup.
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).
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.