← Voltar aos artigos

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:

  1. compilar um programa pequeno em C para ARM (Thumb-2), em dois níveis de otimização, e executá-lo no arm-box;
  2. importar o ELF no Ghidra e aprender a ler o que o decompilador mostra, incluindo seus defeitos;
  3. ligar o Ghidra a um cliente de IA pelo GhidraMCP e chamar as ferramentas dele;
  4. 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.c tem 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 strip nã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 negado

O 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:

1008a:f812 3f01ldrb.wr3, [r2, #1]!
1008e:ea83 63f1eor.wr3, r3, r1, ror #27
10092:4290cmpr0, r2
10094:fb0c f103mul.wr1, ip, r3
10098:d1f7bne.n1008a <chave_ok+0x26>
1009a:f243 725emovwr2, #14174@ 0x375e
1009e:f2cc 0203movtr2, #49155@ 0xc003
100a2:1a98subsr0, r3, r2

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  =  0x3096b5fe

Essa é 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:

  1. File > New Project..., projeto Non-Shared, numa pasta sua.
  2. File > Import File... e escolha cofre-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.
  3. Dê duplo clique no arquivo para abrir o CodeBrowser e aceite a análise automática.
  4. 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.
Janela Import do Ghidra com o formato Executable and Linking Format (ELF) e a linguagem ARM:LE:32:v8:default já preenchidos para o arquivo cofre-O2.elf(abre a imagem original em outra aba)
O Ghidra detecta ARM de 32 bits little-endian. Thumb não aparece como linguagem separada.
Dois recortes do CodeBrowser do Ghidra para a função chave_ok de cofre-O2.elf. Em cima, o Listing com o cabeçalho da função (TMode = 0x1) e as primeiras instruções Thumb: ldrb, cbz, mov, ldrb.w e cmp. Embaixo, o painel Decompile com o C reconstruído: um laço que mede a chave, outro que calcula o hash com XOR, rotação e multiplicação, e o retorno uVar5 == 0xc003375e(abre a imagem original em outra aba)
Listing e decompilador para chave_ok em cofre-O2.elf. Repare na constante 0xc003375e na linha 25, que não aparece no código-fonte.

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.java

Saí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, pcVar4 sã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 em pcVar4 + -7 == param_1, uma comparação de ponteiros. É a mesma lógica, mas você precisa reconstituí-la.
  • Constantes mudam de forma.-0x61c8864f é 0x9e3779b1 visto como inteiro com sinal de 32 bits. E 0xc003375e já vimos que não é o ALVO.
  • O hash saiu no pé da letra.uVar2 >> 0x1b | uVar2 << 5 é a rotação à esquerda de 5 bits (>> 27 ou << 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

  1. Chamadas de sistema opacas.escreve virou software_interrupt(0) e um retorno undefined8 com CONCAT44(param_1,1). O decompilador não sabe que svc 0 no Linux ARM lê r7 para escolher a chamada, então o número da chamada (4 = write, 1 = exit) só aparece no assembly.
  2. Lixo depois de sai(0) em c_start. O Ghidra não sabe que sai não retorna. Ele continua decodificando e interpreta as duas strings, mais uma constante do pool de literais, como instruções, e imprime halt_baddata() e um monte de atribuições sem sentido. Nada disso é código real.
  3. Funções órfãs sem símbolos. No binário strip, o Ghidra só encontrou 5 das 7 funções: soma_ate e mistura não têm nenhum chamador (o compilador embutiu mistura em chave_ok), então nada as referencia e a análise automática não as cria. Confirmei com list_functions.
Dois painéis Symbol Tree do Ghidra lado a lado. À esquerda, cofre-O2.elf com símbolos lista sete funções com nome: _start, c_start, chave_ok, escreve, mistura, sai e soma_ate. À direita, cofre-O2-strip.elf sem símbolos lista só cinco: entry, FUN_00010000, FUN_00010010, FUN_00010064 e FUN_000100b0(abre a imagem original em outra aba)
O mesmo programa com e sem símbolos. Sem eles, o Ghidra só tem endereços, e soma_ate e mistura nem aparecem na lista.

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_comment

Dois 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):

  1. O que o programa faz, do início ao fim? Descreva o fluxo geral e o papel de cada função.
  2. O que a função em 0x10064 faz? Qual a condição exata para ela retornar verdadeiro?
  3. O que a função em 0x10038 calcula? Que tipo de algoritmo é esse? De onde vem a constante -0x61c8864f?
  4. A constante 0xc003375e do decompilado é o valor que o programador escreveu no código-fonte? Justifique.
  5. Qual argumento de linha de comando (8 caracteres) faz o programa imprimir a mensagem de sucesso? Se não der para determinar, diga.
  6. O que fazem as chamadas software_interrupt(0)? Que chamadas de sistema são e com quais argumentos?
  7. Sugira nomes melhores para todas as funções.
  8. 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)

