diff options
| author | blasty <blasty@local> | 2026-07-25 22:33:16 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-25 22:33:16 +0200 |
| commit | dcb76f92e8851f747e17cae848e800a394a073e1 (patch) | |
| tree | 7b2ef9e92049e7a394f707e313cb79ba65309ced /server/patch_server.py | |
| parent | nav: one notion of "which pane you're in" — delete _pref (diff) | |
| download | ida-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 'server/patch_server.py')
| -rw-r--r-- | server/patch_server.py | 35 |
1 files changed, 35 insertions, 0 deletions
diff --git a/server/patch_server.py b/server/patch_server.py index 09bca86..0e43ea5 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -733,6 +733,41 @@ def list_strings( "total": len(items), "next_offset": offset + len(page), } + +@tool +@idasync +def list_linkage( + kind: Annotated[str, "'import', 'export' or 'both'"] = "both", +) -> dict: + """What this binary imports from, and exports to, other modules: + {imports:[{addr,name,module}], exports:[{addr,name,ordinal}]}. Feeds the + project-wide import/export join, which resolves a PLT stub in one binary to + the real implementation in another.""" + import idaapi + import idautils + import ida_nalt + want = str(kind or "both").lower() + imports = [] + exports = [] + if want in ("import", "both"): + n = ida_nalt.get_import_module_qty() + for i in range(n): + mod = ida_nalt.get_import_module_name(i) or "" + + def _cb(ea, name, ordinal, _mod=mod): + # An ordinal-only import has no name; skip rather than invent one. + if name: + imports.append({"addr": hex(ea), "name": name, "module": _mod}) + return True + + ida_nalt.enum_import_names(i, _cb) + if want in ("export", "both"): + for index, ordinal, ea, name in idautils.Entries(): + if name: + exports.append({"addr": hex(ea), "name": name, + "ordinal": int(ordinal)}) + return {"imports": imports, "exports": exports, + "n_imports": len(imports), "n_exports": len(exports)} ''' SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n" |
