Pular para o conteúdo
Novo

DeepSeek V4.1 Flash grátis por 7 dias: 10M tokens por dia após qualquer recarga. Até 30/11.

Como funciona

Introdução ao Ghidra com um binário benigno que você compila

Compile uma verificação de licença de brinquedo em C de duas formas e use o Ghidra para achar a string, seguir referências e comparar com seu código.

Intermediário14 min de leituraPor NotHoly

Engenharia reversa é mais fácil de aprender com código que você mesmo escreveu. Você sabe o que o programa deveria fazer, então consegue checar cada afirmação que a ferramenta faz sobre ele. Este artigo segue essa ideia. Você vai escrever um programa pequeno em C com uma verificação de licença de brinquedo, compilá-lo de duas formas e abrir as duas versões no Ghidra. Depois, vai comparar o que o decompilador diz com o código-fonte que você digitou.

A principal lição não está nos botões do Ghidra. Quando você coloca a saída do decompilador ao lado do seu próprio código, aprende rápido o que a ferramenta acerta, o que ela chuta e o que perde quando o compilador otimiza.

Se a ideia de ler código de máquina é nova para você, leia primeiro O que é engenharia reversa?. Você não precisa de experiência prévia com assembly. Precisa de um terminal e de cerca de uma hora.

O que é o Ghidra

O Ghidra é um framework de engenharia reversa de software desenvolvido pela Agência de Segurança Nacional dos Estados Unidos (NSA) e publicado no GitHub, no repositório NationalSecurityAgency/ghidra. Ele roda em Windows, macOS e Linux. Para um programa compilado, faz quatro coisas que importam aqui:

  • Desmonta o código de máquina em instruções de assembly.
  • Decompila funções para um pseudocódigo parecido com C, com o decompilador que vem embutido.
  • Encontra strings e referências cruzadas (XREFs), que mostram em que pontos do programa uma string ou um endereço é usado.
  • Guarda suas alterações em um projeto, incluindo os nomes que você dá a funções e variáveis.

O Ghidra não recupera o código-fonte original. Ele produz uma aproximação. O pseudocódigo costuma ter forma parecida com a do seu código, mas nomes, tipos e parte do fluxo de controle podem ser diferentes do que você escreveu. Essa diferença é o que este laboratório foi desenhado para mostrar.

Por que analisar um programa que você escreveu

Você poderia baixar um binário de exemplo de um fórum e começar a olhar. Não faça isso. Binários desconhecidos podem ser malware, e você não teria como conferir a saída da ferramenta, porque não sabe a resposta certa.

Um programa que você compila sozinho dá três vantagens:

  1. Verdade de referência. Você sabe que check() compara o argumento com "LAB-1234".
  2. Duas versões do mesmo código. Uma mantém informações de depuração e símbolos. A outra vem sem símbolos e otimizada. Comparar as duas mostra o que o compilador removeu.
  3. Um alvo seguro. Ele só imprime ok ou no. Não faz chamadas de rede nem grava arquivos.

Tudo neste laboratório fica na sua máquina. Você está analisando um arquivo seu, que é o único escopo necessário para este exercício.

Preparar o laboratório

O que você precisa

  • Um compilador C. No Linux, instale o gcc pelo gerenciador de pacotes. No macOS, o clang das ferramentas de linha de comando do Xcode serve para este laboratório, e as flags são as mesmas. No Windows, use o WSL ou um contêiner com GCC.
  • Um JDK para o Ghidra. O Ghidra roda em Java, então o JDK precisa estar instalado antes de o lançador abrir.
  • O Ghidra, baixado das releases oficiais no GitHub.
  • Um terminal com sha256sum. O Git Bash no Windows já inclui o comando.

Instalar o JDK e o Ghidra

Instale o JDK que a versão do Ghidra pede, e não confie num guia antigo para esse número. Quando conferi o README do repositório e o guia na data em que escrevi, o README e o guia de primeiros passos pediam o JDK 25 de 64 bits, e o README apontava para as builds Temurin. Leia o README ou as notas da versão que você baixar e use a versão indicada ali. Versão de Java incompatível é um motivo comum para o Ghidra não abrir.