#PerguntaResultadoObservação
1Fluxo geralAcertouDescreveu _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.
2chave_okAcertou, com uma imprecisãoTamanho 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.
3mistura e a constanteAcertou, com um exageroAlgoritmo 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).
40xc003375e é o do fonte?AcertouInferiu 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.
5Qual 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.
6svc 0Acertour7=4 = write(1, buf, n), r7=1 = exit(status); apontou que o CONCAT44 é ruído do decompilador.
7NomesRazoávelsys_write_stdout, sys_exit, verifica_senha, hash_rotxor_mul, soma_1_ate_n. Equivalentes aos originais.
8BugsAcertouNenhum 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 (0x10020 e 0x10038), leu os bytes com read_memory e decodificou o Thumb-2 à mão, chegando à descrição correta de soma_ate e mistura;
  • a descrição do fluxo (1) e de chave_ok (2) estão corretas, incluindo o x antes da última multiplicação;
  • repetiu o mesmo exagero sobre a constante (chamou 0x9e3779b1 de "2³²/φ" e "a mesma do TEA"; a constante clássica round(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

  1. Peça a conta, não a conclusão. "Explique o que esse binário faz" gera prosa. "Mostre como 0xc003375e se relaciona com o hash" gera algo que você refaz.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Baixe o cofre.c, rode build.sh e execute os .elf no arm-box.
  2. Instale o Ghidra 12.1.4, o GhidraMCP 2.0 (o .zip da extensão e o .whl do bridge) e conecte ao seu cliente de IA. Você aprova cada chamada de ferramenta; mantenha só as de leitura enquanto avalia.
  3. Faça as 8 perguntas, registre modelo, data, prompt e cliente, e preencha a tabela contra o gabarito.
  4. Repita com cofre-O0.elf e cofre-O2-strip.elf e 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

TermoO que é
GhidraFramework de engenharia reversa da NSA, código aberto: desmonta, decompila, analisa e é extensível por scripts
DecompiladorParte 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)
ELFExecutable and Linkable Format: formato de executável com código, dados, símbolos e ponto de entrada
Símbolos / stripTabela de nomes (funções, variáveis) dentro do ELF; strip a remove
Thumb / Thumb-2Conjunto de instruções compacto do ARM (16 bits; Thumb-2 mistura 16 e 32 bits)
TModeRegistrador do Ghidra que indica "decodificar como Thumb" (1) ou como ARM (0)
Bit 0 do endereçoConvençã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 0Chamada de sistema (supervisor call); no Linux ARM EABI o número da chamada vai em r7
r7, r0–r2Registradores: r7 carrega o número da chamada de sistema; r0–r3 levam os primeiros argumentos de função e r0 o resultado
LLMLarge Language Model: modelo de linguagem usado como assistente
MCPModel Context Protocol: protocolo aberto para um aplicativo de IA descobrir e chamar ferramentas de um servidor
Cliente / servidor MCPQuem usa as ferramentas (o aplicativo de IA) e quem as oferece (aqui, o GhidraMCP)
GhidraMCPServidor MCP para o Ghidra: um plugin (HTTP local) e um bridge Python (ghidra-mcp)
HeadlessModo sem interface gráfica (analyzeHeadless, ghidra-mcp --headless)
HashFunção que resume dados num número de tamanho fixo; aqui, 32 bits
ColisãoDuas 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çãoAfirmação fluente e plausível do modelo que não corresponde aos fatos
arm-box / arm-jitterExecutor de binários ARM Linux e emulador ARM com JIT em Java (projetos meus)

Comandos

ComandoO 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.elfCompila 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.elfRemove os símbolos
arm-none-eabi-objdump -d cofre-O2.elfDesmonta o código
arm-none-eabi-readelf -h cofre-O2.elfMostra o cabeçalho do ELF (inclui o endereço de entrada)
java -jar armbox.jar --arch=armv7a cofre-O2.elf ARM-2026Executa 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.javaImporta, analisa e roda o script do Ghidra sem interface (no Linux/macOS, analyzeHeadless sem .bat)
python -m venv venv-ghidramcpCria 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 18081Sobe 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.zipConfere 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:

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 registrador TMode) e ARM.ldefs (linguagem ARM: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 pacote ghidra_mcp-2.0.0.
  • Model Context Protocol — Specification (revisão 2026-07-28) e Tools: papéis de host, cliente e servidor; tools/list e tools/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–r3 carregam os primeiros argumentos e r0 o resultado (seção 6.1.1).
  • Linux man-pages — syscall(2): convenção ARM EABI (swi 0x0, número da chamada em r7, argumentos em r0–r6, retorno em r0).
  • Código-fonte deste artigo — cofre.c, user.ld, build.sh, scripts/DecompilaTudo.java, mcp/ e avaliacao/ 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.