mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
164 lines
6.2 KiB
Python
164 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Locate metadata-usage tokens / pointers inside an il2cpp binary.
|
|
|
|
On metadata v27 the compiled code does not carry literal indices: a string
|
|
literal is reached through a metadata-usage value `(type << 29) | (index << 1) | 1`
|
|
(type 5 = string literal). That value shows up in the image either as an
|
|
immediate operand in code or as the pre-loader content of a usage slot, so a
|
|
plain byte scan is enough to find the referencing function - parse #29705's
|
|
token with tools/sep-literals.py, scan here, then read the enclosing method
|
|
from the runtime address map.
|
|
|
|
Usage:
|
|
python tools/sep-scan.py --binary <GameAssembly.dll> --token 0xA000E813[,..]
|
|
python tools/sep-scan.py --binary <GameAssembly.dll> --pointer 0x1891B46E8[,..]
|
|
python tools/sep-scan.py --binary <GameAssembly.dll> --map <radicade-addr-map.txt> --token ...
|
|
|
|
Options:
|
|
--section .text restrict the scan (repeatable; default: every section)
|
|
--context N extra bytes of hex context to print (default 16)
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import struct
|
|
import sys
|
|
|
|
METADATA_USAGE_STRING_LITERAL = 5
|
|
|
|
|
|
def literal_token(index):
|
|
return (METADATA_USAGE_STRING_LITERAL << 29) | (index << 1) | 1
|
|
|
|
|
|
def parse_number(text):
|
|
return int(text, 0)
|
|
|
|
|
|
def read_sections(blob):
|
|
pe_offset = struct.unpack_from("<I", blob, 0x3C)[0]
|
|
if blob[pe_offset : pe_offset + 4] != b"PE\0\0":
|
|
raise SystemExit("not a PE image")
|
|
machine, section_count = struct.unpack_from("<HH", blob, pe_offset + 4 + 2)
|
|
optional_size = struct.unpack_from("<H", blob, pe_offset + 4 + 16)[0]
|
|
optional = pe_offset + 4 + 20
|
|
magic = struct.unpack_from("<H", blob, optional)[0]
|
|
if magic == 0x20B:
|
|
image_base = struct.unpack_from("<Q", blob, optional + 24)[0]
|
|
elif magic == 0x10B:
|
|
image_base = struct.unpack_from("<I", blob, optional + 28)[0]
|
|
else:
|
|
raise SystemExit(f"unknown optional header magic {magic:#x}")
|
|
sections = []
|
|
header = optional + optional_size
|
|
for i in range(section_count):
|
|
entry = header + 40 * i
|
|
name = blob[entry : entry + 8].split(b"\0")[0].decode("ascii", "replace")
|
|
virtual_size, virtual_address, raw_size, raw_offset = struct.unpack_from("<IIII", blob, entry + 8)
|
|
sections.append((name, virtual_address, virtual_size, raw_offset, raw_size))
|
|
return image_base, machine, sections
|
|
|
|
|
|
def load_map(path, image_base):
|
|
entries = []
|
|
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
for line in handle:
|
|
if line.startswith("#"):
|
|
continue
|
|
parts = line.rstrip("\r\n").split("\t")
|
|
if len(parts) < 3:
|
|
continue
|
|
try:
|
|
rva = int(parts[0], 16)
|
|
except ValueError:
|
|
continue
|
|
entries.append((rva, parts[1], parts[2]))
|
|
entries.sort()
|
|
return entries
|
|
|
|
|
|
def enclosing(entries, rva, image_base):
|
|
lo, hi = 0, len(entries) - 1
|
|
best = None
|
|
while lo <= hi:
|
|
mid = (lo + hi) // 2
|
|
if entries[mid][0] <= rva:
|
|
best = entries[mid]
|
|
lo = mid + 1
|
|
else:
|
|
hi = mid - 1
|
|
if best is None:
|
|
return None
|
|
return best, rva - best[0]
|
|
|
|
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(add_help=True)
|
|
parser.add_argument("--binary", required=True)
|
|
parser.add_argument("--map")
|
|
parser.add_argument("--token", action="append", default=[])
|
|
parser.add_argument("--pointer", action="append", default=[])
|
|
parser.add_argument("--literal", action="append", type=int, default=[])
|
|
parser.add_argument("--section", action="append", default=[])
|
|
parser.add_argument("--context", type=int, default=16)
|
|
args = parser.parse_args(argv[1:])
|
|
|
|
patterns = []
|
|
labels = []
|
|
for value in args.literal:
|
|
patterns.append(struct.pack("<I", literal_token(value)))
|
|
labels.append(f"literal #{value} token 0x{literal_token(value):X} (4-byte)")
|
|
for group in args.token:
|
|
for value in group.split(","):
|
|
number = parse_number(value)
|
|
patterns.append(struct.pack("<I", number))
|
|
patterns.append(struct.pack("<Q", number))
|
|
labels.append(f"token {number:#x}")
|
|
labels.append(f"token {number:#x} (8-byte)")
|
|
for group in args.pointer:
|
|
for value in group.split(","):
|
|
number = parse_number(value)
|
|
patterns.append(struct.pack("<Q", number))
|
|
labels.append(f"pointer {number:#x}")
|
|
if not patterns:
|
|
raise SystemExit("nothing to search for: pass --literal/--token/--pointer")
|
|
|
|
blob = open(args.binary, "rb").read()
|
|
image_base, machine, sections = read_sections(blob)
|
|
print(f"binary {os.path.basename(args.binary)}: machine {machine:#x}, image base {image_base:#x}, {len(sections)} sections")
|
|
|
|
entries = load_map(args.map, image_base) if args.map else None
|
|
total = 0
|
|
for index, pattern in enumerate(patterns):
|
|
wanted = [s for s in sections if not args.section or s[0] in args.section]
|
|
for name, virtual_address, virtual_size, raw_offset, raw_size in wanted:
|
|
size = min(raw_size, max(0, len(blob) - raw_offset))
|
|
start = raw_offset
|
|
end = raw_offset + size
|
|
offset = start
|
|
while True:
|
|
offset = blob.find(pattern, offset, end)
|
|
if offset < 0:
|
|
break
|
|
va = image_base + virtual_address + (offset - raw_offset)
|
|
line = f" {labels[index]} @ RVA {virtual_address + (offset - raw_offset):#x} VA {va:#x} ({name})"
|
|
if entries:
|
|
found = enclosing(entries, virtual_address + (offset - raw_offset), image_base)
|
|
if found:
|
|
(rva, module, method), delta = found
|
|
line += f" -> {module} {method} +{delta:#x}"
|
|
print(line)
|
|
context = blob[max(offset - 4, start) : offset + len(pattern) + args.context]
|
|
print(f" ctx {context.hex(' ')}")
|
|
total += 1
|
|
offset += 1
|
|
print(f"{total} occurrence(s)")
|
|
|
|
if entries:
|
|
print("map loaded: %d methods" % len(entries))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|