diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 325 |
1 files changed, 139 insertions, 186 deletions
@@ -1,248 +1,201 @@ -# ida-tui +<div align="center"> -A minimal, keyboard-first (mouse-capable) **TUI frontend for IDA Pro**, built with -[Textual](https://textual.textualize.io/) and using -[ida-codemode-mcp](../ida-codemode-mcp) as a Python library. +<img src="logo-trans.png" alt="ida-tui" width="420"> -ida-tui attaches to databases through `ida_codemode.client.DatabaseHandle`. A -matching database already open in the IDA GUI is reused; otherwise Code Mode -reuses or starts a shared managed idalib worker. The TUI owns only a client lease, -never the GUI or worker process. +**IDA Pro in a terminal.** Listing, decompiler, graph — keyboard-first, mouse-capable. -## ⚠️ Status: not ready for public consumption +`disasm` · `pseudocode` · `cfg` · `hex` · `strings` · `structs` · `traces` · `rpc` -This is a personal, actively-hacked-on project. It is **not** packaged, polished, -or supported for general use. Expect sharp edges: +</div> -- Hardcoded paths and assumptions (e.g. a venv at `~/ida-venv`, a specific - IDA/idalib layout). -- No stable API, no versioning promises, no changelog — things move and break. -- Requires a working IDA Pro + idalib install, which you must license and set up - yourself. -- Known open bugs (see the backlog below), including no handling of PLT/import - stubs. -- Basically undocumented beyond this file and `docs/`. +--- -If you found this expecting a finished tool: it isn't one yet. Poke around, but -don't file expectations. **Use at your own risk.** - -## What it does - -- A unified **IDA-style listing** (continuous disassembly interleaved with data / - undefined heads) as the default code view; `F5`/`Tab` drops into the - **decompiler (pseudocode)** for the function under the cursor. Both are - line-virtualized and page lazily over the Code Mode database. -- The startup splash draws the **real logo image** on terminals that speak the - kitty graphics protocol (~10× the resolution of the block art), and falls back - to `logo.ans` everywhere else. Support is detected by *asking the terminal*, - not by sniffing `$TERM` — under a multiplexer that passes the protocol through, - every environment variable you'd test is empty while the protocol works fine. -- A **control-flow graph** (`space`, IDA's own key): the current function's basic - blocks as boxes with routed, colour-coded edges (green taken / red fall-through - / blue unconditional / purple loop), laid out with a proper layered - (Sugiyama) algorithm. The boxes hold the *same listing rows* as the text view, - so highlighting, renames, xrefs and comments all work inside them. `z` cycles - three zoom levels, `m` toggles a minimap, `J`/`K` walk edges, and the mode is - sticky — following a call lands in the callee's graph. Above 400 blocks it - declines and says so, because nothing readable comes out at that size. - Details in [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md). -- A **Ghidra-style split view** (`s`): listing and pseudocode side by side, kept - in cursor sync — the focused pane drives and the other highlights the linked - region (every instruction a C line owns), following you across functions. - `Tab` or a click switches which pane leads. -- Keyboard navigation: follow (`enter`), xrefs (`x`, tagged call/read/write/ - offset), rename (`n`), retype (`y`), comment (`;`), incremental search (`/` - `?`), history (`back`), hex view (`\`), and `home`/`end`/`shift+home` line - motions. -- **Literal formats** (`o`, IDA's own key): cycle how the number under the - cursor is displayed — hex → decimal → binary → character → offset → IDA's - own choice, `O` to go the other way. Only the stops that make sense for that - value are visited (no `char` unless it prints as one, no `offset` unless the - target is something you could name), so no press is a silent no-op. It works - in the pseudocode too, on Hex-Rays' separate number formats. The opcode-bytes - column, which used to own `o`, moved to `B`. - - A line usually holds more than one literal (`test byte ptr [rsi+rax*2+1], 20h` - has two), so **the one the cursor is on is marked** — that mark is what `o` - changes, and it keeps up as the text reflows (`0x30` ↔ `48` move everything - after them). Land on something with no format of its own — a register — and it - says so and names the operand that does, rather than quietly reformatting a - different one. -- A **functions panel** (fuzzy symbol palette on `Ctrl+N`), a **strings browser** - (`"`, filterable, Enter jumps to the literal), **hex viewer**, **struct - editor**, and inline **make code/data/function/string** edits. -- A **command palette** (`Ctrl+P`) with the real ida-tui actions. -- An optional unix-socket **RPC layer** to puppeteer the live TUI from another - process (agent-driven RE / livestreaming). See `docs/RPC.md`. - -## Architecture (three layers, kept separate) - -- **`idatui/codemode_client.py`** — lifecycle and execution adapter. It leases a - registered GUI/idalib instance with `DatabaseHandle`, waits for autoanalysis, - normalizes errors, saves, and releases the lease. Address-centric operations - are sent through Code Mode's `execute_python` surface and use its preloaded - `ida-domain` `db` object. -- **`idatui/domain.py`** — synchronous, thread-safe paging/caching - (`FunctionIndex`, `DisasmModel`, `ListingModel`, decompile, xrefs, resolve). - It has no process/database ownership logic. -- **`idatui/app.py`** — the Textual app (virtualized `ScrollView`s, shared cursor/ - search/nav mixins, modals). +``` +┌ ida-tui ─────────────────────────────────────────────────────────────────────┐ +│ .text:00002490 ; ---------- S U B R O U T I N E ---------- │ +│ .text:00002490 main proc near │ +│ .text:00002490 endbr64 │ +│ .text:00002494 push rbp ; ← cursor │ +│ .text:00002495 mov rbp, rsp │ +│ .text:00002498 sub rsp, 0B0h ; `o` → 176 │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` -`idatui/pool.py` retains LRU project leases. Releasing an entry never kills a GUI -or another client's worker. See `docs/CODEMODE_PORT.md` for what maps to public -ida-domain APIs and which remaining features require IDAPython inside the Code -Mode execution sandbox. +> **Status: personal project, actively hacked on.** No packaging, no versioning, +> no support. It assumes a licensed IDA Pro and a venv at `~/ida-venv`. Things +> move and break. Poke around; don't file expectations. -## Requirements +## Why -- Python ≥ 3.11 -- IDA Pro 9.4+ with idalib configured -- `ida-codemode-mcp` installed in the TUI environment (this checkout uses the - editable sibling path `../ida-codemode-mcp`) -- The ida-codemode IDA plugin installed so GUI databases register themselves -- Textual ≥ 8 and Pygments ≥ 2 (`uv sync` installs both) +IDA's own UI is excellent and it is a GUI. This is for the times you're in a +terminal over SSH, in a tmux pane next to your notes, or driving RE from a +script — and still want the listing, Hex-Rays, and a real control-flow graph. -Code Mode's own worker launcher carries the correct Python environment; ida-tui -no longer searches for a second Python or imports `ida_pro_mcp`. +It is **not** a reimplementation of IDA. It's a frontend: IDA does the analysis, +this draws it. -## Running +## Install -Install the project and its TUI dependencies: +Needs **Python ≥ 3.11**, **IDA Pro 9.4+ with idalib**, and +[ida-codemode](https://github.com/HexRaysSA/ida-codemode) checked out next to +this repo (`../ida-codemode`): ```sh +git clone https://github.com/HexRaysSA/ida-codemode ../ida-codemode uv sync ``` -Pass an executable/IDB path. If the plugin has registered a matching GUI session, -ida-tui attaches to it; otherwise Code Mode opens a managed idalib database: +Install the ida-codemode plugin in IDA too, if you want the TUI to attach to +databases you already have open in the GUI. -```sh -./ida-tui /path/to/binary -``` - -With exactly one registered database, the path may be omitted: +## Run ```sh -./ida-tui +./ida-tui /path/to/binary # attach to a GUI session, or open a managed database +./ida-tui # attach, when exactly one database is registered ``` -When several databases are registered, the launcher lists their paths and asks -for one explicitly. A newly managed single-binary database still needs a writable -output location; projects stage binaries and IDBs in their sidecar directory. +ida-tui never owns an IDA process. It takes a **lease**: a matching database open +in the IDA GUI is reused, otherwise Code Mode starts or shares a managed idalib +worker. Quitting drops the lease and leaves everyone else alone. -Headerless blobs need to be told what they are — a raw firmware dump has no -format to detect, and IDA falls back to x86 at address 0, which analyses to -nothing: +Headerless blobs have no format to detect — IDA falls back to x86 at address 0 +and analyses nothing, so say what it is: ```sh ./ida-tui fw.bin --processor arm --base 0x8000000 ``` -ARM images that use Thumb need one more thing: press `t` on the listing to switch -ARM/Thumb decoding at the cursor (it sets IDA's `T` register, and the segment to -32-bit, since Thumb doesn't exist in AArch64). +`--base` is a real address. These apply only when a database is being **created**; +an existing IDB already records them. On ARM, `t` toggles ARM/Thumb decoding at +the cursor and `T` scans a vector table for Thumb entry points. -`--base` is a real address (Code Mode's typed loading address is also natural, -so no paragraph conversion crosses the dependency boundary). In a project the -options are recorded per binary. They apply only when Code Mode must create the -first database; a registered or existing IDB already records them. Arbitrary -`--ida-args` are rejected because `DatabaseHandle.open()` has no equivalent; -processor, base, and loader/file type are the supported import surface. +## Keys -ida-tui never deletes unpacked IDA scratch files during discovery: those files -may belong to a registered GUI or another Code Mode client. Registry locks and -health probes are the ownership authority. +| | | +|---|---| +| `enter` `escape` | follow / back | +| `tab` `F5` | listing ↔ pseudocode | +| `space` | control-flow graph (`z` zoom, `m` minimap, `J`/`K` walk edges) | +| `s` | split view — listing and pseudocode, cursor-synced | +| `g` `/` `?` | goto · search · search back | +| `x` `n` `y` `;` | xrefs · rename · retype · comment | +| `c` `d` `p` `u` `a` | make code · data · function · undefine · string | +| `o` `O` `B` | cycle this literal's format · reverse · opcode bytes | +| `\` `"` `ctrl+t` | hex · strings · structs | +| `ctrl+n` `ctrl+p` | symbol palette · command palette | +| `ctrl+s` `ctrl+l` `q` | save · reload as… · quit | +| `F1` | all of them | -## Execution traces +## What's in it -Load a [Tenet](https://github.com/gaasedelen/tenet) trace alongside the binary -and explore it in time: +**Listing** — one continuous IDA-style view: code, data and undefined runs +together, with IDA's own colour tags and per-operand marks. Line-virtualized, so +a 400 MB binary scrolls like a text file. -```sh -./ida-tui /path/to/binary --trace trace.0.log -``` +**Decompiler** — Hex-Rays pseudocode with syntax highlighting, per-line address +anchors, and rename/retype/comment that write back. -A docked pane on the right shows the registers at the current timestamp (the -ones the current instruction wrote are highlighted) and a timeline. `]` and `[` -step one instruction forward and back; `}` and `{` step over a call by following -the stack pointer. The code view follows. +**Graph** (`space`) — the current function's basic blocks, laid out with a real +layered (Sugiyama) algorithm and routed edges: green taken, red fall-through, +blue unconditional, purple loop. The boxes hold the *same rows* as the listing, +so highlighting, renames and xrefs work inside them. Above 400 blocks it declines +and says so, because nothing readable comes out at that size. +→ [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md) -Both code views are painted with the execution trail: where you just came from, -where you're about to go, and the instruction you're standing on. The pseudocode -view is painted too — a trace records instructions, but `decomp_map` says which -instructions each C line covers, so the same trail lands on the decompilation. +**Split view** (`s`) — listing and pseudocode side by side. The focused pane +drives; the other highlights every instruction the current C line owns. -The dock also shows the **stack as of that instant**, read out of the trace. -Bytes the trace never observed print as `??` rather than zeros — a trace knows -what it saw and nothing else. The hex view (`\`) gets the same treatment: bytes -the trace saw at this timestamp are shown in green over the file's own contents. +**Literal formats** (`o`) — hex → decimal → binary → char → offset, IDA's own +key. Only stops that change what you see are visited, so no press is a silent +no-op. The literal under the cursor is *marked*, and the mark is what changes — +it keeps up as the text reflows. Works on Hex-Rays' separate number formats too. -Trace addresses are rebased onto the database automatically — a traced process -is relocated, so nothing lines up until that's solved. +**Execution traces** — load a [Tenet](https://github.com/gaasedelen/tenet) trace +and move through time: -Traces are recorded separately; see `~/dev/tenet/tenet-original/tracers/` for the -QEMU tracer. +```sh +./ida-tui /path/to/binary --trace trace.0.log +``` -## RPC / driving the TUI +`]`/`[` step, `}`/`{` step over. Both code views are painted with the execution +trail — including the pseudocode, since `decomp_map` knows which instructions +each C line covers. The dock shows registers and the stack *as of that instant*; +bytes the trace never saw print as `??`, not zeros. Trace addresses are rebased +onto the database automatically. -Give the TUI `--rpc <sock>` to expose a unix-socket control channel, then drive -it from another pane: +**RPC** — drive the live TUI from another process (agent-driven RE, livestreams): ```sh ./ida-tui /abs/path/bin --rpc /tmp/ida.sock -python -m idatui.drive where # ergonomic terse-text helper +python -m idatui.drive where # terse-text helper python -m idatui.drive pc main # pseudocode of main python -m idatui.drive rename sub_5BE0 foo # goto + rename -python -m idatui.drive fmt dec # show this literal in decimal ``` -Or let `idatui.pane` spawn + manage TUI panes in tmux (see the idatui-rpc skill): +→ [`docs/RPC.md`](docs/RPC.md) -```sh -python -m idatui.pane spawn --open /abs/path/bin # -> {sock, pane, ready} -python -m idatui.pane list -python -m idatui.pane stop --sock <sock> +**Splash** — the logo renders as a real image on terminals that speak the kitty +graphics protocol, `logo.ans` everywhere else. Support is detected by *asking the +terminal*, not by sniffing `$TERM` (under a multiplexer, every variable you'd +test is empty while the protocol works fine). + +## Layout + +``` +idatui/codemode_client.py lease a database, run operations through Code Mode +idatui/remote_tools.py IDAPython that runs inside the database process +idatui/domain.py paging + caching, synchronous and thread-safe +idatui/app.py the Textual app +idatui/graph.py CFG layout — pure, no IDA, no Textual +idatui/rpc.py unix-socket control channel ``` -See `docs/RPC.md` for the full protocol. +Layers stay separate: `domain.py` owns no processes, `graph.py` does no I/O +(which is why its tests run offline in milliseconds), and `app.py` does no +analysis. ## Tests -`tests/run.py` is the front door — it runs every suite and prints one table. -The live suites attach through Code Mode (a registered GUI database, or a managed -idalib worker started on demand) for the given binary: - ```sh -python3 tests/run.py --fast # 257 checks, ~0.5s, any python3 — between edits -python3 tests/run.py trace -x # only files matching "trace", stop at first failure -python3 tests/run.py # all 733 checks, ~2m20s — before a commit -python3 tests/run.py --list # what would run, and whether it needs IDA +python3 tests/run.py --fast # 302 checks, <1s, any python3 — between edits +python3 tests/run.py --list # what runs, and what needs IDA +python3 tests/run.py # 788 checks, ~2m — before a commit ``` -It runs the suites **serially on purpose**: idalib contends hard enough that -running them 4-up took the suite from 153s to 296s and got three of them killed -mid-analysis. See the note in `tests/run.py`. - -Test files come in two kinds and **each one declares which** with a module-level -`NEEDS_IDA` marker (`run.py` reads it without importing the file, and refuses to -run if a file doesn't have one): - -- **pure** — stdlib only, no IDA, no worker, no binary. Seconds. This is what - you run between edits. -- **IDA** — spawns a real idalib worker on a real target and drives the headless - Textual `Pilot` against it. Minutes, and needs a licensed IDA. - -The individual suites still run standalone, which is how you iterate on one: +Every suite declares `NEEDS_IDA`; `--fast` runs only the pure ones (stdlib, no +IDA, no database). The rest drive a headless Textual `Pilot` against a real +database. Iterate on one with `--only`: ```sh -~/ida-venv/bin/python tests/test_scenarios.py targets/echo # full UI suite -~/ida-venv/bin/python tests/test_scenarios.py --only hex,rename +~/ida-venv/bin/python tests/test_scenarios.py targets/echo --only hex,rename ``` +Before optimising or debugging a slow run, read +[`.fastfeedback/SPEED.md`](.fastfeedback/SPEED.md) — per-suite timings, the known +flake, and the four ways a test here wastes minutes. + ## Docs -- `docs/RPC.md` — the RPC protocol -- `docs/CODEMODE_PORT.md` — port coverage, API gaps, and lifecycle semantics -- `docs/PAGING_FINDINGS.md` — historical paging/scale findings -- `docs/TEXTUAL_NOTES.md` — Textual pitfalls encountered -- `docs/TUI_DRIVING_BLUEPRINT.md` — generalizing the driving layer +| | | +|---|---| +| [`docs/RPC.md`](docs/RPC.md) | the RPC protocol | +| [`docs/GRAPH_VIEW.md`](docs/GRAPH_VIEW.md) | graph layout engine | +| [`docs/CODEMODE_PORT.md`](docs/CODEMODE_PORT.md) | lifecycle, API gaps, what's still IDAPython | +| [`docs/CODEMODE_UPSTREAM.md`](docs/CODEMODE_UPSTREAM.md) | findings sent to the Code Mode maintainers | +| [`docs/PAGING_FINDINGS.md`](docs/PAGING_FINDINGS.md) | paging and scale findings | +| [`docs/TEXTUAL_NOTES.md`](docs/TEXTUAL_NOTES.md) | Textual pitfalls, the hard way | + +## Known sharp edges + +- PLT/import stubs: following a libc symbol lands on `extrn X:near`, which + Hex-Rays can't decompile. No pretty-printing yet. +- Xref-select can land on the wrong line in the `x` popup. +- No "discard changes and quit": a managed worker saves when its last lease + closes, so the TUI can't promise your edits weren't persisted. +- A GUI user editing the same database won't invalidate our caches — Code Mode + has no change feed yet (see `docs/CODEMODE_UPSTREAM.md`). + +<div align="center"> +<sub><a href="https://sl0p.foo">sl0p.foo</a></sub> +</div> |
