Ghidra + LLM: quanto você pode confiar quando o modelo explica um binário ARM?
Um modelo de linguagem (LLM, Large Language Model) consegue ler o código que o Ghidra produz e explicar o que um binário faz. A conversa fica fluente, os nomes das funções melhoram, o resumo aparece em segundos. A pergunta que importa para quem trabalha com baixo nível é outra: quanto dessa explicação está certa? Um texto que soa seguro pode estar errado, e num binário sem código-fonte você não tem como notar.
Este artigo responde à pergunta do jeito mais simples e menos enganoso que encontrei: usando um binário cujo código-fonte eu tenho. Assim cada afirmação do modelo vira uma linha de uma tabela de "acertou / errou / exagerou", conferida contra o fonte.
Antes de começar, o vocabulário mínimo, para que o texto se sustente sozinho:
- Ghidra é um framework gratuito de engenharia reversa, criado e mantido pela NSA (agência de segurança dos EUA) e publicado como código aberto. Ele abre um executável, desmonta as instruções e, com o decompilador, tenta reconstruir um pseudocódigo em C a partir delas. O decompilador não tem acesso ao código original: ele adivinha tipos, nomes e laços a partir do que as instruções fazem.
- ELF (Executable and Linkable Format) é o formato de executável usado em Linux e na maioria dos sistemas embarcados. Ele guarda o código, os dados, a tabela de símbolos e o endereço de entrada.
- ARM é a família de processadores presente em celulares, consoles portáteis e microcontroladores. Thumb é o conjunto de instruções compacto do ARM: instruções de 16 bits (e, no Thumb-2, mistura de 16 e 32 bits) em vez de 32.
- MCP (Model Context Protocol) é um protocolo aberto que padroniza como um aplicativo de IA (o "cliente") descobre e chama ferramentas expostas por um "servidor". É o que permite que o modelo peça ao Ghidra "decompile esta função" e receba a resposta como dado.
- arm-box é um executor de binários ARM de Linux (como o
qemu-arm) escrito em Java, construído sobre o arm-jitter, um emulador ARM com compilação JIT (Just-In-Time) para bytecode da JVM. Os dois são projetos meus e servem aqui só para executar o binário de teste e provar o que ele faz.
O que você vai fazer até o fim:
- compilar um programa pequeno em C para ARM (Thumb-2), em dois níveis de otimização, e executá-lo no arm-box;
- importar o ELF no Ghidra e aprender a ler o que o decompilador mostra, incluindo seus defeitos;
- ligar o Ghidra a um cliente de IA pelo GhidraMCP e chamar as ferramentas dele;
- conferir as respostas de um modelo contra o código-fonte, com um protocolo que você pode repetir.
Pré-requisitos. JDK 25 (o README do Ghidra pede JDK 25; usei a JetBrains Runtime 25.0.3), Ghidra 12.1.4, Python 3.10 a 3.13 para o bridge do GhidraMCP, o toolchain arm-none-eabi-gcc (usei o devkitARM, GCC 16.1.0) e o armbox.jar para executar o binário. Você não precisa ter lido nenhum outro artigo do blog, nem saber assembly ARM de cor: as poucas instruções que aparecem são explicadas na hora.
O experimento em uma página
Para o resultado valer algo, as regras ficam fixas antes de olhar qualquer resposta:
- Binário conhecido. O programa
cofre.ctem 4 funções pequenas e um comportamento que dá para executar. O gabarito vem do fonte e da execução real. - Duas versões do binário. Uma compilada com
-O0(tradução quase literal do C) e outra com-O2(otimizada). A segunda é a que desafia o decompilador. - Com e sem símbolos. Um binário com a tabela de símbolos entrega os nomes das funções de graça. A versão
stripnão. Isso muda muito o que o modelo "deduz" e o que ele apenas "lê". - Mesmas perguntas, nas mesmas palavras. Oito perguntas, em português, sempre iguais (estão mais abaixo).
- Resposta de LLM não é determinística. Rodar de novo pode dar outro texto. Por isso cada resultado é rotulado como uma execução, com modelo, data e prompt registrados. Não é um benchmark.
- O modelo só enxerga o que o Ghidra mostra (ferramentas MCP somente de leitura). Sem código-fonte, sem web.
O programa de teste: cofre.c
O programa recebe uma chave de 8 caracteres na linha de comando e diz se o "cofre" abre. Ele reúne o que costuma confundir quem lê um binário: um laço simples, um hash com rotação e multiplicação, uma checagem de tamanho e duas chamadas de sistema escritas à mão, já que não há biblioteca C. A função soma_ate é a mesma do artigo Do C ao binário ARM.
Arquivo: cofre.c.
#define SEMENTE 0x1505u
#define ALVO 0x3096b5feu
static const char MSG_OK[] = "acesso liberado\n";
static const char MSG_NEGA[] = "acesso negado\n";
int soma_ate(int n) {
int acc = 0;
for (int i = 1; i <= n; i++) {
acc += i;
}
return acc;
}
unsigned mistura(const char *s, int len) {
unsigned h = SEMENTE;
for (int i = 0; i < len; i++) {
h = ((h << 5) | (h >> 27)) ^ (unsigned char)s[i];
h *= 0x9e3779b1u;
}
return h;
}
int chave_ok(const char *chave) {
int n = 0;
while (chave[n] != '\0') {
n++;
}
if (n != 8) {
return 0;
}
return mistura(chave, n) == ALVO;
}
void c_start(int argc, char **argv) {
if (argc > 1 && chave_ok(argv[1])) {
escreve(MSG_OK, sizeof MSG_OK - 1);
sai(0);
}
escreve(MSG_NEGA, sizeof MSG_NEGA - 1);
sai(1);
}As funções escreve e sai (as duas chamadas de sistema) e o _start ficam em assembly num bloco __asm__ no fim do arquivo, que está completo no download. Elas existem em assembly porque, em Thumb, o gcc reserva o registrador r7 e recusa um asm inline que o use (a mensagem real foi r7 cannot be used in 'asm' here), e r7 é justamente o registrador que o Linux ARM usa para o número da chamada de sistema (man syscall(2)).
Compilar e executar
O script build.sh compila os dois níveis:
export PATH=/c/devkitPro/devkitARM/bin:$PATH
F="-g0 -mcpu=cortex-a8 -mthumb -nostdlib -static -ffreestanding -fno-tree-loop-distribute-patterns -T user.ld"
arm-none-eabi-gcc -O0 $F cofre.c -o cofre-O0.elf
arm-none-eabi-gcc -O2 $F cofre.c -o cofre-O2.elf
arm-none-eabi-strip -s -o cofre-O2-strip.elf cofre-O2.elf-mthumb pede código Thumb-2; -nostdlib -ffreestanding tiram a biblioteca C; -fno-tree-loop-distribute-patterns impede o compilador de trocar o laço de chave_ok por uma chamada a strlen, que não existe aqui. O user.ld é um linker script mínimo que coloca o código em 0x10000.
Executar no arm-box (a saída abaixo é a real, e o código de saída do processo vem do exit do binário):
java -jar armbox.jar --arch=armv7a cofre-O2.elf ARM-2026 # acesso liberado (código de saída 0)
java -jar armbox.jar --arch=armv7a cofre-O2.elf errada # acesso negado (código de saída 1)acesso liberado
acesso negadoO mesmo vale para cofre-O0.elf. Sabemos então, por execução e não por leitura, que a chave planejada é ARM-2026. Guarde essa informação: ela é o gabarito da pergunta 5.
O que o -O2 faz com chave_ok
Abra a desmontagem de chave_ok (arm-none-eabi-objdump -d cofre-O2.elf) e procure o fim do laço do hash:
A comparação final usa r3 (o valor antes da última multiplicação) e a constante 0xc003375e, não o 0x3096b5fe que escrevi no fonte. O motivo é aritmética: como 0x9e3779b1 é ímpar, ele tem inverso multiplicativo módulo 2³², e o compilador transformou h * K == ALVO em h == ALVO * K⁻¹, descartando a última multiplicação. A conta fecha:
0xc003375e * 0x9e3779b1 mod 2^32 = 0x3096b5feEssa é a armadilha deliberada do experimento: o número que aparece no decompilador não é o que está no código-fonte, e quem só olhar o binário precisa perceber isso.
Uma observação sobre Thumb que o ELF mostra de cara: o endereço de entrada gravado no cabeçalho é 0x10017 (readelf -h), enquanto o símbolo _start está em 0x10016. O bit 0 ligado num endereço de código é a convenção ARM para "este código é Thumb" (é o que a instrução BX usa para trocar de estado; ver ARM ARM, na bibliografia). O endereço real da instrução continua par.
Ghidra: importar e ler o decompilador
Pela interface gráfica
O fluxo pela interface, que é o das capturas abaixo:
File > New Project..., projeto Non-Shared, numa pasta sua.File > Import File...e escolhacofre-O2.elf. O Ghidra reconhece o formato (Executable and Linking Format) e propõe a linguagem. Neste binário a linguagem éARM:LE:32:v8: ARM, little-endian, 32 bits. Não procure por "Thumb" nem por "v7" na lista: a mesma linguagem cobre os dois conjuntos de instruções.- Dê duplo clique no arquivo para abrir o CodeBrowser e aceite a análise automática.
- Abra o painel
Window > Decompile. Selecione uma função na lista ou no Listing e o decompilador mostra o C reconstruído ao lado do assembly.
(abre a imagem original em outra aba)
(abre a imagem original em outra aba)Em modo headless (reproduzível por script)
Para obter a saída que está neste artigo sem depender da interface, o Ghidra tem o analyzeHeadless, que importa, analisa e roda scripts sem abrir janela. O script DecompilaTudo.java imprime a linguagem, o valor do registrador TMode em cada função e o C do decompilador:
export JAVA_HOME=/caminho/do/jdk-25 # JDK 25 (o java do PATH pode ser antigo demais)
analyzeHeadless.bat projetos-ghidra proj1 -import cofre-O2.elf -overwrite \
-scriptPath scripts -postScript DecompilaTudo.javaSaída real (trecho; completa em saidas/ghidra-decompila-O2.txt):
Linguagem: ARM:LE:32:v8 | compilador: default
==== escreve @ 00010000 (TMode=1) ====
==== sai @ 00010010 (TMode=1) ====
==== _start @ 00010016 (TMode=1) ====
==== soma_ate @ 00010020 (TMode=1) ====
==== mistura @ 00010038 (TMode=1) ====
==== chave_ok @ 00010064 (TMode=1) ====
==== c_start @ 000100b0 (TMode=1) ====TMode é o registrador que a especificação de processador do Ghidra (ARM.sinc, linguagem ARM:LE:32:v8) usa para dizer "decodifique como Thumb". Valor 1 em todas as funções: o Ghidra tratou o programa inteiro como Thumb, decodificou as instruções de 16 e 32 bits e produziu C sem nenhuma intervenção manual. O suporte a Thumb funcionou neste binário.
Ler o decompilador sem se deixar enganar
O C do decompilador não é o seu C. Veja chave_ok (-O2), como o Ghidra a mostrou:
bool chave_ok(char *param_1)
{
char *pcVar1;
uint uVar2;
byte *pbVar3;
char *pcVar4;
uint uVar5;
pcVar1 = param_1;
if (*param_1 != '\0') {
do {
pcVar4 = pcVar1;
pcVar1 = pcVar4 + 1;
} while (*pcVar1 != '\0');
if (pcVar4 + -7 == param_1) {
pbVar3 = (byte *)(param_1 + -1);
uVar2 = 0x1505;
do {
pbVar3 = pbVar3 + 1;
uVar5 = (uint)*pbVar3 ^ (uVar2 >> 0x1b | uVar2 << 5);
uVar2 = uVar5 * -0x61c8864f;
} while ((byte *)(param_1 + 7) != pbVar3);
return uVar5 == 0xc003375e;
}
}
return false;
}O que ler com desconfiança:
- Nomes inventados.
param_1,uVar2,pcVar4são nomes gerados. O tipo vem do uso (char *porque há leitura de bytes). - O tamanho 8 sumiu. O fonte tem
n != 8. O compilador transformou empcVar4 + -7 == param_1, uma comparação de ponteiros. É a mesma lógica, mas você precisa reconstituí-la. - Constantes mudam de forma.
-0x61c8864fé0x9e3779b1visto como inteiro com sinal de 32 bits. E0xc003375ejá vimos que não é oALVO. - O hash saiu no pé da letra.
uVar2 >> 0x1b | uVar2 << 5é a rotação à esquerda de 5 bits (>> 27ou<< 5).
Com -O0, a mesma função sai bem mais próxima do fonte e com a constante original:
bool chave_ok(int param_1)
{
...
for (local_c = 0; *(char *)(local_c + param_1) != '\0'; local_c = local_c + 1) { }
if (local_c == 8) {
iVar1 = mistura(param_1,8);
bVar2 = iVar1 == 0x3096b5fe;
} ...Três defeitos reais do decompilador neste binário
- Chamadas de sistema opacas.
escrevevirousoftware_interrupt(0)e um retornoundefined8comCONCAT44(param_1,1). O decompilador não sabe quesvc 0no Linux ARM lêr7para escolher a chamada, então o número da chamada (4 =write, 1 =exit) só aparece no assembly. - Lixo depois de
sai(0)emc_start. O Ghidra não sabe quesainão retorna. Ele continua decodificando e interpreta as duas strings, mais uma constante do pool de literais, como instruções, e imprimehalt_baddata()e um monte de atribuições sem sentido. Nada disso é código real. - Funções órfãs sem símbolos. No binário
strip, o Ghidra só encontrou 5 das 7 funções:soma_ateemisturanão têm nenhum chamador (o compilador embutiumisturaemchave_ok), então nada as referencia e a análise automática não as cria. Confirmei comlist_functions.
(abre a imagem original em outra aba)MCP: ligando o Ghidra ao modelo
O MCP separa quem usa as ferramentas de quem as oferece. Segundo a especificação (revisão 2026-07-28), há hosts (o aplicativo de IA), clientes (os conectores dentro dele) e servidores (quem expõe contexto e ferramentas). Uma ferramenta é uma função que o modelo pode chamar: o cliente pergunta tools/list e recebe nomes e esquemas; depois envia tools/call com os argumentos. A mesma especificação diz que deve haver sempre um humano no circuito com poder de negar chamadas, e que as ferramentas representam execução arbitrária de código.
O GhidraMCP (Laurie Wired) tem duas peças: um plugin que roda dentro do Ghidra e abre um servidor HTTP local, e um bridge em Python (ghidra-mcp) que fala MCP com o cliente e HTTP com o plugin. A versão 2.0 (lançada em 10/10/2026) casa com o Ghidra 12.1.4. Há também um modo headless, em que o próprio ghidra-mcp importa e analisa o binário via PyGhidra, sem abrir a interface. Foi o que usei, por ser reproduzível.
Importante para a sua instalação: o README propõe adicionar o servidor ao seu cliente (por exemplo,
claude mcp add ghidra -- uvx --from git+https://github.com/LaurieWired/GhidraMCP ghidra-mcp --ghidra-server http://127.0.0.1:8080/). Isso altera a configuração do cliente. Faça só se quiser.
Subir o servidor e listar as ferramentas
Instalação (num ambiente virtual do Python, sem mexer em nenhum cliente):
python -m venv venv-ghidramcp
venv-ghidramcp/Scripts/python -m pip install "GhidraMCP-release-2-0/ghidra_mcp-2.0.0-py3-none-any.whl[headless]"
export GHIDRA_INSTALL_DIR=/caminho/do/ghidra_12.1.4_PUBLIC JAVA_HOME=/caminho/do/jdk-25
ghidra-mcp --headless --binary cofre-O2.elf --project projeto-mcp \
--transport streamable-http --mcp-host 127.0.0.1 --mcp-port 18081(No Windows, coloquei JAVA_HOME\bin no PATH: sem isso a JVM não achou uma biblioteca do JDK e o PyGhidra falhou ao iniciar.) Um cliente MCP mínimo em Python, lista_ferramentas.py, conecta e pergunta o que o servidor oferece. Saída real:
servidor: ghidra-mcp 1.30.0 | protocolo: 2025-11-25
28 ferramentas: get_program_info, list_functions, list_methods, search_functions_by_name, list_classes, list_namespaces, list_segments, list_imports, list_exports, list_data_items, list_strings, get_function_by_address, decompile_function, decompile_function_by_address, disassemble_function, get_xrefs_to, get_xrefs_from, get_function_xrefs, get_function_calls, read_memory, rename_function_by_address, rename_function, rename_data, rename_variable, set_function_prototype, set_local_variable_type, set_decompiler_comment, set_disassembly_commentDois detalhes: o 1.30.0 coincide com a versão do pacote mcp (SDK Python) instalado, não com a do GhidraMCP (que é 2.0.0), e o protocolo negociado foi 2025-11-25, uma revisão mais antiga que a mais recente da especificação.
As ferramentas se dividem em leitura (listar funções e strings, decompilar, desmontar, referências cruzadas, ler memória) e escrita (renomear funções e variáveis, mudar tipos, comentar). A divisão importa para a segurança: o experimento abaixo só liberou as de leitura, por meio de uma lista permitida em mcp_call.py. Chamando uma ferramenta de leitura:
>>> decompile_function {'name': 'chave_ok'}
{
"function": { "name": "chave_ok", "address": "00010064" },
"code": "\r\nbool chave_ok(char *param_1)\r\n\r\n{ ... }",
"truncated": false
}Ou seja, o que o modelo recebe é exatamente o texto do decompilador que você viu acima, com os mesmos defeitos. O modelo não tem acesso a nada que o Ghidra não mostre.
Avaliando o modelo: protocolo e resultados
As perguntas (fixas; arquivo avaliacao/perguntas.md):
- O que o programa faz, do início ao fim? Descreva o fluxo geral e o papel de cada função.
- O que a função em
0x10064faz? Qual a condição exata para ela retornar verdadeiro? - O que a função em
0x10038calcula? Que tipo de algoritmo é esse? De onde vem a constante-0x61c8864f? - A constante
0xc003375edo decompilado é o valor que o programador escreveu no código-fonte? Justifique. - Qual argumento de linha de comando (8 caracteres) faz o programa imprimir a mensagem de sucesso? Se não der para determinar, diga.
- O que fazem as chamadas
software_interrupt(0)? Que chamadas de sistema são e com quais argumentos? - Sugira nomes melhores para todas as funções.
- Há algum bug ou vulnerabilidade? Onde?
O gabarito (de cofre.c e da execução; avaliacao/gabarito.md): chave_ok exige tamanho 8 e mistura == 0x3096b5fe; 0xc003375e é ALVO multiplicado pelo inverso da constante, não o valor do fonte; a chave planejada é ARM-2026, mas o hash é de 32 bits e a chave tem 64, então existem colisões; svc 0 com r7 = 4 é write(1, ...) e com r7 = 1 é exit(...); não há bug de memória, só fraqueza de projeto (hash curto, sem sal).
Como foi rodado. Uma sessão de um modelo (claude-sonnet-5-5, informado por ele mesmo), em 21/10/2026, executada como subagente do Claude Code, usando o cliente mcp_call.py via linha de comando (somente ferramentas de leitura). Não é o mesmo que ligar o servidor ao Claude Desktop ou a outro cliente interativo; o resultado de uma sessão assim pode ser diferente. O texto integral da resposta está em avaliacao/execucao-1-com-simbolos.md.
Execução 1: cofre-O2.elf (com símbolos)
| # | Pergunta | Resultado | Observação |
|---|---|---|---|
| 1 | Fluxo geral | Acertou | Descreveu _start, c_start, as duas mensagens (14 e 16 bytes), os wrappers de write e exit; notou que mistura foi embutida no -O2 e soma_ate é código morto; reconheceu o "lixo" depois de sai(0) como dados decodificados como código. |
| 2 | chave_ok | Acertou, com uma imprecisão | Tamanho 8 e hash conferidos; notou que o teste usa o valor antes da última multiplicação. Listou "o ponteiro não é nulo" como condição, mas o código não testa isso: é pré-condição, e um ponteiro nulo travaria o programa. |
| 3 | mistura e a constante | Acertou, com um exagero | Algoritmo e semente (0x1505 = 5381, como no djb2) corretos; -0x61c8864f = 0x9e3779b1 corretos. Mas afirmou que é "o inteiro de 32 bits mais próximo de 2³²/φ": errado. A constante clássica é 0x9e3779b9; a deste programa é uma variante com o último dígito alterado (conferi: round(2^32/φ) é 0x9e3779b9). |
| 4 | 0xc003375e é o do fonte? | Acertou | Inferiu a transformação do compilador, calculou 0xc003375e * 0x9e3779b1 mod 2^32 = 0x3096b5fe por script e reconheceu que não podia provar que o fonte tinha esse valor. Bateu com ALVO. |
| 5 | Qual a chave? | Acertou ao dizer que não dá | Disse que o hash de 32 bits tem colisões e deu três candidatos (v738aaak, 34q8ad55, ejraalis). Conferi no arm-box: os três liberam o cofre. Não achou ARM-2026, e não tinha como. |
| 6 | svc 0 | Acertou | r7=4 = write(1, buf, n), r7=1 = exit(status); apontou que o CONCAT44 é ruído do decompilador. |
| 7 | Nomes | Razoável | sys_write_stdout, sys_exit, verifica_senha, hash_rotxor_mul, soma_1_ate_n. Equivalentes aos originais. |
| 8 | Bugs | Acertou | Nenhum erro de memória; fraqueza de projeto (hash curto, sem sal); soma_ate estoura int para n >= 65536, mas ninguém a chama. Nenhum falso alarme. |
Conta honesta: 8 respostas úteis, 2 afirmações erradas ou imprecisas (a constante "mais próxima da razão áurea" e a "condição de ponteiro não nulo") e nenhuma invenção grosseira. As duas falhas são exatamente do tipo perigoso: frases plausíveis, ditas com segurança, que um leitor sem o fonte engoliria.
Execução 2: cofre-O2-strip.elf (sem símbolos) — incompleta
Rodei a segunda execução no binário sem símbolos, com as mesmas perguntas (a 3 ficou "o código a partir de 0x10038", porque o Ghidra não cria função ali). A sessão foi interrompida pela API antes do relatório final, com o erro Sonnet 5.5's safeguards flagged this session ... [cyber], e eu só recebi um relatório truncado no meio da resposta 4. Não refiz nem contornei. O que chegou está em avaliacao/execucao-2-sem-simbolos-INCOMPLETA.md. Das respostas 1 a 3 (e do começo da 4), o que dá para dizer sem inventar:
- o modelo notou que o Ghidra deixou duas funções reais de fora (
0x10020e0x10038), leu os bytes comread_memorye decodificou o Thumb-2 à mão, chegando à descrição correta desoma_ateemistura; - a descrição do fluxo (1) e de
chave_ok(2) estão corretas, incluindo oxantes da última multiplicação; - repetiu o mesmo exagero sobre a constante (chamou
0x9e3779b1de "2³²/φ" e "a mesma do TEA"; a constante clássicaround(2³²/φ)é0x9e3779b9, e a deste programa difere no último dígito).
As respostas 5 a 8 desta execução não existem: a sessão foi interrompida pelo filtro de segurança da API antes de terminar, e eu não a refiz para contornar o filtro. Se quiser completar o experimento, a seção "Repita você mesmo" mostra como.
O que esses números não provam
- O programa é minúsculo (4 funções) e o modelo pôde usar Python para checar contas. Binários grandes, ofuscados ou sem nomes mudam o jogo: o estudo da Universidade do Norte da Geórgia (CCERP 2025, com o Claude Sonnet 4 e o GhidraMCP em amostras de pseudo-ransomware e nos desafios ESCALATE) relata ganho de velocidade, mas também anotações inexatas ou especulativas em dois dos desafios, que exigiram correção humana. Aqui o modelo foi bem; lá, nem sempre.
- Uma execução. Outro dia, outro prompt, outro modelo: outro resultado.
- Eu confiro com o fonte; você, no mundo real, não. O valor do método está justamente em treinar o olhar onde a resposta é conhecida.
- Rastro de decisão importa mais que acerto. Na pergunta 4 o modelo acertou o número e disse o que não conseguia provar. Esse é o comportamento que você quer: afirmações verificáveis (uma multiplicação que você refaz em um segundo) e limites declarados.
Regras práticas que saíram daqui
- Peça a conta, não a conclusão. "Explique o que esse binário faz" gera prosa. "Mostre como
0xc003375ese relaciona com o hash" gera algo que você refaz. - Verifique o que dá para executar. Os três "palpites" de chave foram aceitos pelo programa de verdade. Sem rodar, seriam só mais três frases.
- Desconfie das frases com "clássico", "o mais próximo", "a mesma de": são onde o modelo mistura memória com o que está na tela.
- Binário sem símbolos e funções órfãs. O decompilador deixou 2 de 7 funções de fora; o modelo só as recuperou lendo os bytes. Confira a lista de funções antes de acreditar que você viu o programa inteiro.
- Ferramentas de escrita são decisão sua. Renomear no banco do Ghidra é fácil de desfazer, mas deixe o modelo sugerir nomes e você aplicar.
Repita você mesmo
- Baixe o
cofre.c, rodebuild.she execute os.elfno arm-box. - Instale o Ghidra 12.1.4, o GhidraMCP 2.0 (o
.zipda extensão e o.whldo bridge) e conecte ao seu cliente de IA. Você aprova cada chamada de ferramenta; mantenha só as de leitura enquanto avalia. - Faça as 8 perguntas, registre modelo, data, prompt e cliente, e preencha a tabela contra o gabarito.
- Repita com
cofre-O0.elfecofre-O2-strip.elfe compare.
Se você tem outra ferramenta, o OGhidra (projeto do LLNL) liga o Ghidra a modelos locais via Ollama ou a modelos de nuvem, num laço em que o modelo coleta mais informação antes de responder. O README dele não faz afirmações sobre acurácia, então o seu protocolo de verificação continua necessário.
Glossário
| Termo | O que é |
|---|---|
| Ghidra | Framework de engenharia reversa da NSA, código aberto: desmonta, decompila, analisa e é extensível por scripts |
| Decompilador | Parte do Ghidra que reconstrói um pseudocódigo em C a partir das instruções de máquina; não recupera nomes nem comentários |
| Desmontagem (disassembly) | Tradução dos bytes de código em instruções legíveis (ldrb, cmp, bne) |
| ELF | Executable and Linkable Format: formato de executável com código, dados, símbolos e ponto de entrada |
Símbolos / strip | Tabela de nomes (funções, variáveis) dentro do ELF; strip a remove |
| Thumb / Thumb-2 | Conjunto de instruções compacto do ARM (16 bits; Thumb-2 mistura 16 e 32 bits) |
TMode | Registrador do Ghidra que indica "decodificar como Thumb" (1) ou como ARM (0) |
| Bit 0 do endereço | Convenção ARM: endereço de código com bit 0 ligado (ex. 0x10017) significa Thumb; o código real fica no endereço par |
svc 0 | Chamada de sistema (supervisor call); no Linux ARM EABI o número da chamada vai em r7 |
r7, r0–r2 | Registradores: r7 carrega o número da chamada de sistema; r0–r3 levam os primeiros argumentos de função e r0 o resultado |
| LLM | Large Language Model: modelo de linguagem usado como assistente |
| MCP | Model Context Protocol: protocolo aberto para um aplicativo de IA descobrir e chamar ferramentas de um servidor |
| Cliente / servidor MCP | Quem usa as ferramentas (o aplicativo de IA) e quem as oferece (aqui, o GhidraMCP) |
| GhidraMCP | Servidor MCP para o Ghidra: um plugin (HTTP local) e um bridge Python (ghidra-mcp) |
| Headless | Modo sem interface gráfica (analyzeHeadless, ghidra-mcp --headless) |
| Hash | Função que resume dados num número de tamanho fixo; aqui, 32 bits |
| Colisão | Duas entradas diferentes com o mesmo hash |
| Inverso multiplicativo (mod 2³²) | Número que, multiplicado pelo original módulo 2³², dá 1; existe para todo ímpar |
| Alucinação | Afirmação fluente e plausível do modelo que não corresponde aos fatos |
| arm-box / arm-jitter | Executor de binários ARM Linux e emulador ARM com JIT em Java (projetos meus) |
Comandos
| Comando | O que faz |
|---|---|
arm-none-eabi-gcc -O2 -mcpu=cortex-a8 -mthumb -nostdlib -static -ffreestanding -fno-tree-loop-distribute-patterns -T user.ld cofre.c -o cofre-O2.elf | Compila cofre.c para Thumb-2, sem libc, no endereço do user.ld (trocar -O2 por -O0 para a versão literal) |
arm-none-eabi-strip -s -o cofre-O2-strip.elf cofre-O2.elf | Remove os símbolos |
arm-none-eabi-objdump -d cofre-O2.elf | Desmonta o código |
arm-none-eabi-readelf -h cofre-O2.elf | Mostra o cabeçalho do ELF (inclui o endereço de entrada) |
java -jar armbox.jar --arch=armv7a cofre-O2.elf ARM-2026 | Executa o binário no arm-box; o código de saída é o do exit do programa |
analyzeHeadless <pasta> <projeto> -import x.elf -overwrite -scriptPath scripts -postScript DecompilaTudo.java | Importa, analisa e roda o script do Ghidra sem interface (no Linux/macOS, analyzeHeadless sem .bat) |
python -m venv venv-ghidramcp | Cria um ambiente virtual do Python para o bridge |
pip install "ghidra_mcp-2.0.0-py3-none-any.whl[headless]" | Instala o bridge com o suporte a PyGhidra |
ghidra-mcp --headless --binary x.elf --project p --transport streamable-http --mcp-port 18081 | Sobe o servidor MCP sobre um binário, sem interface (use outra porta se a 8081 estiver ocupada) |
python mcp_call.py decompile_function '{"name":"chave_ok"}' | Chama uma ferramenta de leitura do servidor MCP (cliente mínimo deste artigo) |
sha256sum arquivo.zip | Confere o checksum de um download (certutil -hashfile arquivo.zip SHA256 no Windows) |
Conclusão
Importar um ELF ARM/Thumb no Ghidra é rápido: a linguagem ARM:LE:32:v8 cobre Thumb, o registrador TMode marca o estado, e o decompilador entrega C legível. Mas esse C tem defeitos que precisam ser conhecidos: constantes que o compilador transformou, chamadas de sistema opacas, lixo depois de funções que não retornam e funções sem chamadores que simplesmente não aparecem.
Ligar um modelo ao Ghidra pelo MCP não muda o que o Ghidra enxerga; muda quem lê. No meu teste (uma execução, programa pequeno, com um modelo que podia calcular em Python), o modelo explicou o fluxo, o hash e as chamadas de sistema corretamente, inferiu a transformação do compilador e admitiu o que não conseguia provar. Também errou, com segurança, em dois pontos, e um deles (uma "constante clássica") é o tipo de frase que passa despercebida. A regra de ouro que sobra é de método: trate a resposta do modelo como uma hipótese e transforme cada afirmação numa verificação que você pode refazer: uma conta, uma execução, uma leitura do assembly.
Isso vale além de ARM: qualquer análise de binário assistida por IA, em firmware, jogos ou malware, fica mais confiável quando você mantém um caso de teste conhecido para calibrar a ferramenta antes de usá-la no desconhecido.
Para ir além:
- Do C ao binário ARM: gcc + objdump: a função
soma_ate,-O0contra-O2e as seções do objeto. - Decodificando Thumb à mão: como ler as instruções de 16 bits que o Ghidra desmonta por você.
- Thumb e Thumb-2: o conjunto de instruções e a troca de estado pelo bit 0.
- Hello, armbox: como executar binários ARM no arm-box.
Referências
- Agência de Segurança Nacional dos EUA (NSA) — Ghidra, README (decompilador, modos interativo e automatizado, requisito de JDK 25) e release 12.1.4 (21/09/2026). SHA-256 do zip conferido:
ddac49f9…d2d4db. - Código-fonte do Ghidra 12.1.4 —
Ghidra/Processors/ARM/data/languages/ARM.sinc(definição do registradorTMode) eARM.ldefs(linguagemARM:LE:32:v8), lidos na distribuição instalada. - LaurieWired — GhidraMCP, README e release 2.0 (10/10/2026): instalação, modo headless, exemplos de cliente. SHA-256 do zip conferido:
ebf904fd…2f5b72. Código do bridge (server.py,headless.py) lido no pacoteghidra_mcp-2.0.0. - Model Context Protocol — Specification (revisão 2026-07-28) e Tools: papéis de host, cliente e servidor;
tools/listetools/call; recomendação de humano no circuito. - Watson, J. C.; Payne, B. R.; McWilliams, D.; Yan, M. — Examining the Capabilities of GhidraMCP and Claude LLM for Reverse Engineering Malware. Conference on Cybersecurity Education, Research and Practice (CCERP) 2025, KSU Proceedings on Cybersecurity Education, Research and Practice. Lido o resumo da página.
- Lawrence Livermore National Laboratory — OGhidra, README (modelos locais via Ollama e de nuvem; laço de revisão).
- Arm Ltd. — ARM Architecture Reference Manual, ARMv7-A and ARMv7-R edition (ARM DDI 0406C). Troca de estado ARM/Thumb pelo bit 0 do endereço em
BX(a página do manual não pôde ser lida pela ferramenta usada, por isso não cito seção). - Arm Ltd. — Procedure Call Standard for the Arm Architecture (AAPCS32), edição 2025Q4:
r0–r3carregam os primeiros argumentos er0o resultado (seção 6.1.1). - Linux man-pages — syscall(2): convenção ARM EABI (
swi 0x0, número da chamada emr7, argumentos emr0–r6, retorno emr0). - Código-fonte deste artigo —
cofre.c,user.ld,build.sh,scripts/DecompilaTudo.java,mcp/eavaliacao/em/curso-arm/exemplos/ghidra-llm/.
Comentários
Os comentários usam o Giscus, que guarda as discussões no GitHub. Ao carregar, seu navegador se conecta ao giscus.app e ao GitHub, que recebem o seu endereço IP. Saiba mais na política de privacidade.