Baixe o Ghidra apenas nas releases oficiais, em github.com/NationalSecurityAgency/ghidra. O repositório aponta para os arquivos de cada release. Descompacte em uma pasta nova. O guia de primeiros passos diz para não extrair o Ghidra por cima de uma instalação existente. Inicie com ghidraRun no Linux e no macOS, ou ghidraRun.bat no Windows.

Conferir o download

Antes de descompactar qualquer coisa, compare o arquivo com o valor SHA-256 publicado. Execute isto na pasta onde você salvou a release:

bash
sha256sum ghidra_*.zip

Compare o hash impresso com o valor SHA-256 que o GitHub mostra ao lado do arquivo na página da release, ou que o projeto publica. Ele precisa ser idêntico. Se você não achar um SHA-256 publicado para aquele arquivo, anote isso nas suas notas do laboratório. Não trate como checksum do publicador um hash copiado de um espelho qualquer.

Conferir o hash só prova que o arquivo é igual ao que o publicador listou. Não prova que o publicador é confiável. Use a fonte oficial e guarde nome do arquivo e hash nas suas notas.

Escrever o programa de brinquedo

Crie uma pasta vazia para o laboratório. Mantenha o código-fonte e os binários juntos, e guarde os projetos do Ghidra fora dela, para não bagunçar a pasta.

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

O programa lê um argumento da linha de comando. Se o argumento for igual a LAB-1234, imprime ok. Caso contrário, imprime no. Essa verificação não tem valor de segurança. É uma string fixa dentro de um arquivo que qualquer pessoa pode ler, e esse é o ponto. Você vai encontrar a string no binário em poucos minutos.

Registre o hash do seu código-fonte para conseguir provar depois qual arquivo analisou:

bash
sha256sum toy.c

Na máquina que usei, o comando imprimiu:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

O seu hash vai ser diferente se o arquivo tiver outros espaços ou outras quebras de linha.

Compilar duas vezes

Compile o mesmo código-fonte em dois binários com configurações diferentes:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

As flags fazem coisas diferentes. Ambas vêm do manual do GCC, que vale ler uma vez:

  • -O0 desliga a maior parte das otimizações. É o padrão, e o manual descreve essa configuração como a que mantém a depuração previsível. O código segue de perto o fonte.
  • -O2 ativa a maior parte das otimizações que não trocam tamanho por velocidade. O manual diz que essa opção também faz inlining de funções pequenas, mesmo as que não foram declaradas como inline. O código pode parecer bem diferente do fonte.
  • -g adiciona informações de depuração no formato nativo do sistema operacional. O Ghidra lê parte delas, e depuradores usam tudo.
  • -s remove a tabela de símbolos do arquivo final, no momento da linkagem. Os nomes check e main deixam de ficar guardados no arquivo.

Dois binários com o mesmo código e flags diferentes é o que torna o laboratório útil. Guarde os dois.

Executar as duas versões

Execute os binários antes de abrir o Ghidra. Um programa que você não rodou é um programa que você ainda não entende.

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

Eu executei dentro de um contêiner Debian com GCC 14.2.0, em um host x86-64. A saída foi:

text
ok
no
no
ok
no

As duas versões se comportam igual. Remover símbolos e otimizar não mudou a lógica, que é o esperado. Se a sua saída for diferente, pare e descubra o motivo antes de seguir.

Inspecionar os binários pela linha de comando

Antes do Ghidra, veja o que está guardado em cada arquivo. Duas ferramentas bastam: nm lista símbolos, e strings lista textos legíveis.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

Na máquina que usei, a saída foi:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

Leia essas linhas com calma:

  • O binário de depuração ainda nomeia check e main. Esses nomes vieram do seu código-fonte.
  • O binário sem símbolos não tem nada para listar. O nm não encontra nenhum símbolo.
  • A string LAB-1234 sobrevive nos dois arquivos. Uma string é dado, e remover símbolos não tira dados que o programa precisa em tempo de execução. Ela fica no arquivo como constante, e é isso que torna o laboratório fácil de começar.

Agora desmonte check no binário de depuração:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

A saída ficou assim:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

