227 lines
11 KiB
Python
227 lines
11 KiB
Python
"""Unit tests for skills/exe-reverse/scripts/dart_snapshot_strings.py, the Dart AOT string reader.
|
|
|
|
The input is an ELF shared object and no sample may be committed, so the container is generated by
|
|
`conftest.build_synthetic_elf`: a valid ELF64 header, a `.text` of sixteen bytes, a `.rodata` with
|
|
one one-byte (ASCII) literal and one two-byte (UTF-16LE) literal at known offsets, and a
|
|
section-name string table. Offsets are read from the fixture's own record of where it put each
|
|
literal, never hardcoded here.
|
|
|
|
What the synthetic image cannot do is be a real snapshot. Literal recovery is what these tests hold:
|
|
a one-byte and a two-byte literal at known offsets, a case-insensitive filter, and an offset probe
|
|
that resolves anywhere a literal occupies. The instruction-level observations this file once carried
|
|
are **withdrawn** -- a byte scan cannot read this format, and the reference records why -- so nothing
|
|
here asserts anything about how a reference is encoded.
|
|
"""
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
ONE_BYTE = "one-byte"
|
|
TWO_BYTE = "two-byte"
|
|
|
|
# The two idioms the observer counts, assembled here so the positive cases are built rather than
|
|
# asserted from a table. `49 8B /r` is a load through r15 -- Dart's pool pointer; `48 8B 05` is a
|
|
# RIP-relative load, which the docstring records as near-absent in a real image.
|
|
POOL_LOAD = bytes((0x49, 0x8B, 0x87, 0x10, 0x00, 0x00, 0x00))
|
|
FILLER = bytes((0x90,)) * 8
|
|
|
|
|
|
def text_section(data, vaddr=0x1000, offset=0):
|
|
return {"offset": offset, "vaddr": vaddr, "size": len(data)}
|
|
|
|
|
|
def hex_bytes(*values):
|
|
return bytes(values)
|
|
|
|
|
|
# --- the container -----------------------------------------------------------
|
|
|
|
def test_the_fixture_is_an_elf_shared_object_the_reader_accepts(load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
elf = dss.Elf(synthetic_elf.data, str(synthetic_elf.path))
|
|
assert elf.machine == 0x3E
|
|
assert elf.etype == 3
|
|
described = elf.describe()
|
|
assert described["machine"] == "x86-64"
|
|
assert described["type"] == "shared-object"
|
|
assert [s["name"] for s in described["sections"]] == [".text", ".rodata", ".shstrtab"]
|
|
|
|
|
|
def test_the_fixture_is_the_size_the_builder_promised(synthetic_elf):
|
|
assert len(synthetic_elf.data) == synthetic_elf.size
|
|
assert synthetic_elf.size < 0x1000, "a virtual address above this cannot be a file offset"
|
|
|
|
|
|
def test_sections_are_found_by_name(load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
elf = dss.Elf(synthetic_elf.data)
|
|
text = elf.section(".text")
|
|
rodata = elf.section(".rodata")
|
|
assert text["offset"] == synthetic_elf.text["offset"]
|
|
assert text["vaddr"] == synthetic_elf.text["vaddr"]
|
|
assert text["size"] == synthetic_elf.text["size"]
|
|
assert rodata["offset"] == synthetic_elf.rodata["offset"]
|
|
assert rodata["vaddr"] == synthetic_elf.rodata["vaddr"]
|
|
assert rodata["size"] == synthetic_elf.rodata["size"]
|
|
assert elf.section(".absent") is None
|
|
|
|
|
|
@pytest.mark.parametrize("mutation,message", [
|
|
(lambda d: bytearray(b"not an elf image at all" + bytes(64)), "not an ELF image"),
|
|
(lambda d: bytearray(b"\x7fELF" + d[4:]), None), # a valid image
|
|
(lambda d: bytearray(d[:4] + bytes((1,)) + d[5:]), "not a 64-bit ELF"),
|
|
(lambda d: bytearray(d[:5] + bytes((2,)) + d[6:]), "not little-endian"),
|
|
(lambda d: bytearray(d[:0x30]), "not an ELF image"), # truncated header
|
|
])
|
|
def test_the_reader_refuses_a_container_it_cannot_read(load_script, synthetic_elf, mutation,
|
|
message):
|
|
"""A refusal is a ValueError with a reason; the CLI turns it into the documented exit code."""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
data = mutation(bytearray(synthetic_elf.data))
|
|
if message is None:
|
|
assert dss.Elf(bytes(data)).etype == 3
|
|
return
|
|
with pytest.raises(ValueError) as excinfo:
|
|
dss.Elf(bytes(data))
|
|
assert message in str(excinfo.value)
|
|
|
|
|
|
def test_the_address_maps_round_trip(load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
elf = dss.Elf(synthetic_elf.data)
|
|
for entry in synthetic_elf.literals:
|
|
assert elf.vaddr_to_offset(entry["vaddr"]) == entry["offset"]
|
|
assert elf.offset_to_vaddr(entry["offset"]) == entry["vaddr"]
|
|
assert elf.vaddr_to_offset(0xDEADBEEF) is None
|
|
assert elf.offset_to_vaddr(0xFFFFFF) is None
|
|
|
|
|
|
# --- literal recovery --------------------------------------------------------
|
|
|
|
def test_every_literal_in_the_fixture_is_recovered_at_the_offset_it_was_placed(
|
|
load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
elf = dss.Elf(synthetic_elf.data)
|
|
recovered = dss.recover_strings(synthetic_elf.data, elf.section(".rodata"))
|
|
assert [(off, va, text, enc) for off, va, text, enc in recovered] == [
|
|
(entry["offset"], entry["vaddr"], entry["text"], entry["encoding"])
|
|
for entry in synthetic_elf.literals]
|
|
|
|
|
|
def test_both_encodings_are_recovered(load_script, synthetic_elf):
|
|
"""A keyword search that only reads ASCII misses half of a localised application's literals."""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
elf = dss.Elf(synthetic_elf.data)
|
|
recovered = dss.recover_strings(synthetic_elf.data, elf.section(".rodata"))
|
|
by_encoding = {}
|
|
for _off, _va, text, encoding in recovered:
|
|
by_encoding.setdefault(encoding, set()).add(text)
|
|
assert by_encoding[ONE_BYTE] == {"premium_license", "trial_mode"}
|
|
assert by_encoding[TWO_BYTE] == {"vip_pass"}
|
|
|
|
|
|
def test_a_two_byte_literal_reports_byte_offsets_and_character_counts(load_script, synthetic_elf):
|
|
"""Pinned, because the two units differ by a factor of two for this encoding.
|
|
|
|
The offsets are bytes; the text is characters. `--at` filters on `offset <= at < offset +
|
|
len(text)`, so for a two-byte literal it accepts only the first half of the bytes the literal
|
|
occupies. Measured here as a unit fact; the CLI layer pins the effect.
|
|
"""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
entry = synthetic_elf.literal("vip_pass")
|
|
recovered = dss.recover_strings(synthetic_elf.data,
|
|
dss.Elf(synthetic_elf.data).section(".rodata"))
|
|
offset, vaddr, text, encoding = [row for row in recovered if row[0] == entry["offset"]][0]
|
|
assert (offset, vaddr, encoding) == (entry["offset"], entry["vaddr"], TWO_BYTE)
|
|
assert text == "vip_pass"
|
|
assert len(text) * 2 == 16, "sixteen bytes on disk, eight characters reported"
|
|
|
|
|
|
def test_a_two_byte_run_does_not_glue_the_tail_of_one_literal_to_the_next(load_script):
|
|
"""A run is reported from its own first byte, never from the tail of the literal before it.
|
|
|
|
This used to be a false positive. The scan accepted *any* printable byte followed by a NUL --
|
|
which is exactly the shape of the last character of an ASCII literal plus its alignment byte --
|
|
so a two-byte literal beginning right after one was reported as a literal that does not exist,
|
|
at an offset inside a different one. Both directions are asserted here, because the cheap fix
|
|
for the glue error (skip anything preceded by a character-shaped pair) reintroduces the opposite
|
|
one: the real run then starts with its own bytes looking like an interior position.
|
|
"""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
ascii_part = b"odd_phase_literal\x00"
|
|
assert len(ascii_part) % 2 == 0, "the two-byte run needs to start at an even offset"
|
|
data = ascii_part + "vip_pass".encode("utf-16-le") + b"\x00\x00"
|
|
recovered = dss.recover_strings(data, {"offset": 0, "vaddr": 0x1000, "size": len(data)})
|
|
texts = [row[2] for row in recovered]
|
|
assert "odd_phase_literal" in texts
|
|
assert "vip_pass" in texts, texts
|
|
assert "lvip_pass" not in texts, "the tail of the ASCII literal was glued onto the next run"
|
|
assert texts.count("vip_pass") == 1
|
|
|
|
|
|
def test_a_two_byte_literal_landing_at_an_odd_offset_is_recovered(load_script):
|
|
"""A two-byte run at an odd offset is found, and the run before it does not manufacture one.
|
|
|
|
Two errors lived here. The scan stepped through the two-byte form from the section base, so a
|
|
run shifted to an odd offset -- which one odd-length literal is enough to do -- was never
|
|
examined and was missed whole. Advancing one byte at a time fixes that, and the second half of
|
|
this test guards the error it introduces: the tail of an even-length one-byte literal is a
|
|
printable byte followed by alignment, which reads as the start of a two-byte run and yields a
|
|
literal that does not exist.
|
|
"""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
ascii_part = b"even_phase_liter\x00"
|
|
assert len(ascii_part) % 2 == 1, "the two-byte run needs to start at an odd offset"
|
|
data = ascii_part + "vip_pass".encode("utf-16-le") + b"\x00\x00"
|
|
recovered = dss.recover_strings(data, {"offset": 0, "vaddr": 0x1000, "size": len(data)})
|
|
texts = [row[2] for row in recovered]
|
|
assert "vip_pass" in texts, texts
|
|
assert "even_phase_liter" in texts, texts
|
|
|
|
# No phantom literal: a run is reported once, from its own first byte.
|
|
assert texts.count("vip_pass") == 1
|
|
two_byte = [row for row in recovered if row[3] == "two-byte"]
|
|
assert len(two_byte) == 1, two_byte
|
|
|
|
# An even-length one-byte literal before a two-byte run must not start a phantom run.
|
|
data2 = b"even_phase_lite\x00" + "vip_pass".encode("utf-16-le") + b"\x00\x00"
|
|
got2 = [row[2] for row in dss.recover_strings(
|
|
data2, {"offset": 0, "vaddr": 0x1000, "size": len(data2)})]
|
|
assert "vip_pass" in got2, got2
|
|
assert "vip_pass"[0] not in [t for t in got2 if t != "vip_pass"], got2
|
|
|
|
|
|
def test_min_length_drops_the_shorter_literals(load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
rodata = dss.Elf(synthetic_elf.data).section(".rodata")
|
|
long_only = dss.recover_strings(synthetic_elf.data, rodata, min_length=12)
|
|
assert [row[2] for row in long_only] == ["premium_license"]
|
|
everything = dss.recover_strings(synthetic_elf.data, rodata, min_length=6)
|
|
assert {row[2] for row in everything} == {"premium_license", "trial_mode", "vip_pass"}
|
|
|
|
|
|
def test_the_limit_caps_how_many_literals_come_back(load_script, synthetic_elf):
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
rodata = dss.Elf(synthetic_elf.data).section(".rodata")
|
|
assert len(dss.recover_strings(synthetic_elf.data, rodata, limit=1)) == 1
|
|
assert len(dss.recover_strings(synthetic_elf.data, rodata, limit=2)) == 2
|
|
|
|
|
|
def test_a_section_claiming_more_bytes_than_the_file_holds_is_clamped(load_script, synthetic_elf):
|
|
"""A truncated image must not read past the end; the reader clamps to the data it has."""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
truncated = synthetic_elf.data[:synthetic_elf.rodata["offset"] + 0x10]
|
|
rodata = dict(synthetic_elf.rodata, size=synthetic_elf.rodata["size"] + 0x1000)
|
|
recovered = dss.recover_strings(truncated, rodata)
|
|
assert [row[2] for row in recovered] == ["premium_license"]
|
|
|
|
|
|
def test_the_code_section_yields_no_literals(load_script, synthetic_elf):
|
|
"""The fixture's `.text` is sixteen bytes of instructions, so nothing survives the length gate."""
|
|
dss = load_script("dart_snapshot_strings.py")
|
|
text = dss.Elf(synthetic_elf.data).section(".text")
|
|
assert dss.recover_strings(synthetic_elf.data, text) == []
|
|
|
|
|
|
# --- pool observation --------------------------------------------------------
|