
Análise binária paralela com IDA Pro, nomeação de funções alimentada por IA, grafo de conhecimento Neo4j e motor de emulação/hooking/fuzzing phantomrt para engenharia reversa automatizada em formatos PE, ELF e NSO.
Fantasma através dos binários.
Um assistente de engenharia reversa local com inteligência artificial: análise paralela do IDA Pro, nomeação de funções por IA, um terminal que não é ruim, um grafo de conhecimento Neo4j de tudo que já descobriu e um servidor MCP para que Claude possa pesquisar e encadear diretamente nesse grafo.
E agora — com phantomrt — não apenas lê as paredes. Ele as atravessa: emula, faz hook e fuzza as funções que nomeou, e escreve de volta no grafo o que realmente acontece.
✓ 00 ✓ 01 ✓ 02 ✓ 03 ▸ 04 · 05 · 06 · 07 ✓ 08 ✓ 09 ✓ 10 ✓ 11 ✓ 12 ✓ 13 ▸ 14 · 15
14/16 shards │ 141,203 functions found ████████████████████████████░░░░ 89% ~4s remaining
## O que é
A análise automática do IDA Pro é single-threaded. Em uma DLL il2cpp de 34 MB, são *minutos*. spectrIDA divide o binário em N fragmentos, executa-os em paralelo via idalib, mescla em um único `.i64` e, em seguida, permite que um modelo 8B ajustado **nomeie cada função** — tudo a partir de uma única interface de terminal com tema cyberpunk e exatamente a quantidade certa de sarcasmo.
Esse é o Capítulo 1, e ele se sustenta por si só: pura velocidade, sem necessidade de IA se você não quiser.
O Capítulo 2 transforma a saída em algo que sobrevive à sessão — um grafo Neo4j no qual um cliente MCP (Claude, [pi](https://pi.dev), qualquer um que fale MCP) pode realmente viver, em vez de você copiar e colar a saída do descompilador em uma janela de chat, uma função de cada vez:```
Binary ─▶ Parallel IDA Analysis ─▶ Demangle ─▶ AI Naming ─▶ Neo4j Graph ─▶ MCP Server ─▶ Claude
(N idalib shards) (free, real) (stripped (persists, (search/chain/
leftovers forever, rename, live)
only) across sessions)
Capítulo 3 (phantomrt) adiciona a metade que ferramentas estáticas nunca têm: ele executa o código. Emula uma função sem SO (funciona em binários que você não consegue nem executar, como um .nso do Switch), ou hooks o processo ativo, ou faz fuzzing — e carimba o veredito (crashes / needs live state / clean) no mesmo nó do grafo que o nome.
A descrição completa, incluindo o que ainda está na lista de pendências, está nos Capítulo 2 e Capítulo 3.
Não é o Ghidra. Ele faz uma coisa irritante (análise lenta + nomeação) rapidamente, e é genuinamente divertido de usar. 199 downloads falam por si mesmos.
Sem nuvem. Sem telemetria. Executa totalmente na sua máquina.
| task | time |
|---|---|
| Among Us DLL — single-threaded IDA | ~4 hours |
| Among Us DLL — spectrIDA (16 workers) | 67 seconds |
| 153,649 function binary — full naming pass | overnight |
| Binary overview (what does this thing do?) | ~30 seconds |
Hardware usado nas medições: AMD Ryzen 7 5800X3D (8C/16T), 32 GB RAM, RTX 4070 12 GB. Hardware diferente altera os números de análise paralela (mais núcleos, mais shards, mais rápido); os números de nomeação são majoritariamente limitados pela GPU. Os números de 4 horas/67 segundos do Among Us são anteriores ao Capítulo 2 e não são re-verificados de forma independente em cada versão — execute spectrida analyze novamente se quiser um número para sua própria máquina e binário, os resultados variam com a densidade de shards e o tamanho do binário.
Números realmente re-verificados durante o desenvolvimento do Capítulo 2, mesmo hardware:
Essa linha do NSO é o equivalente real da antiga alegação de "4 horas → 67 segundos" do Among Us, medida novamente nesta versão em um binário do Switch com 74.790 funções, sem envolvimento de IA na nomeação (apenas demangling — populate=False). A fase paralela (16 núcleos, ~55s) faz a descoberta inicial fragmentada; a fase de mesclagem (~143s) é single-threaded por design — um banco de dados IDA, um escritor — então se você olhar o Gerenciador de Tarefas durante essa parte e ver 15 núcleos dormindo, isso não é um relatório de bug, é física.
Conseguir um número honesto aqui foi sua própria pequena história de terror. A primeira versão do suporte a NSO rodou limpo, saiu com código 0, e orgulhosamente retornou 727 funções para um binário que tem aproximadamente 75.000 delas — não uma falha, apenas espetacularmente, confiantemente errado, o que é de alguma forma pior. Acontece que o IDA não tem um carregador nativo para NSO, então o arquivo foi carregado silenciosamente como x86 simples ("metapc") mesmo que o Switch seja ARM64 desde o lançamento. Cada shard executou um scanner de prólogo x86 contra instruções puramente AArch64 e chamou de "função" qualquer coisa que acidentalmente correspondesse. Corrigir a arquitetura não resolveu nada por si só, porque o binário também ainda estava comprimido em LZ4 na memória — então metade do que foi escaneado era, generosamente, ruído. E mesmo depois de devidamente descomprimido e marcado como AArch64, cada shard estava apenas caçando alvos de chamada dentro de sua própria fatia minúscula do binário, perdendo toda chamada que cruzava o limite do shard — o que em um binário deste tamanho é a maioria delas. Três bugs, um número, e nenhum deles teve a decência de lançar uma exceção. (Também tentamos reduzir a fase de mesclagem pulando a análise de stack frame do IDA — conseguimos uma bela aceleração e um banco de dados onde o Hex-Rays educadamente se recusou a descompilar metade dele. Isso foi revertido rapidamente. Mantivemos a omissão muito menor e muito mais segura da assinatura FLIRT, que vale uns ~3% dignos de um encolher de ombros e não quebrou nada, o que neste ponto parecia um traço de personalidade que valia a pena manter.)
Sobre a precisão da nomeação: não é verdade absoluta no nível do Ghidra, é um modelo de 8B adivinhando a partir de pseudocódigo. Helpers/getters genéricos tendem a acertar bem; lógica profundamente específica de jogos é mais um cara ou coroa. Renomeie qualquer coisa que ele errar — é por isso que rename_function persiste diretamente de volta no grafo.
.i64. Workers configuráveis via flag, config ou variável de ambiente..so/Linux) vêm embutidos; adicionar um novo é um único arquivo, sem alterações no núcleo. spectrida formats lista o que está registrado. Veja Adicionando um novo formato de binário.N. Observe-o pensar. O nome aparece.B para nomear toda função sub_* na lista. Afaste-se. Volte.O ou execute spectrida overview file.i64. O modelo lê 120 nomes de função amostrados e informa o que o binário faz, quais são seus subsistemas e qualquer coisa relevante para segurança. Identificou corretamente um runtime IL2CPP de 153k funções em 30 segundos.C mostra chamadores e chamados. O modelo usa isso como contexto ao nomear — uma função chamada por é nomeada melhor do que uma isoladamente.pip install spectrida
Requisitos: **IDA Pro 9.x** com idalib · **Python 3.10+** · **Ollama**```bash
# install Ollama (Windows)
winget install Ollama.Ollama
# pull the model (8.7 GB — go get coffee)
ollama pull hf.co/gdfhhjk/spectrida-re-gguf:latest
# first run — detects your IDA install and sets everything up
spectrida onboard
# or just try the demo right now
spectrida --demo
spectrida analyze GameAssembly.dll spectrida analyze GameAssembly.dll --workers 8 # custom worker count
spectrida open file.i64
spectrida overview file.i64 spectrida overview file.i64 --addr 0x10001000 --addr 0x10353fd0 # include specific functions
spectrida export file.i64 -f idc # IDA script — apply names to any install spectrida export file.i64 -f json # full dump with addresses + sizes spectrida export file.i64 -f csv # spreadsheet spectrida export file.i64 -f symbols # addr name pairs spectrida export file.i64 --named-only # skip sub_* functions
spectrida serve
spectrida onboard
---
## Teclas da TUI
| Tecla | Ação |
|-----|--------|
| `N` | Nomear função selecionada — a IA transmite o resultado ao vivo |
| `R` | Renomear — pré-preenchido com a sugestão da IA |
| `D` | Alternar pseudocódigo descompilado (Hex-Rays) |
| `C` | Cadeia de chamadas — chamadores e chamados |
| `B` | Nomear em lote todas as funções `sub_*` na lista atual |
| `O` | Visão geral — resumo da IA de todo o binário |
| `/` | Pesquisa difusa |
| `?` | Ajuda |
| `Q` | Sair |
---
## API Programática
Nenhuma TUI necessária — controle o spectrIDA a partir de scripts, Claude Code, notebooks, etc.:```python
import asyncio
from spectrida.api import open_i64
async def main():
async with open_i64("GameAssembly.i64") as db:
# list all 153k functions
funcs = await db.list_functions()
# name one function — returns name + reasoning + confidence
result = await db.name_function(0x10001000)
print(result["new_name"]) # init_atexit_handler
print(result["reasoning"]) # allocates array of 3 fn ptrs, calls _atexit...
# batch name everything (with live progress)
async def on_progress(done, total, r):
print(f" {done}/{total} {r['old_name']} -> {r['new_name']}")
await db.batch_name(limit=500, rename=True, progress_cb=on_progress)
# ask what the binary does
overview = await db.overview()
print(overview)
# export to IDA script
await db.export("names.idc", fmt="idc", named_only=True)
asyncio.run(main())
hf.co/gdfhhjk/spectrida-re-gguf — Qwen3-8B
ajustado fino para engenharia reversa.
Treinado em:
jtsylve/ida-mcp — IDA headless com idalibAbordagem de treinamento: SFT + GRPO direcionado a neurônios. Apenas os neurônios relevantes para RE são ajustados — o conhecimento base do Qwen3 permanece intacto, você apenas adicionou uma habilidade muito específica por cima.
Executa localmente via Ollama. GGUF — funciona em CPU, GPU ou ambos.
Você está revertendo algo. Você tem um binário com 150.000 funções. Talvez 2.000 tenham nomes a
partir de metadados. As outras 148.000 são sub_XXXXXXXX. Você quer encontrar o código de rede.
Não consegue usar grep para isso porque nada tem um nome ainda.
Um RE humano pode nomear de ~50 a 100 funções por hora se for rápido. Nesse ritmo, 150k funções = 3 anos.
spectrIDA as nomeia durante a noite. Não perfeitamente — talvez 70% de precisão em funções
genéricas, muito maior em padrões que o modelo reconhece. Mas agora, em vez de 148k funções
sub_, você tem network_send_packet, serialize_player_state, validate_checksum — e você
sabe onde olhar.
Isso não substitui um engenheiro reverso habilidoso. Ele faz os chatos 80% para que você possa se concentrar nos 20% interessantes. É a camada de orientação.
Casos de uso reais:
sub_140001234 por 20 minutos pensando tem que
haver um jeito melhor~/.spectrida/config.toml:```toml
[ida]
idalib = "C:/Program Files/IDA Professional 9.1"
output_dir = "~/.spectrida/output"
[ollama] base_url = "http://localhost:11434" model = "spectrida-re" # any ollama model name works
[pipeline] workers = 16
Env var overrides: `SPECTRIDA_IDALIB` · `SPECTRIDA_MODEL` · `SPECTRIDA_WORKERS` · `SPECTRIDA_OLLAMA_URL`
---
## Adicionando um novo formato binário
O suporte a formatos é um sistema de plugins, não uma pilha de ramificações if/elif — PE, NSO e ELF são apenas arquivos em `spectrida/analysis/formats/`, descobertos automaticamente. Execute `spectrida formats` para ver o que está registrado atualmente.```bash
$ spectrida formats
ELF spectrida.analysis.formats.elf.ELFHandler
NSO spectrida.analysis.formats.nso.NSOHandler
PE spectrida.analysis.formats.pe.PEHandler
generic spectrida.analysis.formats.generic.GenericHandler
O trabalho de um format handler é restrito — olhar para um arquivo e dizer se você o possui, então descrever seu layout de código. Todo o resto (estratégia de fragmentação, varredura de prólogo GPU, mesclagem de fragmentos em um único .i64) é tratado uma vez, genericamente, fora do pacote de formatos. NSO é o exemplo completo: sua lógica de descompressão LZ4 + idaapi mem2base/add_segm já existia em nso_loader.py (a correção para os bugs de arquitetura errada/ainda comprimido/ponto de entrada localmente cego da história do Capítulo 2) — formats/nso.py é um adaptador fino que expõe esse módulo existente e validado através do contrato FormatHandler, não uma reescrita.
Para adicionar um formato, coloque um novo arquivo em spectrida/analysis/formats/ e nada mais. Sem edições em parallel_analyze.py, shard_worker.py ou no registro — ele é capturado pela varredura do diretório em busca de qualquer módulo que exponha uma instância HANDLER.```python
from spectrida.analysis.formats.base import FormatHandler, PreparedImage, Section
class MyFormatHandler(FormatHandler): name = "MYFMT"
@staticmethod
def sniff(header: bytes, path: str) -> bool:
return header[:4] == b"MYF0" # however you recognize the format
def prepare(self, path: str, workdir: str) -> PreparedImage:
# Format idalib already loads natively (ELF, PE, Mach-O)? Just parse
# the section/segment table — return the original path unchanged.
return PreparedImage(
binary_path=path,
image_base=0,
sections=[Section(name=".text", va=0x1000, raw_off=0x400,
raw_size=0x2000, vsize=0x2000, is_code=True)],
arch=None, # set "x86_64"/"arm64" only if IDA can't detect it itself
)
# Only needed if idalib has NO native loader for this format (NSO is the
# example): do any manual idaapi/ida_segment setup here, called right
# after idapro.open_database() succeeds, before analysis starts.
# def post_open(self) -> None: ...
HANDLER = MyFormatHandler()
That's the whole contract:
| Method | Required? | What it does |
|---|---|---|
| `sniff(header, path)` | sim | Verificação de bytes mágicos/extensão — este handler é o dono do arquivo? |
| `prepare(path, workdir)` | sim | Retorna uma `PreparedImage`: o arquivo que o idalib deve abrir + sua tabela de seções |
| `post_open()` | não (padrão sem operação) | Configuração manual de segmentos para formatos sem um loader IDA nativo (veja `nso.py`) |
| `make_shard_binary(image, dst, va_start, va_end)` | não (padrão funciona) | Sobrescreva apenas se zerar bytes de seção fora do shard estiver errado para seu formato (veja `nso.py` — nunca zere um arquivo comprimido) |
| `code_range(image)` | não (padrão funciona) | Sobrescreva apenas se "mín/máx das seções `is_code`" não for a resposta correta |
| `read_bytes(image, va_start, va_end)` | não (padrão funciona) | Sobrescreva se `prepare()` já mantiver os bytes relevantes em memória (NSO) em vez de em disco |
| `global_entry_points(image, text_start, text_end)` | não (padrão: None) | Sobrescreva apenas se uma varredura local por shard perder pontos de entrada reais — NSO precisa disso porque funções folha AArch64 só são descobertas através de alvos BL vistos em outras partes do binário, não por uma varredura local de prólogo |
Veja `formats/pe.py` para o handler mais simples possível (parseamento puro de cabeçalho, sem sobrescritas) e
`formats/nso.py` para o caso completo (empacota descompressão + configuração manual de segmentos + todas as sobrescritas).
Pacotes de terceiros também podem registrar um handler, sem tocar no código-fonte do spectrIDA, através
do grupo de pontos de entrada `spectrida.formats`:```toml
# in a separate package's pyproject.toml
[project.entry-points."spectrida.formats"]
myformat = "spectrida_myformat_plugin:HANDLER"
Test coverage for the format system lives in tests/test_formats.py — pure Python, no
IDA/idalib required, so it runs in CI.
Chapter 1 was a faster, funnier IDA. Chapter 2 is spectrIDA as a teammate: a persistent, queryable knowledge graph of every function it's ever named, and an MCP server so Claude (or any MCP client — pi works too) can search and reason through it directly, instead of you copy-pasting decompiler output into a chat window.```bash spectrida install mcp
É isso. Ele registra o servidor com Claude Code e pi automaticamente (puxando `mcp` + `neo4j` se um simples `pip install spectrida` os pulou), escreve a configuração deles e informa qual reinicialização você deve.
**O que Claude realmente obtém, uma vez que o Neo4j esteja em execução (seção `[graph]` da configuração do `spectrida`, ou apenas aponte para uma instância local):**
- `search_functions` / `get_function` / `get_callees` / `get_callers` / `trace_chain` — leituras rápidas do grafo em cache. `get_function` retorna pseudocódigo **e** desassembly (limites exatos de instrução e operandos — o que a camada de pseudocódigo não pode fornecer, o que importa no momento em que você passa de "o que isso faz" para "onde exatamente eu aplicaria um patch") além de chamadores/callees inline, para que Claude decida se aprofunda a cadeia verificando se um callee ainda é `sub_*` ali mesmo na resposta — sem viagem de ida e volta extra apenas para descobrir que não há mais nada a ver.
- `get_full_pseudocode` / `rename_function` — leituras/gravações diretas e autoritativas no `.i64` quando o trecho em cache não é suficiente ou um nome é finalmente descoberto.
- `analyze_binary` — entregue a ele um binário que nunca viu (PE ou NSO, paralelamente fragmentado de qualquer forma) e ele executa todo o pipeline — analisar → demangle (Itanium *e* MSVC) → nomear via IA os resíduos genuinamente despojados → empurrar tudo para o grafo — como um trabalho em segundo plano que você consulta, para que uma execução de vários minutos nunca bloqueie a conversa.
- `doctor` / `start_all` — verificar ou iniciar llama-server + Neo4j sem sair do chat. Se o próprio llama-server não estiver instalado em nenhum lugar, o `start_all` o obtém via winget (Windows) ou brew (macOS) primeiro — nenhum download/configuração separada do llama.cpp necessária.
Não é mágica — uma função que ainda é `sub_140001234` porque ninguém olhou para ela ainda continua sendo `sub_140001234`. Mas o grafo lembra de tudo que o modelo *descobriu*, para sempre, entre sessões, e Claude pode percorrê-lo como um colega que já leu o código-fonte, em vez de ficar olhando para uma função de cada vez.
**Ainda por vir:**
- **Nomeação de contexto profundo** — seguir árvores de chamada N níveis de profundidade, alimentar a cadeia completa para o modelo. Uma função a 3 saltos de `encrypt_block` deve saber que está no caminho cripto.
- **Deofuscação** — detecção de padrões TigressVM e rastreamento de handlers
- **Aplicação real de patches** — o desassembly está no grafo agora, então um agente *pode* planejar um patch no nível de byte; transformar "aqui está a instrução exata para mudar" em "e aqui está a escrita" é o próximo passo.
---
## Capítulo 3 — o fantasma atravessa paredes
Os Capítulos 1 e 2 leem o binário e o lembram. Mas ler uma função diz o que ela *é*, nunca o que ela *faz* quando você puxa o gatilho. O Capítulo 3 — [**phantomrt**](https://pypi.org/project/phantomrt/) — puxa o gatilho.```bash
pip install "spectrida[atlas]"
Ela pega uma função spectrIDA já nomeada e faz uma de três coisas assombradas com ela:
.nso do Switch ou um .so do Android.Então ele carimba o veredito no mesmo nó Neo4j como propriedades dyn_*, para que o agente percorrendo o gráfico veja o nome e o comportamento em um só lugar. Seis novas ferramentas MCP — emulate_function, hunt_crashes, live_trace, dynamic_overview, risk_functions, learn_vm — todas apoiando o único gráfico. O canto de raciocínio é qualquer LLM que esteja dirigindo; o phantomrt apenas garante que ele tenha fatos reais de tempo de execução para raciocinar, em vez de vibrações.```
spectrIDA (names it) ─▶ phantomrt (runs it) ─▶ graph (dyn_status / crash / live args) ─▶ the agent reasons
**E, no espírito da história de terror da NSO acima:** a primeira verdadeira caçada de bugs honestamente não encontrou
*nada* — e a razão foi mais interessante do que um travamento teria sido. Apontado para um
FreeType genuinamente antigo, genuinamente vulnerável, o fuzzing cego obteve zero. O fuzzing guiado por cobertura então
relatou um triunfante **64.000 arestas** cobertas — o que era mentira, porque o binário era PIE e
ASLR estava silenciosamente re-randomizando os endereços a cada execução, então "nova cobertura" era principalmente o carregador
embaralhando o baralho. Desabilitar ASLR, o número honesto cai para *reais* 727 arestas, e estabiliza
lá — porque cada semente era uma fonte TrueType, então o fuzzer ficou preso dentro de um analisador e
não conseguiu mutar estruturalmente seu caminho para o código CFF/Type1/BDF onde os bugs realmente vivem. O
conserto não foi um mutador mais inteligente, foram melhores sementes: o agente buscou amostras reais OpenType/Type1/BDF,
a cobertura saltou 727 → 4.013 antes de uma única iteração de fuzzing, e explorou corretamente.
Ainda assim, nenhum travamento no orçamento — que é o estado honesto de uma execução de 3 minutos em um núcleo contra um
trabalho que normalmente leva horas. A maquinaria é real. Ela pegou seu *próprio* número falso em vez de
reportá-lo. Esse é o ponto principal.
**Disclaimer honesto de fantasma:** phantomrt é `0.1.0`. Alfa. É uma camada dinâmica sólida, não um oráculo
mágico de bugs — encontrou bugs plantados em um alvo de brinquedo instantaneamente, e em alvos reais diz honestamente
quando está travado (`needs_state`), quando um travamento é apenas um *candidato* (vá verificar se o ponteiro
é realmente controlado por entrada), e quando precisa apenas de mais tempo e melhores sementes. Binários do Switch
não executam ao vivo — Frida precisa de algo que possa lançar — então lá, emulação é a única porta
e dirá `needs_state` bastante. Isso é honesto, não quebrado.
É o extra pesado intencionalmente (`torch`, `unicorn`, `frida`) — a base `pip install spectrida`
não puxa nada disso. O código-fonte está em [`phantomrt/`](https://github.com/ggfuchsi-oss/spectrida-reverse_engineering_stack/blob/HEAD/phantomrt/); seu próprio README com sabor fantasma está
[aqui](https://github.com/ggfuchsi-oss/spectrida-reverse_engineering_stack/blob/HEAD/phantomrt/README.md).
*O fantasma estático nomeia suas funções. Este as faz confessar.* 👻
---
## Licença
MIT. Faça o que quiser com ele. Se funcionar, legal.
Se não funcionar, culpe a quantização GGUF.
Construído com rancor, café e uma RTX 4070.
O modelo tem 199 downloads com zero marketing. Cada um adiciona 0,01% à velocidade de desenvolvimento.
(Isto não é verdade. Mas está perto.) 👻
| binary | functions | task | time / result |
|---|
| test_small.dll (PE) | 189 | parallel analysis, 4 workers, CLI | 6.4s |
| test_small.dll (PE) | 164 | full MCP pipeline (analyze + demangle + graph write) | 9.8s |
| main.nso — Mario Odyssey (NSO), 16 workers | 28,038 seed functions | parallel sharded scan phase | 54.5s |
| main.nso — Mario Odyssey (NSO), 16 workers | 74,790 total functions | + merge/full-analysis phase | 143.1s |
| main.nso — Mario Odyssey (NSO), 16 workers | 74,790 total functions | end-to-end wall time | 197.6s |
| main.nso — Mario Odyssey (NSO) | 74,790 | resolved via demangling alone (Itanium ABI, free, no AI) | 67,300 (90.0%) |
Player$$TakeDamageD alterna o pseudocódigo do Hex-Rays..idc ou um arquivo de símbolos. O .idc aplica todos os nomes gerados por IA de volta em qualquer instalação do IDA com um clique.from spectrida.api import open_i64. Controle tudo a partir de scripts, notebooks ou Claude Code sem tocar na TUI.spectrida install mcp conecta diretamente ao Claude Code e/ou pi, sem edição manual de JSON. Claude pode então pesquisar/ler/encadear através de um grafo de funções apoiado por Neo4j (nome, pseudocódigo, desmontagem, chamadores/chamados) e iniciar uma análise nova em um novo binário por si só — analyze_binary executa todo o pipeline (análise paralela → demangle → nomeação por IA → grafo) a partir de uma única chamada de ferramenta, como um job em background que ele consulta. Funciona em PE e NSO. Veja Capítulo 2 abaixo.spectrida --demo) — experimente tudo com zero configuração. Sem IDA, sem Ollama.