Você reconhece o seu código nessas instruções. Uma chamada a strcmp, um teste sobre o valor de retorno e um sete que transforma "igual a zero" em 0 ou 1. Guarde isso na cabeça quando for ler o decompilador.

Agora olhe o build otimizado. Desmontei o toy_stripped do mesmo jeito. O compilador colocou a comparação direto em main, que fica em 0x1060 no meu build: um cmp edi,0x1 para o teste c>1, depois mov rdi,[rsi+0x8] para carregar v[1], depois call strcmp@plt. Ele ainda manteve uma cópia separada de check em 0x1190, porque a função não é static e outro arquivo poderia chamá-la. Os endereços mudam conforme o seu toolchain, então use isso como um mapa do que procurar, e não como números para igualar.

Importar no Ghidra

Os nomes exatos dos menus mudam conforme a versão do Ghidra, e eu não rodei a interface gráfica na máquina onde este artigo foi conferido. Os passos abaixo seguem a documentação oficial e o que a interface costuma mostrar. Confira cada nome com a sua versão. Tire uma captura da sua própria tela ao final de cada passo, e não reaproveite capturas de outro tutorial.

  1. Inicie o Ghidra com ghidraRun (ou ghidraRun.bat).
  2. Escolha File > New Project. Selecione Non-Shared Project e dê um nome, como toy-lab. Salve em uma pasta sob seu controle.
  3. Escolha File > Import File. Selecione toy_debug. Aceite o formato detectado, se bater com o seu sistema. Um arquivo gerado em Linux x86-64 deve aparecer como ELF com processador x86.
  4. Repita a importação com toy_stripped. Agora o projeto tem duas entradas.
  5. Abra cada arquivo na ferramenta CodeBrowser. Quando o Ghidra perguntar se deve analisar o programa, aceite a análise padrão para este laboratório.

Espere a análise terminar. O Ghidra mostra o progresso na barra inferior. Não comece a renomear nada antes disso.

Encontrar a string e seguir suas referências

Comece pelo toy_stripped, que é o caso mais difícil. Depois compare com o toy_debug.

  1. Abra Window > Defined Strings.
  2. Encontre LAB-1234 na lista. Dê duplo clique para ir ao endereço dela na listagem.
  3. Clique com o botão direito no endereço da string e abra o menu de referências. Procure um item como References > Show References to. Cada referência é um ponto do código que lê esse endereço.
  4. Siga a referência. Ela leva até a função que compara a sua entrada com a string.

No build de depuração, o Ghidra também deve mostrar o nome check da tabela de símbolos. No build sem símbolos, ele vai mostrar um nome gerado, como FUN_00101190. O número depende do build, então use o nome que aparecer na sua tela.

Faça o mesmo com as strings "ok" e "no". Cada uma é usada em main. Seguir essas referências a partir de main mostra o mesmo quadro pelo outro lado.

Ler o decompilador ao lado do seu código

Abra o painel do decompilador na função que compara a string. Deixe o toy.c aberto em outra janela, ao lado.

O resultado que você deve procurar é deste tipo, descrito como ilustração de como ler, e não como saída de uma versão específica do Ghidra:

  • Uma função com um argumento que é ponteiro para caracteres.
  • Uma chamada a uma função chamada strcmp, ou um símbolo que a ferramenta rotula como strcmp.
  • Uma comparação que retorna 1 quando o resultado é zero.

Agora liste as diferenças que você vê. As mais comuns são:

  • Tipos. O decompilador pode mostrar um char * como char *, ou usar um tipo mais vago, como undefined8. Ele pode não saber que a entrada é const.
  • Nomes. Seu parâmetro k aparece como param_1 ou um marcador parecido.
  • Valor de retorno. A expressão strcmp(...) == 0 pode aparecer como uma comparação seguida de um retorno condicional. O sentido é o mesmo, mas o formato não.
  • Tratamento de argumentos em main. A sua checagem c > 1 vira uma comparação sobre a contagem de argumentos. Preste atenção no deslocamento de um: argv[0] é o nome do programa, e v[1] é o primeiro argumento de verdade.

O objetivo não é dar nota ao decompilador. O objetivo é notar onde a história dele e a sua batem, e onde divergem. Quando divergirem, volte à desmontagem e ao código-fonte e decida qual está certo.

