Map REDscript native registration targets
This commit is contained in:
Executable
+174
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Map REDscript/native registration strings to nearby code pointer candidates."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from find_pe_string_xrefs import Section, parse_pe
|
||||||
|
|
||||||
|
|
||||||
|
def section_for_rva(sections: list[Section], rva: int) -> Section | None:
|
||||||
|
for section in sections:
|
||||||
|
if section.contains_rva(rva):
|
||||||
|
return section
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_readable_data(section: Section) -> bool:
|
||||||
|
return bool(section.characteristics & 0x40000000) and not section.is_code
|
||||||
|
|
||||||
|
|
||||||
|
def iter_ascii_strings(data: bytes, section: Section, min_len: int) -> list[tuple[int, str]]:
|
||||||
|
start = section.raw_offset
|
||||||
|
end = min(len(data), section.raw_offset + section.raw_size)
|
||||||
|
pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_len)
|
||||||
|
return [
|
||||||
|
(section.virtual_address + match.start(), match.group(0).decode("ascii", "replace"))
|
||||||
|
for match in pattern.finditer(data[start:end])
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def read_i32(data: bytes, offset: int) -> int | None:
|
||||||
|
if offset < 0 or offset + 4 > len(data):
|
||||||
|
return None
|
||||||
|
return struct.unpack_from("<i", data, offset)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def find_rip_disp_xrefs(data: bytes, sections: list[Section], image_base: int, target_rvas: set[int]) -> dict[int, list[int]]:
|
||||||
|
target_vas = {image_base + rva: rva for rva in target_rvas}
|
||||||
|
hits = {rva: [] for rva in target_rvas}
|
||||||
|
for section in sections:
|
||||||
|
if not section.is_code:
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = section.raw_offset
|
||||||
|
end = min(len(data), section.raw_offset + section.raw_size)
|
||||||
|
for offset in range(start, max(start, end - 4)):
|
||||||
|
disp = read_i32(data, offset)
|
||||||
|
if disp is None:
|
||||||
|
continue
|
||||||
|
instr_end_va = image_base + section.virtual_address + (offset - section.raw_offset) + 4
|
||||||
|
target_rva = target_vas.get(instr_end_va + disp)
|
||||||
|
if target_rva is not None:
|
||||||
|
hits[target_rva].append(section.virtual_address + (offset - section.raw_offset))
|
||||||
|
return hits
|
||||||
|
|
||||||
|
|
||||||
|
def iter_nearby_rip_code_targets(
|
||||||
|
data: bytes,
|
||||||
|
sections: list[Section],
|
||||||
|
image_base: int,
|
||||||
|
code_section: Section,
|
||||||
|
center_disp_rva: int,
|
||||||
|
radius: int,
|
||||||
|
) -> list[tuple[int, int, str]]:
|
||||||
|
# Common x64 RIP-relative LEA/MOV forms used by the generated registration
|
||||||
|
# thunks to pass function pointers and type descriptors around.
|
||||||
|
patterns: tuple[tuple[bytes, int, str], ...] = (
|
||||||
|
(b"\x48\x8d\x05", 3, "lea rax"),
|
||||||
|
(b"\x48\x8d\x0d", 3, "lea rcx"),
|
||||||
|
(b"\x48\x8d\x15", 3, "lea rdx"),
|
||||||
|
(b"\x48\x8d\x1d", 3, "lea rbx"),
|
||||||
|
(b"\x48\x8d\x35", 3, "lea rsi"),
|
||||||
|
(b"\x48\x8d\x3d", 3, "lea rdi"),
|
||||||
|
(b"\x4c\x8d\x05", 3, "lea r8"),
|
||||||
|
(b"\x4c\x8d\x0d", 3, "lea r9"),
|
||||||
|
(b"\x4c\x8d\x15", 3, "lea r10"),
|
||||||
|
(b"\x4c\x8d\x1d", 3, "lea r11"),
|
||||||
|
(b"\x4c\x8d\x25", 3, "lea r12"),
|
||||||
|
(b"\x4c\x8d\x2d", 3, "lea r13"),
|
||||||
|
(b"\x4c\x8d\x35", 3, "lea r14"),
|
||||||
|
(b"\x4c\x8d\x3d", 3, "lea r15"),
|
||||||
|
)
|
||||||
|
|
||||||
|
center_off = code_section.rva_to_offset(center_disp_rva)
|
||||||
|
start = max(code_section.raw_offset, center_off - radius)
|
||||||
|
end = min(len(data), code_section.raw_offset + code_section.raw_size, center_off + radius)
|
||||||
|
out: list[tuple[int, int, str]] = []
|
||||||
|
|
||||||
|
for offset in range(start, end):
|
||||||
|
for prefix, disp_start, label in patterns:
|
||||||
|
if data[offset : offset + len(prefix)] != prefix:
|
||||||
|
continue
|
||||||
|
|
||||||
|
disp = read_i32(data, offset + disp_start)
|
||||||
|
if disp is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
instr_rva = code_section.virtual_address + (offset - code_section.raw_offset)
|
||||||
|
instr_end_va = image_base + instr_rva + disp_start + 4
|
||||||
|
target_rva = instr_end_va + disp - image_base
|
||||||
|
target_section = section_for_rva(sections, target_rva)
|
||||||
|
if target_section and target_section.is_code:
|
||||||
|
out.append((instr_rva, target_rva, label))
|
||||||
|
|
||||||
|
return sorted(set(out))
|
||||||
|
|
||||||
|
|
||||||
|
def likely_registered_code(ref_rva: int, candidates: list[tuple[int, int, str]]) -> tuple[int, int, str] | None:
|
||||||
|
for candidate in candidates:
|
||||||
|
instr_rva, _target_rva, label = candidate
|
||||||
|
if instr_rva > ref_rva and label == "lea rax":
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("pe", type=Path)
|
||||||
|
parser.add_argument("--pattern", required=True, help="case-insensitive regex for registration strings")
|
||||||
|
parser.add_argument("--min-len", type=int, default=5)
|
||||||
|
parser.add_argument("--radius", type=int, default=192)
|
||||||
|
parser.add_argument("--max-strings", type=int, default=200)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
data = args.pe.read_bytes()
|
||||||
|
image_base, sections = parse_pe(data)
|
||||||
|
regex = re.compile(args.pattern, re.IGNORECASE)
|
||||||
|
|
||||||
|
strings: list[tuple[int, str]] = []
|
||||||
|
for section in sections:
|
||||||
|
if is_readable_data(section):
|
||||||
|
strings.extend((rva, text) for rva, text in iter_ascii_strings(data, section, args.min_len) if regex.search(text))
|
||||||
|
strings = sorted(strings, key=lambda item: (item[1].lower(), item[0]))
|
||||||
|
|
||||||
|
xrefs = find_rip_disp_xrefs(data, sections, image_base, {rva for rva, _ in strings})
|
||||||
|
print(f"image_base=0x{image_base:x}")
|
||||||
|
print(f"matches={len(strings)} pattern={args.pattern!r}")
|
||||||
|
|
||||||
|
emitted = 0
|
||||||
|
for string_rva, text in strings:
|
||||||
|
refs = xrefs.get(string_rva, [])
|
||||||
|
if not refs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"name={text!r} string_rva=0x{string_rva:x} xrefs={len(refs)}")
|
||||||
|
for ref in refs[:12]:
|
||||||
|
code_section = section_for_rva(sections, ref)
|
||||||
|
if not code_section:
|
||||||
|
continue
|
||||||
|
candidates = iter_nearby_rip_code_targets(data, sections, image_base, code_section, ref, args.radius)
|
||||||
|
print(f" xref_disp_rva=0x{ref:x} nearby_code_ptrs={len(candidates)}")
|
||||||
|
likely = likely_registered_code(ref, candidates)
|
||||||
|
if likely:
|
||||||
|
instr_rva, target_rva, label = likely
|
||||||
|
print(f" likely_registered_code {label} instr_rva=0x{instr_rva:x} target_rva=0x{target_rva:x}")
|
||||||
|
for instr_rva, target_rva, label in candidates[:16]:
|
||||||
|
print(f" {label} instr_rva=0x{instr_rva:x} target_rva=0x{target_rva:x}")
|
||||||
|
if len(candidates) > 16:
|
||||||
|
print(f" ... +{len(candidates) - 16}")
|
||||||
|
|
||||||
|
emitted += 1
|
||||||
|
if emitted >= args.max_strings:
|
||||||
|
break
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user