aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/app.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 22:33:16 +0200
committerblasty <blasty@local>2026-07-25 22:33:16 +0200
commitdcb76f92e8851f747e17cae848e800a394a073e1 (patch)
tree7b2ef9e92049e7a394f707e313cb79ba65309ced /idatui/app.py
parentnav: one notion of "which pane you're in" — delete _pref (diff)
downloadida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.gz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.xz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.zip
projects phase 3: follow an import into the binary that implements it
Following a call to strcmp reached the PLT/extern entry and stopped there — Hex-Rays has nothing to decompile, because the code lives in a library this binary only references. With the other binary open in the same project we already had everything needed to cross that gap; we just weren't indexing it. Index each binary's imports and exports (KIND_IMPORT / KIND_EXPORT) alongside its functions and strings. On a follow, _import_stub asks whether the target address is one of this binary's import stubs; if so _cross_binary_impl asks the index who exports that name, and we switch there instead of landing on the thunk. Verified end to end on a real echo + libc project: Enter on `strrchr(a1, 47)` in echo's pseudocode switches to libc.so.6 and lands on strrchr at 0xaf960. Three things it turns on: * ELF symbol versioning. The importer sees strrchr@@GLIBC_2.2.5 while the provider may export any of three spellings, so raw names resolve almost nothing. domain.link_name() cuts at the first '@'; Linkage.raw keeps what IDA reported, which is what the listing shows. * Exact match, not substring — ProjectIndex.exact(), so `read` doesn't bind to pread/read_line/thread_start. It also answers below the 3-char trigram floor, and plenty of real exports are that short. * Resolution reads the on-disk index, so a provider resolves while its worker is evicted. That's what the index was for. When nothing in the project provides the symbol _follow_import declines and the normal navigation runs: landing on the stub is still the honest answer, and a single-binary session is unchanged. The PLT-stub PRESENTATION item stays open — an unprovided import should say "imported, provider not in project" rather than show a decompiler error. server/patch_server.py gains list_linkage (idautils.Entries + enum_import_names); a worker without it degrades to no linkage rather than failing. tests: index join +8 (exact vs substring, short names, exclude-self, reverse join, kind isolation, forget unresolves) and link_name +4. 36/0 index, 195/0 scenarios, 23/0 project UI, 33/0 project, 22/0 pool.
Diffstat (limited to 'idatui/app.py')
-rw-r--r--idatui/app.py59
1 files changed, 57 insertions, 2 deletions
diff --git a/idatui/app.py b/idatui/app.py
index ff383be..39a4b94 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -3495,7 +3495,7 @@ class IdaTui(App):
ref = self._project.by_label(self._binary)
if ref is None or not self._index.is_stale(self._binary, ref.source):
return
- from .index import KIND_FUNC, KIND_STRING
+ from .index import KIND_EXPORT, KIND_FUNC, KIND_IMPORT, KIND_STRING
idx = self._func_index
entries = [(KIND_FUNC, f.addr, f.name) for f in (idx.all_loaded() if idx else [])]
try:
@@ -3504,12 +3504,18 @@ class IdaTui(App):
except Exception: # noqa: BLE001 -- symbols alone are still worth indexing
pass
try:
+ imps, exps = self.program.linkage()
+ entries += [(KIND_IMPORT, i.addr, i.name) for i in imps]
+ entries += [(KIND_EXPORT, e.addr, e.name) for e in exps]
+ except Exception: # noqa: BLE001 -- an old worker has no list_linkage
+ pass
+ try:
n = self._index.reindex(self._binary, entries, source=ref.source)
except Exception as e: # noqa: BLE001
self.app.call_from_thread(self._status, f"indexing failed: {e}")
return
self.app.call_from_thread(
- self._status, f"indexed {self._binary}: {n} symbols + strings")
+ self._status, f"indexed {self._binary}: {n} symbols, strings + linkage")
# -- initial landing --------------------------------------------------- #
#: function names tried (in order) as the startup landing spot
@@ -4228,8 +4234,55 @@ class IdaTui(App):
if tgt is None or tgt.to is None:
self.app.call_from_thread(self._status, "nothing to follow here")
return
+ if self._follow_import(tgt.to):
+ return
self._do_navigate(tgt.to, push=True)
+ def _import_stub(self, ea: int) -> str | None:
+ """The import name at ``ea``, if ``ea`` is one of this binary's import
+ stubs. That's the dead end phase 3 exists to open up: a call to strcmp
+ reaches the PLT/extern entry and Hex-Rays has nothing to decompile,
+ because the code lives in a library this binary only references."""
+ try:
+ imps, _ = self.program.linkage()
+ except Exception: # noqa: BLE001
+ return None
+ for i in imps:
+ if i.addr == ea:
+ return i.name
+ return None
+
+ def _cross_binary_impl(self, name: str) -> tuple[str, int] | None:
+ """``(binary, addr)`` of a project binary that EXPORTS ``name``.
+
+ Reads the on-disk index, so a provider resolves even when its worker was
+ evicted — the whole reason the index exists.
+ """
+ if self._index is None or self._project is None or not name:
+ return None
+ try:
+ hits = self._index.providers(name, exclude=self._binary)
+ except Exception: # noqa: BLE001
+ return None
+ return (hits[0].binary, hits[0].addr) if hits else None
+
+ def _follow_import(self, ea: int) -> bool:
+ """Follow an import stub into the binary that implements it. True when
+ it was handled (caller must not also navigate locally)."""
+ name = self._import_stub(ea)
+ if not name:
+ return False
+ found = self._cross_binary_impl(name)
+ if found is None:
+ # Leave the local navigation alone: landing on the stub is still the
+ # honest answer when nothing in the project provides the symbol.
+ return False
+ label, addr = found
+ self.app.call_from_thread(
+ self._status, f"{name} \u2192 {label} (import resolved)")
+ self.app.call_from_thread(self._switch_then_goto, label, addr)
+ return True
+
@work(thread=True, group="nav")
def _follow_decomp(self, line: str, word: str | None,
line_ea: int | None = None) -> None:
@@ -4257,6 +4310,8 @@ class IdaTui(App):
if addr is None:
self.app.call_from_thread(self._status, "nothing to follow on this line")
return
+ if self._follow_import(addr):
+ return
# Following FROM pseudocode: keep the reader in the decompiler when the
# target is decompilable, instead of dropping to the linear listing.
self._do_navigate(addr, push=True, prefer_decomp=True)