AI Pentesting, Part 11: Ghidra and IDA Pro: Evidence-Grounded AI Reverse Engineering
Decompiler output is a hypothesis about machine code. AI can make that hypothesis readable, but plausible variable names and recovered types can also hide mistakes. Anchor every claim to a binary hash, address, instruction range, and explicit observation. Start with a small parser whose source is known.
Lab boundary: written authorisation, synthetic identities and data, isolated targets, bounded requests, and human approval for active actions. Examples are instructional fixtures, not claims of testing a live organisation. Tool inventory reviewed 2026-10-28; compatibility and availability can change.
1. The current Ghidra and IDA integration landscape
Inventory reviewed 28 October 2026. Ghidra provides disassembly, decompilation, scripting, headless analysis, and PyGhidra integration; those are analysis substrates, not themselves an AI agent. LaurieWired/GhidraMCP is a community bridge exposing operations such as decompilation, function listing, imports/exports, and renaming. Its reviewed README targets Ghidra 12.1.4 even though the main Ghidra project lists a newer release. Match the extension's declared compatibility, Java runtime, and Python requirements; 'latest everywhere' is not a compatibility plan.
| Integration | Ownership / role | Version and capability caution |
|---|---|---|
| Ghidra scripts / PyGhidra | Native analysis and automation APIs | Use the API shipped with the installed Ghidra release |
| GhidraMCP | Community MCP bridge | Reviewed README names Ghidra 12.1.4; inspect GUI versus headless requirements |
| Hex-Rays IDA MCP | Official Hex-Rays bridge built on IDA Nexus | Reviewed requirements: IDA 9.4+, idalib, Python 3.11+, Git and uv |
| mrexodia/ida-pro-mcp | Community bridge and historical ecosystem | README now recommends official Hex-Rays server; older prerequisites are not interchangeable |
| IDAPython / Hex-Rays APIs | Native scripting and decompiler interfaces | Licensing, decompiler availability, and headless support vary |
An AI assistant extension and an MCP server are not synonyms. A plugin may call its own provider API; a bridge may expose tools to any compatible external client. Inspect both data flow and database-write permissions before loading proprietary binaries.
2. Compile a known, benign parser fixture
Save this as lab_parser.c in an isolated analysis directory. It reads a two-byte little-endian length and returns a status; it copies no data and contains no intentional memory-corruption primitive. The input requests nine bytes against a capacity of eight, so the program prints -2. Use the source as ground truth, then hide it during the first analysis pass.
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
int parse_record(const uint8_t *header, size_t available, size_t capacity) {
if (header == NULL || available < 2) return -1;
size_t length = (size_t)header[0] | ((size_t)header[1] << 8);
if (length > capacity) return -2;
return (int)length;
}
int main(void) {
const uint8_t header[2] = {9, 0};
printf("%d\n", parse_record(header, sizeof header, 8));
return 0;
}cc -g -O0 -fno-inline -o lab_parser lab_parser.c
./lab_parserThe commands assume a POSIX-style shell and GCC/Clang toolchain; use the corresponding local compiler workflow on Windows. Do not disable ASLR or other platform protections. For a second sample, compile with optimisation and compare recovered control flow. A changed layout does not imply changed semantics.
3. How to analyse the fixture in Ghidra
- Create a fresh project and import the hashed executable with the correct language and compiler specification.
- Run the standard analysis and locate
parse_recordthrough symbols or its call from main. - Inspect the disassembly and decompilation side by side.
- Identify the null check, two-byte minimum, little-endian decode, capacity comparison, and return values.
- Ask AI to explain only that function and its caller, with addresses for each claim.
- Compare the explanation with the known source and record discrepancies.
A native Ghidra script can list function entry points without modifying the program. Run this in the scripting environment supported by your installed release.
functions = currentProgram.getFunctionManager().getFunctions(True)
count = 0
while functions.hasNext() and count < 10:
function = functions.next()
print(str(function.getEntryPoint()) + " " + function.getName())
count += 1For GhidraMCP, install the extension matched to the declared Ghidra version, restart, enable the plugin, configure a loopback listener, and connect the client using the bridge's documented transport. Discover the tool schema. Begin with function listing and one decompile request; do not auto-approve renames, prototypes, patches, or arbitrary scripts.
4. How to use IDA and its MCP boundary
For the official server, follow the installed release's Hex-Rays CLI workflow and verify IDA, idalib, and Python compatibility first. The reviewed documentation provides stdio and Streamable HTTP modes and session logs. Avoid enabling multiple overlapping IDA MCP servers; duplicate tools can confuse the client and route a request to the wrong database. Licensing and decompiler support remain prerequisites.
Open a copied analysis database, allow auto-analysis to finish, and confirm the binary hash and image base. Request a bounded function list, the fixture's decompilation, and cross-references. Names in a community bridge's schema are not guaranteed names in the official server. Query the actual tools rather than copying calls from an unrelated tutorial.
Analyse parse_record in the selected lab database only.
Return: architecture, image base, function address, input widths,
branch conditions, return values, caller assumptions.
For every statement cite an instruction or decompiler address.
Separate observed code from inferred types.
Do not rename, patch, run scripts, or open other databases.The older community bridge exposes powerful operations including Python evaluation, patching, and optional debugger control. A read-only session must not expose those capabilities merely because the same package also contains inspection tools.
5. Test type, signedness, and address assumptions
Example one: a decompiler recovers length as signed. Compare the actual extension and comparison instructions before claiming a signedness defect. Example two: the model interprets the two bytes as big-endian. The shift and OR establish little-endian assembly in this source; verify in instructions. Example three: the function name looks like a copying routine, but the code only validates a length. Names are clues, not semantics.
Keep file offset, relative virtual address, and runtime address separate. ASLR changes runtime addresses; relocations and loader mappings change what is present in memory. An address citation without module identity and base can point at the wrong bytes. On x86-64, calling conventions differ between common Windows and System V ABIs; on ARM64, registers and argument recovery differ again.
Allow AI to propose clearer names and types as a draft. Apply changes only after review, group them into reversible database transactions where supported, and retain the original database. Model-renamed functions can bias later analysis, so mark provisional names distinctly in the evidence record.
6. Validate conclusions independently
Work the boundaries: available bytes 0, 1, and 2; decoded lengths 0, 7, 8, and 9 with capacity 8; and a null pointer passed only from a controlled test harness. The fixture should reject missing headers, accept lengths through capacity, and reject nine. Do not infer a buffer overflow: no copy occurs in this function. Whether a caller safely uses the result is a separate question.
Export a compact report with sample hash, tool versions, analysis settings, function addresses, confirmed semantics, unresolved assumptions, and source comparison. Use the same binary in Part 12 to distinguish static hypotheses from runtime state. Primary materials checked for this inventory were the Ghidra project README, GhidraMCP README, official Hex-Rays IDA MCP README, and community ida-pro-mcp README; no compatibility is implied beyond those dated declarations.
7. Step-by-Step Lab: Recover the Same Parser in Two Analysis Databases
Prerequisites: A patched, appropriately licensed analysis environment; the known benign C fixture in Section 2; a compiler; and isolated project directories. Ghidra/IDA, decompiler, Python, Java, and MCP prerequisites must match the exact installed releases. No unknown executable needs to run for static analysis.
- Compile the known fixture with debug information and no optimisation using the earlier command. Record source hash, executable hash, architecture, toolchain, and build flags. Preserve this binary unchanged for both analysis tools.
- Import a copy into Ghidra, confirm the language/compiler specification, and allow standard analysis to finish. Locate parse_record and its caller. Do not apply AI renames before the first evidence pass.
- Open the same hashed binary in an isolated IDA database. Confirm image base, bitness, and decompiler availability. Record function entry points rather than assuming addresses will match another build.
- In each tool, locate the minimum-header check, byte loads, shift/OR expression, capacity comparison, and return blocks. Request an AI explanation with one address citation for each claim.
- Run the offline byte-order fixture below as an arithmetic control. Compare it with the actual instruction semantics; do not ask a language model to convert bytes by intuition.
- Draft names and types separately from observed facts. Review signedness, calling convention, and pointer width before applying reversible database changes. Keep the original databases and an edit log.
- Compare both explanations with source ground truth. If they disagree, identify the specific instruction or caller assumption that distinguishes them. Carry only unresolved runtime questions into the LLDB walkthrough in Part 12.
const header = [9, 0];
const littleEndian = header[0] | (header[1] << 8);
const bigEndian = (header[0] << 8) | header[1];
function fixtureResult(bytes, available, capacity) {
if (bytes === null || available < 2) return -1;
const length = bytes[0] | (bytes[1] << 8);
return length > capacity ? -2 : length;
}
const cases = [fixtureResult(header, 2, 8), fixtureResult([8, 0], 2, 8), fixtureResult(header, 1, 8)];
if (littleEndian !== 9 || bigEndian !== 2304 || cases.join("|") !== "-2|8|-1") throw Error("parser control mismatch");
console.log(JSON.stringify({ littleEndian, bigEndian, cases }));Expected result: Little-endian length 9, big-endian comparison value 2304, and parser control results -2, 8, and -1. This reproduces the source-level fixture logic; actual binary semantics still require instruction evidence. The function validates a length and performs no copy, so these results do not demonstrate memory corruption.
8. Worked Examples and Expected Outcomes
Example A: a recovered signed type looks suspicious
The decompiler displays a signed local, while the actual comparison uses the semantics of the original size type. Inspect extension, comparison, and branch instructions before reporting a signedness defect. Record a provisional type until the evidence supports it.
Example B: a friendly name invents behaviour
An assistant renames the function copy_payload even though no copy occurs. Revert the misleading name and use decoded-length terminology. Later reviewers should not inherit an implied exploit primitive from an AI-generated label.
Example C: a newer extension fails to load
The analysis tool was upgraded beyond the bridge's supported release. Keep the core tool patched through your approved process, verify extension compatibility, and use native scripting or a compatible supported bridge rather than forcing an old binary into an unsupported runtime.
Cross-tool evidence row:
sample SHA-256 | tool + version | image base | function address
instruction range | decompiler hypothesis | caller assumption
source-ground-truth comparison | analyst decision | unresolved question9. Troubleshooting, Retesting, and Completion Evidence
| Symptom | Likely cause | Discriminating check |
|---|---|---|
| Functions appear at different addresses | Different build, base, or analysis naming | Compare sample hashes, image bases, and relative offsets |
| Decompiler output is unavailable | Licence, architecture, or analysis prerequisite missing | Inspect disassembly and document the unavailable higher-level view |
| Both tools agree on a wrong type | Shared recovery assumptions are not independent proof | Check instructions and source/runtime evidence directly |
Retest and completion: Retain both database versions, build manifest, address-cited claims, reversible edits, and source comparison. Add capacity-minus-one, capacity, and capacity-plus-one cases. Static agreement should not become an unsupported claim about exploitability or caller safety.
10. Real Tool Workflow: Build a Known Parser and Compare Ghidra with IDA
Prerequisites: A trusted C compiler, a patched compatible Ghidra/JDK installation, and an appropriately licensed IDA/decompiler environment. Use the complete known benign lab_parser.c source earlier in this article, not a downloaded unknown binary. Commands below show a Windows Clang/Ghidra setup; adapt paths through your installed tools' documentation.
Use reviewed releases and record the versions actually installed. Command references for this workflow were reviewed on 29 October 2026; product support and flags can change. Instructions and output shapes are not claims of a live assessment.
| Tool | Role | Important boundary |
|---|---|---|
| Clang and file hashing | Build and identify the same known sample for both tools | Running the compiled sample is allowed only because this source is reviewed and owned |
| Ghidra / analyzeHeadless | Create a reproducible static-analysis project | Native parsers and extensions still need isolation and patching |
| IDA / IDAPython | Inspect function boundaries and decompiler evidence | Decompilation/headless/MCP features have separate version and licensing requirements |
Step-by-Step Tool Operation
- Create an isolated workspace with the known C fixture. Record compiler version and source hash, then compile a debug/no-optimisation build. Run only this reviewed fixture to confirm its documented -2 output.
- Record the executable hash and keep that exact file unchanged for both analysis tools. Do not compare two independent recompilations as if matching filenames implied matching bytes.
- Set GHIDRA_HOME to the actual compatible installation directory and create a fresh project directory. Run the headless import command below, retain its log, and check analysis completion before trusting function metadata.
- Open the resulting project in Ghidra's GUI and inspect the parser's disassembly and decompilation side by side. Identify byte loads, shift/OR, capacity comparison, and return blocks with addresses.
- Open the same hashed sample in IDA, allow auto-analysis to finish, and inspect its known parser and caller. The bounded IDAPython snippet lists only ten function names/addresses; it does not rename, patch, or execute the sample.
- Give AI a bounded excerpt from each tool, require address citations and competing type interpretations, then compare with source ground truth. Preserve original databases before reviewed edits and close both analysis sessions when finished.
clang --version
Get-FileHash -LiteralPath .\lab_parser.c -Algorithm SHA256
clang -g -O0 -fno-inline -o .\lab_parser.exe .\lab_parser.c
if ($LASTEXITCODE -ne 0) { throw 'Known fixture compilation failed' }
.\lab_parser.exe
Get-FileHash -LiteralPath .\lab_parser.exe -Algorithm SHA256
if (-not $env:GHIDRA_HOME) { throw 'Set GHIDRA_HOME to the reviewed compatible installation' }
$Headless = Join-Path $env:GHIDRA_HOME 'support\analyzeHeadless.bat'
if (-not (Test-Path -LiteralPath $Headless)) { throw 'Headless launcher not found' }
$ProjectDirectory = New-Item -ItemType Directory -Path .\ghidra-projects -ErrorAction Stop
& $Headless $ProjectDirectory.FullName ParserLab -import .\lab_parser.exe -log .\ghidra-analysis.logimport idautils
import ida_funcs
for address in list(idautils.Functions())[:10]:
print(hex(address), ida_funcs.get_func_name(address))Expected Evidence and Interpretation
The Python snippet belongs in IDA's reviewed Python console/script environment, not an arbitrary system interpreter. A successful headless import should produce a project and log; exact recovered names and addresses depend on the build and tool analysis. The known source's runtime result is -2, but the analysis tools may render the signed constant differently. Verify instructions and caller use before interpreting it.
If a compatible MCP bridge is added, discover its actual tools and default to bounded read operations. Do not expose arbitrary IDAPython, Java, or Python execution merely because the same bridge also supplies decompilation. The official and community IDA servers are not interchangeable packages with identical requirements.
Troubleshooting and Cleanup
If Clang lacks a configured SDK/runtime, use the platform's approved build environment; do not disable protections or elevate simply to force compilation. If the Ghidra extension or JDK is incompatible, use supported versions or native analysis. Keep databases, sample hashes, logs, and reversible edits as private research artefacts; remove only disposable projects you created and no longer need.