mirror of
https://codeberg.org/reverse-skill-lab/exe-reverse
synced 2026-11-15 20:34:25 +08:00
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:
@@ -19,7 +19,8 @@ pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _args(image, **kw):
|
||||
base = {"image": str(image), "out": None, "base": None, "iat": None, "max_sites": 40}
|
||||
base = {"image": str(image), "out": None, "base": None, "iat": None, "max_sites": 40,
|
||||
"no_discover": False, "discover_gap": 0x1000}
|
||||
base.update(kw)
|
||||
return argparse.Namespace(**base)
|
||||
|
||||
@@ -110,18 +111,41 @@ def test_the_report_pairs_the_restorable_sites_and_names_the_others(iat_scenario
|
||||
assert report["input"]["base_used"] == scen.base
|
||||
|
||||
|
||||
def test_without_the_slot_range_the_report_says_needs_trace(iat_scenario, load_script):
|
||||
"""No slot describes these targets, and the calls leave the image: that is the handle case.
|
||||
def test_without_the_slot_range_the_table_is_found_from_the_calls(iat_scenario, load_script):
|
||||
"""Discovery is the default, and it recovers the same two pairs the explicit range produced.
|
||||
|
||||
It is also the reason the verdict is not "clean". Nothing was found by pairing, and the correct
|
||||
reading is that pairing cannot work on this image, not that the image is untouched.
|
||||
The scenario's slots sit in a range no directory describes -- the normal shape of a captured
|
||||
image -- so this is the case discovery exists for. Arriving at the same two pairs as the --iat
|
||||
run is the whole claim: the table was found, not guessed.
|
||||
"""
|
||||
mod = load_script("iat_callsite_restore.py")
|
||||
scen = iat_scenario("pe64")
|
||||
blob = scen.path.read_bytes()
|
||||
pe = load_script("peutil.py").PE(blob)
|
||||
report = mod.build_report(_args(scen.path), blob, pe)
|
||||
|
||||
assert report["discovery"]["status"] == "unique"
|
||||
assert report["discovery"]["trigger"] == "no-pairs"
|
||||
assert report["discovery"]["chosen"] == {"start": 0x3020, "size": 0x18}
|
||||
assert "discovered-range" in report["iat"]["sources"]
|
||||
assert [p["site_rva"] for p in report["pairs"]] == [scen.sites["filler_after"],
|
||||
scen.sites["filler_before"]]
|
||||
assert {r["reason"] for r in report["refused"]} == {"no-filler", "ambiguous-slot"}
|
||||
|
||||
|
||||
def test_discovery_can_be_turned_off_for_a_control_run(iat_scenario, load_script):
|
||||
"""With discovery suppressed, the same image reports what the described slots could not do.
|
||||
|
||||
This is the control for the test above: without it, "discovery worked" could be a statement
|
||||
about the scenario rather than about the search.
|
||||
"""
|
||||
mod = load_script("iat_callsite_restore.py")
|
||||
scen = iat_scenario("pe64")
|
||||
blob = scen.path.read_bytes()
|
||||
pe = load_script("peutil.py").PE(blob)
|
||||
report = mod.build_report(_args(scen.path, no_discover=True), blob, pe)
|
||||
assert report["pairs"] == []
|
||||
assert report["discovery"] is None
|
||||
assert len(report["unpaired"]) == 4
|
||||
assert mod.decide_result(report, wrote=False) == "needs-trace"
|
||||
|
||||
@@ -145,6 +169,38 @@ def test_a_wrong_base_produces_the_same_verdict_as_a_missing_table(iat_scenario,
|
||||
assert report["input"]["base_source"] == "explicit --base"
|
||||
|
||||
|
||||
def test_two_clusters_of_matching_pointers_are_refused(iat_scenario_twin, load_script):
|
||||
mod = load_script("iat_callsite_restore.py")
|
||||
blob = iat_scenario_twin.read_bytes()
|
||||
pe = load_script("peutil.py").PE(blob)
|
||||
report = mod.build_report(_args(iat_scenario_twin), blob, pe)
|
||||
|
||||
assert report["discovery"]["status"] == "ambiguous"
|
||||
assert len(report["discovery"]["clusters"]) == 2
|
||||
assert report["discovery"]["chosen"] is None
|
||||
assert report["pairs"] == []
|
||||
assert mod.decide_result(report, wrote=False) == "ambiguous-iat"
|
||||
|
||||
|
||||
def test_discovery_reports_when_no_slot_holds_a_target(iat_scenario, load_script, tmp_path):
|
||||
"""A search that found nothing must say so; an empty result is not a clean image."""
|
||||
mod = load_script("iat_callsite_restore.py")
|
||||
scen = iat_scenario("pe64")
|
||||
blob = bytearray(scen.path.read_bytes())
|
||||
pe = load_script("peutil.py").PE(bytes(blob))
|
||||
for rva in (0x3020, 0x3028, 0x3030):
|
||||
off = pe.rva_to_offset(rva)
|
||||
blob[off:off + 8] = b"\x00" * 8
|
||||
path = tmp_path / "no-table.exe"
|
||||
path.write_bytes(bytes(blob))
|
||||
pe2 = load_script("peutil.py").PE(bytes(blob))
|
||||
report = mod.build_report(_args(path), bytes(blob), pe2)
|
||||
assert report["discovery"]["status"] == "none"
|
||||
assert report["discovery"]["hits"] == 0
|
||||
assert report["pairs"] == []
|
||||
assert mod.decide_result(report, wrote=False) == "needs-trace"
|
||||
|
||||
|
||||
def test_a_clean_image_reports_no_pairs(load_script, fixtures):
|
||||
mod = load_script("iat_callsite_restore.py")
|
||||
blob = fixtures.pe64.data
|
||||
|
||||
Reference in New Issue
Block a user