ida-reverse · git:20260925.95cd4b3 · 2026-09-25 · sha256 b9e2b7aec007575f

ida-reverse git:20260925.95cd4b3B

Immutable. This exact content never changes and is served at /api/v1/blob/b9e2b7aec007575f.

---
name: ida-reverse
description: Headless reverse engineering with IDA Pro — batch decompilation, IDAPython scripting, xref mapping, and automated function analysis via idat. Use for scripted binary analysis, decompiler output at scale, and CI-style RE pipelines.
---

# IDA Pro Reverse Engineering (headless)

IDA's advantage over every other RE tool is **automation at scale**: one script,
thousands of functions, decompiler output you can grep. This skill drives
`idat.exe` in batch mode (`-A`) with IDAPython (`-S`).

## Verified environment

This skill was validated on a real install — use these exact paths on this host:

```bash
IDA="/c/Program Files/IDA Professional 9.3/idat.exe"      # headless
IDAGUI="/c/Program Files/IDA Professional 9.3/ida.exe"    # GUI, same DB
IDAPYSWITCH="/c/Program Files/IDA Professional 9.3/idapyswitch.exe"
```

**Binaries lost their `64` suffix in 9.3.** On 9.0–9.2 they are `idat64.exe`,
`ida64.exe`, and `idalib64.dll`. On 9.3 they are `idat.exe`, `ida.exe`, and
`idalib.dll`. Resolve the name at runtime rather than hardcoding it:

```bash
IDA_HOME="${IDA_HOME:-/c/Program Files/IDA Professional 9.3}"
for n in idat.exe idat64.exe; do
  [ -x "$IDA_HOME/$n" ] && { IDA="$IDA_HOME/$n"; break; }
done
```

Confirm the install before relying on it:

```bash
"$IDA" -h 2>&1 | head -3
"$IDAPYSWITCH" --dry-run 2>&1 | tail -3
```

Expected: IDA 9.3, and IDAPython bound to a Python 3.13 DLL. If
`idapyswitch --dry-run` reports no Python, run `idapyswitch -a` to auto-apply.

**Verified working output** (357 functions from `notepad.exe`, real symbol names —
if you get an empty log, IDAPython is not bound):

```
IDAPY_OK version=9.3
FUNCS=357
IMPORTS=1
HEXRAYS=True
  ??$Write@U?$_tlgWrapperByRef@$0BA@@@... @ 0x140001008
  _tlgWriteTransfer_EventWriteTransfer @ 0x14000125c
```

## MCP access

Two transports. The **headless idalib supervisor is the recommended one** —
`ida-pro-mcp`'s own README states the GUI plugin "is no longer recommended and
will eventually be deprecated". Do not register both, or tools register twice.

```bash
pip install ida-pro-mcp        # 2.0.0 or later
idalib-mcp --port 8745         # headless supervisor; or:
python -m ida_pro_mcp.idalib_supervisor --port 8745
```

It serves MCP on `http://127.0.0.1:8745/mcp` — loopback only — with **66 tools**.

Every tool call must carry `database=<session_id>`. Give the supervisor a binary
at startup and it opens automatically; find its id with `idb_list`, or open one
with `idb_open`.

```
survey_binary  → arch, base address, md5/sha256, segment table, function counts
decompile      → takes `addr`, NOT `address`
list_funcs     → takes `queries` (a list); it rejects `limit`
```

> **Version note.** The headless supervisor needs IDA **9.1+**: it calls
> `enable_console_messages` and friends, which first appear in the 9.1 `idalib`
> library. On IDA 9.0 that library exports only `init_library`, `open_database`
> and `close_database`, so the worker exits immediately. Verified end to end on
> **9.3** (`idalib 9.3.260213`).

The **legacy GUI plugin** still works from IDA 8.3 up: `ida-pro-mcp --install`,
restart IDA, then `Edit → Plugins → MCP` (`Ctrl-Alt-M`). It listens on
`http://127.0.0.1:13337/mcp`.

Set `IDA_HOME` so launchers find the install. To move the port, edit
`servicePort` for `idapro` in
`scripts/reverse-skill/bootstrap-manifest.json` — not an env var.

## The batch driver

```bash
# -A  autonomous: no dialogs, no prompts (required for scripting)
# -S"script.py"  run this IDAPython script on load
# -c  force a new database even if one exists
# result: <target>.i64  plus whatever your script writes
"$IDA" -A -S"probe.py" target.exe
```

### Rule 1 — IDA's stdout is unreliable, write to a file

Console output from `idat` is frequently swallowed, especially when launched
from a non-interactive shell. **Always write findings to a log file from inside
the script** and read the file afterwards. A script that only `print()`s will look
like it silently failed.

```python
LOG = r"C:\path\to\out.log"
with open(LOG, "w", encoding="utf-8") as f:
    f.write("IDAPY_OK version=%s\n" % idaapi.get_kernel_version())
```

### Rule 2 — always `qexit(0)`

Without it the process can hang waiting on the database, and your batch job
never returns.

```python
idc.qexit(0)
```

### Rule 3 — `-A -c` for a clean run

