aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/project.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 23:49:14 +0200
committerblasty <blasty@local>2026-07-25 23:49:14 +0200
commit57b09047881d4bb42b2b98e4bb0739d5bc5e5af8 (patch)
treef55ca12add86873a875fe70cf318524fca694d95 /idatui/project.py
parentxrefs: show project binaries that import this export (diff)
downloadida-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/project.py')
-rw-r--r--idatui/project.py70
1 files changed, 63 insertions, 7 deletions
diff --git a/idatui/project.py b/idatui/project.py
index ac4f033..249ccd6 100644
--- a/idatui/project.py
+++ b/idatui/project.py
@@ -52,12 +52,50 @@ class BinaryRef:
label: str # unique within the project; names the staged file
source: str # absolute path to the original binary
staged: str # absolute path IDA actually opens (inside the sidecar)
+ #: How to LOAD it. Only meaningful for a headerless blob: an ELF/PE says what
+ #: it is, a raw firmware image doesn't, and IDA defaults to metapc at 0.
+ processor: str = "" # IDA processor name: arm, armb, mipsb, metapc, …
+ base: int = 0 # load address (natural, e.g. 0x8000000)
+ ida_args: str = "" # escape hatch: extra IDA command-line switches
@property
def db(self) -> str:
"""The database IDA creates for the staged file."""
return self.staged + ".i64"
+ @property
+ def load_args(self) -> str:
+ """``processor``/``base`` as IDA command-line switches.
+
+ ``-b`` is in PARAGRAPHS, not bytes — ``-b1000`` loads at 0x10000. That
+ is a trap worth hiding: projects say ``"base": "0x8000000"`` and the
+ conversion happens here.
+ """
+ parts = []
+ if self.processor:
+ parts.append(f"-p{self.processor}")
+ if self.base:
+ parts.append(f"-b{self.base >> 4:x}")
+ if self.ida_args:
+ parts.append(self.ida_args)
+ return " ".join(parts)
+
+
+def _as_addr(v) -> int:
+ """A load address from JSON: int, or a string in any base ("0x8000000").
+
+ Addresses are written by hand in a project file, so accept how people write
+ them rather than demanding decimal.
+ """
+ if v is None or v == "":
+ return 0
+ if isinstance(v, int):
+ return v
+ try:
+ return int(str(v), 0)
+ except ValueError:
+ return 0
+
def _stat_key(path: str) -> tuple[int, int] | None:
"""(size, mtime) identity used to spot a source that changed under us."""
@@ -109,7 +147,12 @@ class Project:
e = {"path": e}
if not isinstance(e, dict) or not e.get("path"):
raise ProjectError(f"project {path}: bad binary entry {e!r}")
- norm.append({k: e[k] for k in ("path", "label") if e.get(k)})
+ # Keep every recognised key: a whitelist of path/label silently
+ # dropped the load options on the first save, so a blob's processor
+ # and base vanished the moment the project was reopened.
+ norm.append({k: e[k] for k in
+ ("path", "label", "processor", "base", "ida_args")
+ if e.get(k) not in (None, "")})
name = raw.get("name") or os.path.splitext(os.path.basename(path))[0]
try:
pct = int(raw.get("memory_pct", DEFAULT_MEMORY_PCT))
@@ -119,8 +162,13 @@ class Project:
@classmethod
def create(cls, path: str, binaries: list[str], name: str | None = None,
- memory_pct: int = DEFAULT_MEMORY_PCT) -> "Project":
- """Write a new project file listing ``binaries`` (an ad-hoc project)."""
+ memory_pct: int = DEFAULT_MEMORY_PCT, load: dict | None = None) -> "Project":
+ """Write a new project file listing ``binaries`` (an ad-hoc project).
+
+ ``load`` carries per-binary load options (processor/base/ida_args) that
+ apply to every binary given here — a headerless blob needs them, and one
+ command line normally adds blobs of the same kind.
+ """
if not binaries:
raise ProjectError("a project needs at least one binary")
entries, seen = [], set()
@@ -130,7 +178,9 @@ class Project:
if key in seen:
continue
seen.add(key)
- entries.append({"path": p})
+ e = {"path": p}
+ e.update({k: v for k, v in (load or {}).items() if v})
+ entries.append(e)
path = os.path.abspath(os.path.expanduser(path))
proj = cls(path, name or os.path.splitext(os.path.basename(path))[0],
entries, memory_pct)
@@ -183,8 +233,12 @@ class Project:
n += 1
label = f"{label}_{n}"
used.add(label)
- refs.append(BinaryRef(label=label, source=src,
- staged=os.path.join(self.bin_dir, label)))
+ refs.append(BinaryRef(
+ label=label, source=src,
+ staged=os.path.join(self.bin_dir, label),
+ processor=str(e.get("processor") or ""),
+ base=_as_addr(e.get("base")),
+ ida_args=str(e.get("ida_args") or "")))
return tuple(refs)
@property
@@ -206,7 +260,8 @@ class Project:
return next((r for r in self._refs
if os.path.realpath(r.source) == key), None)
- def add(self, binary: str, label: str | None = None) -> BinaryRef:
+ def add(self, binary: str, label: str | None = None,
+ load: dict | None = None) -> BinaryRef:
"""Add a binary, or return the existing entry if it's already here."""
existing = self.by_source(binary)
if existing is not None:
@@ -214,6 +269,7 @@ class Project:
entry = {"path": os.path.abspath(os.path.expanduser(binary))}
if label:
entry["label"] = label
+ entry.update({k: v for k, v in (load or {}).items() if v})
self._entries.append(entry)
self._refs = self._build_refs()
return self._refs[-1]