diff options
| author | blasty <blasty@local> | 2026-07-25 23:49:14 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 23:49:14 +0200 |
| commit | 57b09047881d4bb42b2b98e4bb0739d5bc5e5af8 (patch) | |
| tree | f55ca12add86873a875fe70cf318524fca694d95 /idatui/launch.py | |
| parent | xrefs: show project binaries that import this export (diff) | |
| download | ida-tui-57b09047881d4bb42b2b98e4bb0739d5bc5e5af8.tar.gz ida-tui-57b09047881d4bb42b2b98e4bb0739d5bc5e5af8.tar.xz ida-tui-57b09047881d4bb42b2b98e4bb0739d5bc5e5af8.zip | |
loading: say how to read a headerless blob (processor, base)
A raw firmware dump has no format to detect, so IDA fell back to x86 at address
0. It doesn't fail — it opens, analyses, and finds nothing. An AArch64 image
loaded this way gave 0 functions; told the truth it gives 35.
ida-tui fw.bin --processor arm --base 0x8000000
and per binary in a project, which is what a multi-image firmware actually
needs:
{"path": "app.bin", "processor": "arm", "base": "0x8000000"}
idapro.open_database() already accepted IDA command-line switches; nothing was
passing any. Plumbed BinaryRef -> WorkerPool -> WorkerClient -> worker argv, plus
a load_args for the single-binary path that has no project ref.
base is written the way people say it (0x8000000, int or string, any base).
IDA's -b is in PARAGRAPHS — -b1000 loads at 0x10000 — so BinaryRef.load_args
converts, and a base that isn't 16-byte aligned is refused rather than silently
landing 16x off. ida_args passes anything else through.
Two bugs found by testing the whole path rather than the happy one:
* Project.load() whitelisted path/label when normalising entries, so the load
options were dropped the first time a project was reopened — set a processor,
come back tomorrow, it's gone.
* Re-passing the switches to an EXISTING database makes IDA refuse the open
(rc != 0, no functions). The .i64 already records how the image was loaded, so
the worker skips them once a database exists. My first guard checked
splitext(path) + ".i64" and never fired, because IDA names it "<file>.i64" —
keeping the extension. It checks both spellings now.
Verified on a real AArch64 blob: fresh load 35 functions based at 0x8002440,
reopen 35 again, and the CLI rejects an unaligned or non-numeric --base.
tests: +6 project (options recorded, paragraph conversion, file round-trip,
add() takes them, an ELF passes nothing, hex-string base). 39/0 project, 195/0
scenarios, 30/0 project UI, 36/0 index, 27/0 pool.
Diffstat (limited to 'idatui/launch.py')
| -rw-r--r-- | idatui/launch.py | 48 |
1 files changed, 45 insertions, 3 deletions
diff --git a/idatui/launch.py b/idatui/launch.py index 9618668..b28559e 100644 --- a/idatui/launch.py +++ b/idatui/launch.py @@ -23,6 +23,18 @@ import sys _LOCK_SUFFIXES = (".id0", ".id1", ".id2", ".nam", ".til") +def _load_args(load: dict) -> str: + """``load`` as IDA switches, for the single-binary path (no project ref).""" + parts = [] + if load.get("processor"): + parts.append(f"-p{load['processor']}") + if load.get("base"): + parts.append(f"-b{int(load['base']) >> 4:x}") # -b is PARAGRAPHS + if load.get("ida_args"): + parts.append(str(load["ida_args"])) + return " ".join(parts) + + def _log(msg: str) -> None: print(f"ida-tui: {msg}", file=sys.stderr) @@ -57,8 +69,37 @@ def main(argv: list[str] | None = None) -> int: help="do not run the keepalive heartbeat") p.add_argument("--rpc", metavar="PATH", help="listen for RPC on this unix socket (puppeteer the TUI)") + g = p.add_argument_group( + "loading a headerless blob", + "An ELF/PE/Mach-O says what it is. A raw firmware dump doesn't, and IDA " + "falls back to x86 at address 0 — which analyses to nothing. These say " + "how to read it, and are recorded per binary in a project.") + g.add_argument("--processor", metavar="NAME", + help="IDA processor: arm, armb (big-endian), mipsb, metapc, …") + g.add_argument("--base", metavar="ADDR", + help="load address, e.g. 0x8000000 (any base; NOT paragraphs)") + g.add_argument("--ida-args", metavar="STR", dest="ida_args", + help="extra IDA command-line switches, passed through as-is") args = p.parse_args(argv) + load: dict = {} + if args.processor: + load["processor"] = args.processor + if args.base: + try: + base = int(args.base, 0) + except ValueError: + _log(f"--base must be a number (got {args.base!r})") + return 2 + if base % 16: + # -b is in paragraphs, so an unaligned base cannot be expressed and + # would silently load somewhere else. + _log(f"--base must be 16-byte aligned (got {base:#x})") + return 2 + load["base"] = base + if args.ida_args: + load["ida_args"] = args.ida_args + project = None binary = None if args.project: @@ -70,7 +111,7 @@ def main(argv: list[str] | None = None) -> int: if args.binary: # extend an existing project, skipping repeats before = len(project.refs) for b in args.binary: - project.add(b) + project.add(b, load=load or None) added = len(project.refs) - before dupes = len(args.binary) - added if added: @@ -80,7 +121,7 @@ def main(argv: list[str] | None = None) -> int: _log(f"{dupes} already in the project (matched by path) " f"— left alone") elif args.binary: - project = Project.create(ppath, args.binary) + project = Project.create(ppath, args.binary, load=load or None) _log(f"created project {ppath} with {len(project.refs)} binaries") else: _log(f"no such project: {ppath} (pass binaries to create it)") @@ -120,7 +161,8 @@ def main(argv: list[str] | None = None) -> int: return 1 rpc_path = os.path.abspath(os.path.expanduser(args.rpc)) if args.rpc else None IdaTui(open_path=binary, keepalive=not args.no_keepalive, - rpc_path=rpc_path, ttl=args.ttl, project=project).run() + rpc_path=rpc_path, ttl=args.ttl, project=project, + load_args=_load_args(load)).run() return 0 |
