Files
UmaViewer/tools/inspect_live_native_calls.py

86 lines
3.5 KiB
Python

"""Bounded native call inventory for the already recovered local viewer binary.
Never execute target code. Reads the recovered PE and API address list only.
"""
from __future__ import annotations
import argparse
import collections
import hashlib
import pathlib
import re
import capstone
import pefile
BASE = pathlib.Path(r"D:\Projects\UmaTools\UmaViewer-master5\tmp")
API = BASE / "render-pipeline-api.txt"
PE = BASE / "target-gameassembly-unpacked.dll"
ORIGINAL = pathlib.Path(r"D:\Projects\UmaTools\UmaViewer\GameAssembly.dll")
EXPECTED_ORIGINAL_SHA256 = "d678a0713257af45a6cd499854a7c8a80cb5999f5cb79e320fd12e315b28b78d"
EXPECTED_RECOVERED_SHA256 = "e273b4f499e4449bbabc361c03c91f260178bf3e687825aa3abe6e686664569b"
def verify_inputs():
for path, expected in ((ORIGINAL, EXPECTED_ORIGINAL_SHA256),
(PE, EXPECTED_RECOVERED_SHA256)):
digest = hashlib.sha256(path.read_bytes()).hexdigest()
if digest != expected:
raise ValueError(f"Binary version mismatch: {path} (SHA256 {digest})")
def symbols():
table = collections.defaultdict(list)
for line in API.read_text(encoding="utf-8").splitlines():
match = re.match(r"METHOD\|([^|]+)\|(.+?)\|ptr=(0x[0-9a-fA-F]+)\|rva=(0x[0-9a-fA-F]+)\|", line)
if match:
table[int(match[3], 16)].append(match[1] + "." + match[2])
return table
def inspect(type_name: str, method_name: str, byte_limit: int):
verify_inputs()
table = symbols()
wanted = type_name + "." + method_name
candidates = [(address, names) for address, names in table.items() if wanted in names]
if len(candidates) != 1:
raise ValueError(f"Expected one method pointer for {wanted}; found {len(candidates)}")
address = candidates[0][0]
pe = pefile.PE(str(PE), fast_load=True)
rva = address - pe.OPTIONAL_HEADER.ImageBase
next_address = min((ptr for ptr in table if ptr > address), default=address + byte_limit)
size = min(byte_limit, next_address - address)
if size <= 0 or size > 65536:
raise ValueError("Unreasonable function extent")
offset = pe.get_offset_from_rva(rva)
data = PE.read_bytes()[offset:offset + size]
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
md.detail = True
calls = []
branches = []
for instruction in md.disasm(data, address):
if instruction.mnemonic in ("call", "jmp") and len(instruction.operands) == 1:
op = instruction.operands[0]
if op.type == capstone.x86.X86_OP_IMM:
destination = op.imm
label = ";".join(table.get(destination, ("unmapped",)))
(calls if instruction.mnemonic == "call" else branches).append(
{"at": hex(instruction.address), "target": hex(destination), "name": label})
return {"method": wanted, "start": hex(address), "sizeLimit": size,
"calls": calls, "jumps": branches}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("type")
parser.add_argument("method")
parser.add_argument("--bytes", type=int, default=16384)
args = parser.parse_args()
result = inspect(args.type, args.method, args.bytes)
print(f"{result['method']} @ {result['start']} capped {result['sizeLimit']} bytes")
for call in result["calls"]:
print(f"{call['at']} -> {call['target']} {call['name']}")
print(f"calls={len(result['calls'])} jumps={len(result['jumps'])}")
if __name__ == "__main__":
main()