ROP - Return Oriented Programing

Aprende hacking en AWS desde cero hasta experto con htARTE (HackTricks AWS Red Team Expert)!

Otras formas de apoyar a HackTricks:

Información Básica

La Programación Orientada a Retorno (ROP) es una técnica avanzada de explotación utilizada para evadir medidas de seguridad como No Ejecutar (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, evitando efectivamente las protecciones NX/DEP.

Cómo Funciona ROP

  1. Secuestro de 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: Luego, el atacante selecciona cuidadosamente y encadena gadgets para realizar las acciones deseadas. Esto podría implicar configurar argumentos para una llamada de función, llamar a la función (por ejemplo, system("/bin/sh")), y manejar cualquier limpieza necesaria u operaciones adicionales.

  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

Normalmente, los gadgets se pueden encontrar utilizando ROPgadget, ropper o directamente desde pwntools (ROP).

Cadena ROP en Ejemplo x86

Convenciones de Llamada x86 (32 bits)

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

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

Encontrar Gadgets

Primero, asumamos que hemos identificado los gadgets necesarios dentro del binario o sus bibliotecas cargadas. Los gadgets de interés son:

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

  • pop ebx; ret: Similar al anterior, pero para el registro EBX, permitiendo 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 comúnmente un gadget de escribir-dónde-qué.

  • Adicionalmente, 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 deshabilitado 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()

Ejemplo de Cadena ROP en x64

Convenciones de llamada x64 (64 bits)

  • Utiliza la convención de llamada System V AMD64 ABI en sistemas tipo Unix, donde los primeros seis argumentos enteros o de puntero 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 de Windows x64 utiliza RCX, RDX, R8 y R9 para los primeros cuatro argumentos enteros o de puntero, 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.

Encontrar Gadgets

Para nuestro propósito, nos enfocaremos en gadgets que nos permitirán 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 conocemos 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 el ret_gadget para alineación si el entorno objetivo lo requiere, lo cual es más común en x64 para garantizar una alineación adecuada del stack antes de llamar a funciones.

Alineación del Stack

El ABI x86-64 asegura que el stack esté alineado en 16 bytes cuando se ejecuta una instrucción call. LIBC, para optimizar el rendimiento, utiliza instrucciones SSE (como movaps) que requieren esta alineación. Si el stack no está alineado 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 de función 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 obtener esta información:

pageIntroduction 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 Stack: En caso de un desbordamiento de búfer, es necesario evitar los canarios de stack para sobrescribir los 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. Basándose en ROP, se desarrollaron muchas técnicas Ret2XXX:

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

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

pageRet2syscall
  • EBP2Ret & 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).

pageStack Pivoting - EBP2Ret - EBP chaining

Otros Ejemplos y Referencias

Última actualización