Find the import table from the calls when no directory describes it

A captured image often has no import data: the descriptor is gone, the IAT directory is
gone, and nobody took a write watch. The table is still findable, because the rewrite left
a relation behind -- the slot holds the address the call targets -- so the call sites
become the search key.

discover_iat scans the non-executable sections for pointer-aligned entries equal to the
target of a relative call that leaves the image, then clusters the matches by proximity.
One cluster is used and its span becomes the inventory. Two are printed and refused: a
duplicated table, a copied table and a table split by a section boundary all look like
this from the outside, and picking one produces a file that loads and calls the wrong API
for one import. A single match is called weak rather than used.

Discovery runs when the described slots explained none of the calls, not only when the
inventory came back empty -- a protector that leaves a plausible-looking but useless
descriptor is the case the narrower rule missed. --iat suppresses it because that is the
reader asserting the range, and --no-discover is the control that keeps "the search
worked" from being a statement about the fixture.

walk_sites and pair_sites replace the single scan-and-pair pass, because discovery needs
the targets before any slot exists and one function could not express that order.
decide_result gains ambiguous-iat, which exits 1 like the other two verdicts that mean
this run did not finish the job.

Tests: the same image with its slots outside every directory recovers the two pairs the
explicit range produced; the control with --no-discover still reports needs-trace; a
second copy of the table in another section is refused with its own token; and a search
that matched nothing says so instead of reading as a clean image.

The reference gains the search, its two refusals, and the pair of failures that look
identical from the report -- slots holding tags have no value relation to search, and a
wrong base finds nothing either, so the base is checked first.
This commit is contained in:
root
2026-10-27 16:45:46 +08:00
parent 9ce5ac29d2
commit 5d7c41bdd5
10 changed files with 331 additions and 74 deletions
+19
View File
@@ -591,6 +591,25 @@ def iat_scenario(tmp_path_factory, build_fixture, load_script):
return _build
@pytest.fixture(scope="session")
def iat_scenario_twin(iat_scenario, fixture_dir, load_script):
"""The same scenario with one more slot in a different section.
Two runs of matching pointers is the case discovery must refuse: the second cluster holds the
same target from a section the first cluster is not in, which is what a duplicated or copied
table looks like from the outside. Built here so both layers test the same bytes.
"""
scen = iat_scenario("pe64")
data = bytearray(scen.path.read_bytes())
pe = load_script("peutil.py").PE(bytes(data))
off = pe.rva_to_offset(0x5010)
assert off is not None, "the second-cluster RVA has no file offset in the fixture"
data[off:off + 8] = struct.pack("<Q", scen.base + 0x9000)
path = fixture_dir / "pe64-iat-twin.exe"
path.write_bytes(bytes(data))
return path
@pytest.fixture(scope="session")
def synthetic_elf(fixture_dir):
"""The generated ELF64, written once for the whole session."""