Files
2026-10-24 01:50:24 +08:00

162 lines
6.6 KiB
Python

"""Unit tests for skills/exe-reverse/scripts/aob_scan.py.
The tool's real output is the *verdict*: a pattern that matches twice is a coin flip, and the
whole point is that the caller is told so. The boundary table is asserted at both sides of each
threshold, and the pattern-file parser is tested in all four of its accepted shapes.
"""
import types
import pytest
pytestmark = pytest.mark.unit
def ns(**kwargs):
base = {"patterns": []}
base.update(kwargs)
return types.SimpleNamespace(**base)
# --- verdict boundaries ------------------------------------------------------
@pytest.mark.parametrize("count,expected", [
(0, "no-match"),
(1, "unique"),
(2, "ambiguous"),
(3, "ambiguous"),
(7, "ambiguous"),
(8, "pervasive"),
(64, "pervasive"),
])
def test_verdict_boundaries(load_script, count, expected):
assert load_script("aob_scan.py").verdict(count, count) == expected
def test_verdict_thresholds_are_the_documented_ones(load_script):
aob = load_script("aob_scan.py")
assert aob.NOTICEABLE_AT == 8
assert aob.AMBIGUOUS_AT == 2
assert aob.verdict(aob.NOTICEABLE_AT - 1, 0) == "ambiguous"
assert aob.verdict(aob.NOTICEABLE_AT, 0) == "pervasive"
def test_verdict_ignores_the_unused_total_argument(load_script):
aob = load_script("aob_scan.py")
assert aob.verdict(1, 999) == aob.verdict(1, 0) == "unique"
# --- locate ------------------------------------------------------------------
def test_locate_reports_offset_rva_and_section(load_script, fixtures):
aob = load_script("aob_scan.py")
peutil = load_script("peutil.py")
pepatch = load_script("pepatch.py")
pe = peutil.PE(fixtures.pe64.data, None)
pat = pepatch.parse_bytes(fixtures.pe64.plant)[0]
rows = aob.locate(pe, fixtures.pe64.data, pat)
assert [r["offset"] for r in rows] == list(fixtures.pe64.plant_offsets)
assert [r["rva_hex"] for r in rows] == [fixtures.pe64.meta["plant"]["first_rva"],
fixtures.pe64.meta["plant"]["second_rva"]]
assert [r["section"] for r in rows] == [".text", ".text"]
assert rows[0]["offset_hex"] == fixtures.pe64.meta["plant"]["first_file_offset"]
def test_locate_section_filter_drops_hits_elsewhere(load_script, fixtures):
aob = load_script("aob_scan.py")
peutil = load_script("peutil.py")
pat = [0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x2E, 0x70, 0x64, 0x62] # "fixture.pdb"
pe = peutil.PE(fixtures.pe64.data, None)
assert [r["section"] for r in aob.locate(pe, fixtures.pe64.data, pat)] == [".rdata"]
assert aob.locate(pe, fixtures.pe64.data, pat, section_filter=".text") == []
assert len(aob.locate(pe, fixtures.pe64.data, pat, section_filter=".rdata")) == 1
def test_locate_context_is_centred_on_the_match(load_script, fixtures):
aob = load_script("aob_scan.py")
peutil = load_script("peutil.py")
pepatch = load_script("pepatch.py")
pe = peutil.PE(fixtures.pe64.data, None)
pat = pepatch.parse_bytes(fixtures.pe64.plant)[0]
rows = aob.locate(pe, fixtures.pe64.data, pat, context=8)
assert "context" in rows[0]
assert rows[0]["context_marks"] == "pre 8 bytes, match 12, post 8"
assert len(pepatch.parse_bytes(rows[0]["context"])[0]) == len(pat) + 16
def test_locate_returns_no_rows_when_the_pattern_is_absent(load_script, fixtures):
"""A negative outcome: an absent pattern yields an empty list, not a nearest match."""
aob = load_script("aob_scan.py")
peutil = load_script("peutil.py")
pe = peutil.PE(fixtures.bare.data, None)
pat = [0x66, 0x69, 0x78, 0x74, 0x75, 0x72, 0x65, 0x2E, 0x70, 0x64, 0x62]
assert aob.locate(pe, fixtures.bare.data, pat) == []
# --- load_patterns -----------------------------------------------------------
def test_load_patterns_accepts_an_inline_pattern(load_script):
aob = load_script("aob_scan.py")
assert aob.load_patterns(ns(patterns=["48 8B ?? 05"])) == [
("inline", [0x48, 0x8B, None, 0x05], "48 8B ?? 05")]
def test_load_patterns_without_patterns_is_empty(load_script):
assert load_script("aob_scan.py").load_patterns(ns()) == []
assert load_script("aob_scan.py").load_patterns(ns(patterns=[])) == []
def test_load_patterns_parses_a_text_file_with_comments_and_names(load_script, tmp_path):
aob = load_script("aob_scan.py")
path = tmp_path / "sigs.txt"
path.write_text("# a comment\n\n48 8B 05\nuse-site\t90 90 90\n", encoding="utf-8")
got = aob.load_patterns(ns(patterns=[str(path)]))
assert got == [("sigs.txt:3", [0x48, 0x8B, 0x05], "48 8B 05"),
("use-site", [0x90, 0x90, 0x90], "90 90 90")]
def test_load_patterns_parses_a_json_object(load_script, tmp_path):
aob = load_script("aob_scan.py")
obj = tmp_path / "named.json"
obj.write_text('{"patterns": [{"name": "gate", "pattern": "74 05"}, "EB 05"]}',
encoding="utf-8")
assert aob.load_patterns(ns(patterns=[str(obj)])) == [
("gate", [0x74, 0x05], "74 05"),
("named.json[1]", [0xEB, 0x05], "EB 05")]
def test_a_bare_json_list_is_accepted(load_script, tmp_path):
"""A top-level array is as reasonable a shape as an object with a "patterns" key.
The reader used to call ``.get`` on whatever ``json.load`` returned, so a top-level array
raised AttributeError instead of the SystemExit this module uses for every other bad input.
Both shapes are read now, and anything else is refused with a message naming what was found.
"""
aob = load_script("aob_scan.py")
bare = tmp_path / "bare.json"
bare.write_text('["CC CC"]', encoding="utf-8")
assert aob.load_patterns(ns(patterns=[str(bare)])) == [("bare.json[0]", [0xCC, 0xCC], "CC CC")]
def test_a_json_scalar_is_refused_with_a_message(load_script, tmp_path):
"""The shape that is still not a pattern file says so instead of raising."""
aob = load_script("aob_scan.py")
scalar = tmp_path / "scalar.json"
scalar.write_text('42', encoding="utf-8")
with pytest.raises(SystemExit) as excinfo:
aob.load_patterns(ns(patterns=[str(scalar)]))
assert "must hold an array or an object" in str(excinfo.value)
@pytest.mark.parametrize("text,expected", [
("ZZ QQ", "not a byte"),
("?? ??", "empty or all wildcards"),
])
def test_load_patterns_exits_on_a_pattern_it_cannot_parse(load_script, tmp_path, text,
expected):
aob = load_script("aob_scan.py")
path = tmp_path / "bad.txt"
path.write_text(text + "\n", encoding="utf-8")
with pytest.raises(SystemExit) as excinfo:
aob.load_patterns(ns(patterns=[str(path)]))
assert expected in str(excinfo.value)