#!/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())