`-A` suppresses prompts. `-c` discards any stale `.i64`, which matters because a
cached database silently skips the analysis your script assumes has run.

## Canonical probe script

```python
import idaapi, idautils, idc

LOG = r"C:\path\to\out.log"
with open(LOG, "w", encoding="utf-8") as f:
    f.write("IDAPY_OK version=%s\n" % idaapi.get_kernel_version())
    funcs = list(idautils.Functions())
    f.write("FUNCS=%d\n" % len(funcs))
    for ea in funcs[:8]:
        f.write("  %s @ %#x\n" % (idc.get_func_name(ea), ea))

    # imports the binary actually resolves
    f.write("IMPORTS=%d\n" % len(list(idautils.Entries())))

idc.qexit(0)
```

## Decompiling at scale

```python
import idaapi, idautils, idc
from ida_hexrays import decompile, init_hexrays_plugin

OUT = r"C:\path\to\pseudocode.txt"
init_hexrays_plugin()

with open(OUT, "w", encoding="utf-8") as f:
    for ea in idautils.Functions():
        try:
            cf = decompile(ea)
            f.write("\n/* ==== %s @ %#x ==== */\n" % (idc.get_func_name(ea), ea))
            f.write(str(cf) + "\n")
        except Exception as exc:
            f.write("\n/* %s @ %#x: decompile failed: %s */\n"
                    % (idc.get_func_name(ea), ea, exc))

idc.qexit(0)
```

Then grep the output for your sink:

```bash
grep -n -B4 -A2 'strcpy\|system\|memcpy' pseudocode.txt
```

### Decompiler availability

`init_hexrays_plugin()` returns falsy when the Hex-Rays decompiler is not
licensed or not loaded. Check it — do not assume `decompile()` works:

```python
if not init_hexrays_plugin():
    f.write("NO DECOMPILER — falling back to disassembly only\n")
```

Fall back to `idc.generate_disasm_line(ea, 0)` when it is unavailable.

## Cross-reference mapping

```python
import idautils, idc

def callers(name):
    out = []
    for ea in idautils.Functions():
        if idc.get_func_name(ea) != name:
            continue
        for xref in idautils.CodeRefsTo(ea, 0):
            out.append((idc.get_func_name(xref), xref))
    return out

with open(LOG, "w", encoding="utf-8") as f:
    for ea in idautils.Functions():
        nm = idc.get_func_name(ea)
        if nm in ("memcpy", "memcpy_s", "strcpy", "system"):
            for caller, addr in callers(nm):
                f.write("%s -> %s @ %#x\n" % (caller, nm, addr))
```

For data xrefs (function pointer tables, config globals) use `idautils.DataRefsTo`.
A **writable function-pointer table with a nearby overflow** is the classic
control-flow hijack — find those first.

## Common workflows

**Security triage of an unknown binary**
1. `idautils.Entries()` — import table tells you the capability set.
2. Strings pass: `idautils.Strings()` filtered for URLs, paths, format strings.
3. Xrefs to dangerous sinks (`strcpy`, `system`, `WinExec`, `sprintf`).
4. Decompile only the reaching functions, not the whole binary.

**"Is this mitigated?"**
Check the PE/ELF hardening before anything else — NX/DEP, ASLR, stack canary,
SafeSEH/CFG. `idc.get_inf_attr(INF_.LOADERFLAGS)` and manual segment checks cover
most of it; a missing canary changes the whole exploitation plan.

**Patching**
```
PatchByte / PatchDword  then  idaapi.save_database(path)
```
Patch the **branch condition**, not the whole check — smaller diff, easier to
review, less likely to break the surrounding code.

**Comparing two builds**
Load both, match by symbol and by structural hashing of the decompiled body.
Function-level diff beats byte-level diff for anything compiler-optimised.

## Pitfalls

- **Silent stdout.** Verified here: console prints vanish; the log file is the
  only reliable channel. This is the single most common cause of a "failed" IDA
  script that actually worked.
- **Stale `.i64`.** A database from a prior run skips analysis. Use `-c`.
- **No `qexit`.** The batch never returns; CI jobs hang until timeout.
- **Decompiler not licensed.** `init_hexrays_plugin()` is falsy — guard for it.
- **356 functions is not "the whole binary."** FLIRT/library signatures collapse
  CRT and runtime code; the interesting code is what is *left* after library
  recognition, so check which signatures loaded.
- **Very long function names are normal** in templated C++ (`??$Write@U?...`).
  Truncate for reporting, never for matching.
- **MSYS path conversion.** `idat.exe` is a native Windows binary: pass it
  `C:/...` paths, not `/c/...`. Pass MSYS paths and it fails to find the target.
  The log path *inside* the script is a native Windows path too — `read_file`
  can read it, but a bare bash `cat` of `/c/...` will not see it.
- **Read-only target directory.** IDA writes `<target>.i64` (plus `.id0/.id1/.id2/
  .nam/.til`) next to the input, so a binary under `C:\Windows\System32` fails
  with `Permission denied`. That error looks like a licensing failure but is not.
  Copy the sample to a writable directory first.
- **Antivirus.** IDA creates a large `.i64` next to the target; some AV products
  flag this pattern. Keep test targets outside protected directories.