Caro leitor, você gosta de desafios? Neste artigo vou contar como resolvi um desafio de engenharia reversa do Shellterlabs, mas sem usar um disassembly!
Para quem não é acostumado com o termo, de acordo com o grupo CTF-BR!, um CTF (Capture The Flag) nada mais é do que uma competição que envolve diversas áreas mas principalmente as áreas ligadas à segurança da informação. No Papo Binário também há um vídeo sobre o assunto.
O desafio em questão é o Shellter Hacking Express Acidentalmente. Em sua descrição, há a seguinte frase: Acidentalmente codificamos a chave.
Isso não diz muita coisa mas ao baixar o binário, percebemos que há dois arquivos:
tar tf ~/Downloads/e74a74b5-86cf-4cb3-a5bb-18a36ef067cf.tgz
RevEng400/
RevEng400/encoder
RevEng400/key.enc
Usando o comando file, verifiquei de que tipo são os arquivos extraídos:
cd RevEng400/
$ file *
encoder: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.2.5, stripped
key.enc: data
Ao ver que o encoder é um binário ELF, fui direto analisar seu código num disassembler usando objdump e gdb, mas percebi que o binário não continha os símbolos, o que torna sua análise um pouco mais difícil.
Sendo iniciante em engenharia reversa e depois de horas analisando a função de cifragem, confesso que fiquei sem saber para onde ir (já viu algum apressadinho tentando aprender a tocar guitarra? Pois é, já quer ir lá tocar aquele solo, e na velocidade Steve Vai, aí não dá né? rs) e desisti, mas não por muito tempo (ei crianças, nunca desistam dos seus sonhos viu! rs), e procurei o nosso querido prof. @Fernando Mercês lá no servidor do Discord, que me deu umas dicas. Segue trecho da conversa:
> @fernandom @gzn sei que vc ta treinando ER, mas nem precisa disassemblar esse binario pra esse desafio nego
> se vc olhar bem, vai ver que a saída do encoder tem o tamanho da string de entrada + o byte 0x03 no final
> olhando a chave encodada (key.enc), é razoável admitir que ela tenha 16 caracteres então
> você só precisa encontrar qual deles é o 0xef .. um loop com bash mata
> supondo que seja o 'A'... então 'A' -> 0xef, aí você vai precisar da letra que gera o 0xf9 e assim sucessivamente, até chegar em 16
Já ouviu a expressão "pensar fora da caixa"? Pois é! Por que eu fui direto disassemblar? Esse é um dos problemas quando nós estamos começando: às vezes a gente acha que o método mais difícil deve ser o único ou o melhor para se resolver problemas, mas nem sempre é assim. Daí pensei: se o Mercês falou que não é muito difícil, vamos ao menos tentar não é?
Bem, a primeira coisa que fiz foi ver uma maneira de imprimir o conteúdo do binário em hexadecimal. Para isso criei um pequeno script que usa o hexdump para me dar uma saída somente com os bytes em hexadecimal do parâmetro que receber. Chamei o script de hexdump.sh e depois dei permissão de execução nele (chmod +x). Seu conteúdo é o seguinte:
#!/bin/sh
hexdump -v -e '/1 "%02X "' $1
Então comecei os testes:
for letra in 0 A a; do echo -n "$letra "; ./encoder $letra | ./hexdump.sh; echo; done
0 7F 01
A F7 02
a F7 03
Parece que nem sempre o final é 0x03... Bem, fui verificar o conteúdo do arquivo key.enc e encontrei isso:
./hexdump.sh < key.enc
EF F9 42 09 A3 1A 43 F7 8C 8B BB 22 2A C2 A3 14 03
Pela lógica, já que essa é a chave codificada, se eu passar a chave correta original em texto como parâmetro para o binário encode ele terá que gerar a sequência acima. Seguindo a dica do Mercês, usei o próprio bash para tentar quebrar o desafio, primeiro mostrando o conteúdo em hexadecimal da chave codificada, depois iterando pelos caracteres possíveis e filtrando pelo primeiro byte dela:
hexdump.sh < key.enc; echo
for ((i=32;i<=126;i++)); do
> l=$(printf "\x$(printf "%x" $i)")
> echo -n "$l "
> ./encoder "$l" | ./hexdump.sh
> echo
done | grep 'EF'
Este código basicamente faz:
Mostra os bytes em hexadecimal da chave a cada vez que executarmos esse comando (só pra saber qual byte é o próximo).
Itera por todos caracteres imprimíveis da tabela ASCII (faixa de 32 à 126 em decimal).
Imprime o caractere na tela sem a quebra de linha.
Passa essa letra para como argumento para o binário encode e imprime a saída dele em hexadecimal.
Por fim, usa o grep para encontrar uma combinação que tenha o próximo byte da chave.
Partindo para um exemplo prático, fui tentar encontrar a primeira letra dessa chave, sabendo que sua versão codificada deve resultar no byte 0xEF:
./hexdump.sh < key.enc; echo
EF F9 42 09 A3 1A 43 F7 8C 8B BB 22 2A C2 A3 14 03
for ((i=32;i<=126;i++)); do
> caractere=$(printf "\x$(printf "%x" $i)")
> echo -n "$caractere ";./encoder "$l" | ./hexdump
> echo
done | grep 'EF'
" EF 01
B EF 02
b EF 03
Conforme pode ver acima, encontrei três caracteres diferentes que, quando encodados pelo encoder, geram o byte 0xEF: ", B, e b. Escolhi seguir com o B, prefixando-o na chave para dar sequência ao script e ver se encontramos o caractere que resulta no próximo byte da chave codificada (0xF9):
./hexdump.sh < key.enc; echo
EF F9 42 09 A3 1A 43 F7 8C 8B BB 22 2A C2 A3 14 03
for ((i=32;i<=126;i++)); do
> caractere=$(printf "\x$(printf "%x" $i)")
> echo -n "B${caractere} "
> ./encoder "B$l" | ./hexdump
> echo
done | grep 'EF F9'
% EF F9 01
E EF F9 02
e EF F9 03
Mais uma vez encontrei três opções. Foi só continuar este processo até encontrar a chave que gera os exatos 16 bytes do arquivo key.enc.
Aproveitei e automatizei um brute forcer com Python:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import subprocess
def encode(arg):
result = subprocess.run(['./encoder', arg], stdout=subprocess.PIPE)
return result.stdout
def loadKey():
key_enc = []
with open('./key.enc', 'rb') as file:
while True:
byte = file.read(1)
if byte:
# a ordem dos bytes aqui não importa (porque trata-se apenas de 1 byte), mas é necessário especificar
key_enc.append(int.from_bytes(byte, byteorder='little'))
else:
break
return key_enc
def permutations(key='', key_enc=loadKey(), key_i=0):
if key_i == len(key_enc) - 1:
print(key)
return
for char in (chr(i) for i in range(32, 127)):
result = encode(key + char)
if result[key_i] == key_enc[key_i] and key_i < len(key_enc):
permutations(key=key + char, key_enc=key_enc, key_i=key_i + 1)
def main():
permutations()
if __name__ == "__main__":
main()
Saída codificada em base64 (pra não estragar a brincadeira de quem vai tentar resolver o desafio por conta própria):
QmV3aXRjaGluZyBTZXh0LwpCZXdpdGNoaW5nIFNleHRPCkJld2l0Y2hpbmcgU2V4dG8K
Segue vídeo do canal com a explicação do algortimo de encoding desse desafio: