September 15, 2026
Wingman, Not Autopilot
“Vibe Reverse Engineering” has a cost problem. I cut it down with a local LLM, a topological sort and some engineering — turns out it makes…

By jakoel
6 min read
"Vibe Reverse Engineering" has a cost problem. I cut it down with a local LLM, a topological sort and some engineering — turns out it makes a pretty decent wingman.
Reverse Engineering binaries with Claude Code is fun. Then you point it at a real binary and hit token limits really quick, because the thing has hundreds or thousands of functions. You pay frontier-model rates for every one of them — you only find out which were worth reading after you've paid for them all.
Running LLMs locally almost feels like magic to me and I'm experimenting with them whenever I can. They're customisable, they run on your local hardware, no API keys, no usage limits, and nothing leaves your machine.
I've split the job into two:
The wingman has your six. A Python tool drives IDA Pro headless and points a local LLM at every function IDA left as sub_* — decompiled pseudo-code, not raw assembly — asking the model for a new name, a one-line summary, and the reasoning behind both. Just let it run overnight on your gaming GPU or on your MacBook.
It isn't IDA MCP and it isn't an agentic loop: one structured call per function, walking the call graph bottom-up, no model choosing what to look at next or deciding it already explored enough. The run is boring and predictable.
The lead flies the mission. You, or a favorite agentic tool, can then open IDA yourself, hook it up via IDA MCP, or drive the .i64 directly with the same idapro library the project uses and observe the renamed functions and their summaries in comments. You spend expensive attention only on the functions that earn it. We won't discuss this part here.
How it runs
- MAP the binary. One pass over the instructions builds the call graph and annotates every function with the triage facts — memory sinks called, reachability from network or file input, referenced strings, imports, caller count. It answers a surprising number of questions on its own.
- ANALYZE is the one that spends LLM calls, so it always requires a scope and quotes the cost before it starts. What it asks about each function is set by
--profile— see the modes below. - APPLY is what makes it a wingman rather than another chatbot: the output isn't a report you read. It's your IDA database. Every accepted name and summary is written back into the IDA Database file, the
.i64file — renames applied, summaries attached as repeatable function comments so they appear at every call site. It also refuses to overwrite a real name — a PDB symbol, a library match, or one you typed yourself. - ASK lets you navigate by intent — what a function's summary means — instead of by address. Summaries are embedded with
nomic-embed-textinto a FAISS cosine-similarity index, andasknever opens the IDA database at all — queries are instant, with IDA closed.
python research_wingman.py ask target.i64 "C2 communication" --top 10python research_wingman.py ask target.i64 "C2 communication" --top 10
Making the model punch above its weight
1. Read the leaves first
Here is how a human reverses a function. You open the decompiled code:
You don't sit there and hallucinate a story about the functions it calls, sub_19898 or sub_232A4. You go into sub_19898 first. You work out what it does, write its name down, and then come back up — and now sub_10AA4 reads differently. That's exactly what we're doing: deep diving into the leaves first.
That's the entire ordering strategy. Functions are processed in a Kahn topological sort of the call graph — leaves first. By the time any caller is analyzed, everything it calls already has a name and a summary, injected straight into its context. The model never sees sub_19898. It sees what sub_19898 does.
2. Then go back up
Once you learn who calls a function, you revisit what you decided about it. A top-down pass re-queries each approved function with its callers' summaries: "looked like a generic memcpy wrapper — but it's only ever called from the header parser, so it's the length validator." When the body says nothing (return -1;), it reads the call site instead, and trusts that over either function's own summary — error sentinel, or unsigned no-limit sentinel? Only the caller knows.
3. What's actually in the prompt
Here's what one function's prompt actually looks like:
Every prompt is assembled once, upfront. It always includes the map-phase facts: memory functions called, reachability from outside input, how many places call it.
More things can get added, but only when they apply:
- A name the binary gave away. Some Windows components leave each function's real C++ name sitting in the binary as a plain string, a side effect of debug-logging — even stripped of symbols. When one turns up, it's the strongest signal available: CClfsLogFcbPhysical::FlushMetadata becomes flush_metadata.
- A known duplicate. Without this, the model can't tell a function is one of forty near-identical copies — same code, different embedded syscall number — and the naming rules push it to invent a unique-sounding reason anyway, fabricating behavior that isn't there. Told upfront — "36 other functions share this exact structure — a known, expected pattern, not evidence of anything unusual" — it names the one true, specific thing instead: something like
syscall_335_wrapper
4. Guardrails
Hedge, don't lie — but there's still a floor. Below 0.6 confidence, a rename is rejected outright. Between 0.6 and 0.7, it's accepted but hedged with a maybe_ prefix, the same convention an analyst would use.
In diff mode, two independent drafts get generated for every comparison. Disagreement means a mismatch on whether a difference was found at all, whether it's security-relevant, or the risk level — not the wording. When that happens, a third call re-examines the code and reconciles, and the disagreement stays visible in the report.
The catch
A small open source LLM quantized into 16GB of VRAM is not a frontier model. It won't catch everything. That's when the expensive model earns its keep. This is where you bring frontier model or yourself — not to grind through hundreds or thousands of functions, but to reason hard about the interesting ones.
So the whole game is what you put in front of it. Everything below answers one question: what context would a human reverse engineer have built up before reaching this function? Build that, inject it, and a small local model becomes good enough for triage.
The three modes
Same pipeline, three different questions.
- vuln_research is the default.
security_relevantandriskget framed around unbounded copies, user-controlled lengths, missing validation etc.
python research_wingman.py analyze target.i64 --all --profile vuln_researchpython research_wingman.py analyze target.i64 --all --profile vuln_research2. malware reframes the same fields around capability instead: C2, persistence, evasion, propagation, payload. It also asks for indicators to be quoted literally rather than paraphrased away.
python research_wingman.py analyze sample.i64 --all --profile malwarepython research_wingman.py analyze sample.i64 --all --profile malware3. patch-diff is a separate workflow, not a --profile choice: give it an old binary and a patched one and it pairs functions across both call graphs — by name where names exist, structurally where they don't. No BinDiff required (might be using BinDiff later). Byte-identical pairs or known compiler-noise drop out for free — same split as before — so the local model only spends a call on what actually changed, or exists on just one side.
What it sees per pair: the function's name, pseudocode before and after, and a one-line note if a neighbor also changed in the same patch — nothing else.
This is the most experimental piece of the pipeline, and I'm not sure per-pair local calls are the right shape for it. A frontier model given every changed function in one context window could reason about the whole diff at once instead of pair by pair — a real advantage this design lacks.
python research_wingman.py diff target-old.i64 target-new.i64 --autopython research_wingman.py diff target-old.i64 target-new.i64 --autoWhat it isn't
It doesn't replace you, and it doesn't find bugs . It reads everything once, cheaply, so that you don't have to — and hands you an up-to-date database to be smart in.
Takeaways
If there's one thing to take from this: the model was never the hard part. A small model on a consumer GPU can triage a decompiled binary — provided you spend your effort building the context a researcher would have had.
That's a wingman. It doesn't fly the mission. It just makes sure you're not flying it blind.
research-wingman is on GitHub: github.com/jakoel/research_wingman.