Renomear variáveis no build sem símbolos

Agora faça a parte que torna um binário sem símbolos legível.

  1. No painel do decompilador, clique no parâmetro que guarda a string de entrada.
  2. Clique com o botão direito e escolha Rename Variable, ou selecione a variável e pressione L. Dê o nome input.
  3. Renomeie a própria função na listagem ou na árvore de símbolos para check_input.
  4. Em main, renomeie o vetor de argumentos para argv e a contagem para argc.

O Ghidra guarda esses nomes no seu projeto. O binário no disco não muda. Isso importa: você está documentando a sua leitura, e não editando o arquivo que compilou.

Repare em quanto o código fica mais fácil depois de renomear. Na primeira vez, os nomes das variáveis eram chutes. Agora batem com o seu código-fonte. É esse processo que engenheiros reversos usam no dia a dia, e é também por isso que nomes em um programa decompilado devem ser tratados como hipóteses até você conferir.

Comparar os dois builds

Coloque a decompilação de depuração e a sem símbolos lado a lado. Responda estas perguntas nas suas notas:

  • Quais nomes de função sobreviveram no build sem símbolos?
  • O build -O2 fez inlining de check dentro de main, ou manteve uma função separada? Procure, na listagem, um ponto de chamada separado para strcmp e um ret separado.
  • Quais partes do seu código são fáceis de ver nas duas visões?
  • Quais partes são difíceis de achar na visão sem símbolos?

Escreva uma frase para cada resposta. Se não conseguir responder alguma, isso também é dado útil. Volte à listagem e encontre a instrução que deixa aquilo confuso.

Erros comuns

Pular a etapa de execução. Se você abre o binário no Ghidra antes de rodá-lo, não sabe qual é o comportamento correto. Rode primeiro e registre as saídas.

Confiar nos nomes do decompilador. Um nome como FUN_00101190 ou local_18 é um marcador. Renomear é uma afirmação sua, e precisa de evidência.

Trocar os builds. Os binários de depuração e sem símbolos têm endereços e nomes diferentes. Anote o nome de cada arquivo e confira em qual entrada do projeto você está antes de renomear qualquer coisa.

Usar um exemplo de fonte não confiável. O seu próprio arquivo compilado é o ponto de partida seguro. Quando estiver confortável com ele, o próximo passo ainda é código seu ou um exemplo de fonte que você verificou antes. O artigo sobre hashes de arquivo e ética do VirusTotal explica por que um hash sozinho não resolve essa questão.

Tratar o laboratório como teste de segurança. A checagem de string aqui é trivialmente quebrável. Encontrá-la não prova nada sobre software real. Código real de licenciamento e proteção é mais difícil, e analisá-lo precisa ficar dentro do escopo que você tem permissão para trabalhar.

Esquecer a versão do JDK. Se o Ghidra não abrir, confira a versão do Java com as notas da release antes de mexer em qualquer outra coisa.

Como continuar

Depois que o brinquedo funcionar, deixe o exercício mais difícil de forma controlada:

  • Troque "LAB-1234" por uma string maior, recompile e encontre-a de novo nos dois builds.
  • Adicione uma segunda função de verificação que compare a entrada com um número, e descubra onde o Ghidra mostra a conversão.
  • Compile com -O0 -s e compare com -O2 -s. A diferença mostra quanto o otimizador muda a forma do código sem remover a lógica.
  • Leia a documentação do próprio Ghidra sobre o decompilador e a visão de listagem. O site oficial e a pasta de documentação no GitHub cobrem a interface com mais profundidade do que este artigo consegue.

Se quiser entender melhor as flags do compilador, as seções do manual do GCC sobre opções de otimização e de depuração são as fontes primárias. Leia antes de confiar em uma flag num relatório.

Guarde suas notas. Um bom caderno de laboratório tem o hash do código-fonte, os comandos de compilação, as saídas da execução, a versão do Ghidra, a versão do JDK e os nomes que você atribuiu, cada um com a evidência que o sustenta. Esse registro é o que transforma este exercício em uma habilidade que você reaproveita no próximo programa.

Fontes

Fontes

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

Mais em Engenharia reversa