forked from ReScrap/ScrapHacks
344 lines
10 KiB
Python
344 lines
10 KiB
Python
|
import rzpipe
|
||
|
import os
|
||
|
import json
|
||
|
from datetime import datetime
|
||
|
import subprocess as SP
|
||
|
from tqdm import tqdm
|
||
|
import sys
|
||
|
import yaml
|
||
|
|
||
|
tqdm_ascii = False
|
||
|
|
||
|
rzcmds = []
|
||
|
x64_dbg_script = []
|
||
|
script_path = os.path.dirname(os.path.abspath(__file__))
|
||
|
scrap_exe = os.path.abspath(sys.argv[1])
|
||
|
scrapland_folder = os.path.abspath(os.path.dirname(scrap_exe))
|
||
|
rz_script_path = os.path.join(scrapland_folder, "scrap_dissect.rz")
|
||
|
x64_dbg_script_path = os.path.join(scrapland_folder, "scrap_dissect.x32dbg.txt")
|
||
|
json_path = os.path.join(scrapland_folder, "scrap_dissect.json")
|
||
|
|
||
|
assert os.path.isfile(scrap_exe), "File not found!"
|
||
|
rz = rzpipe.open(scrap_exe)
|
||
|
file_hashes = rz.cmdj("itj")
|
||
|
target_hashes = {
|
||
|
"sha1": "d2dde960e8eca69d60c2e39a439088b75f0c89fa",
|
||
|
"md5": "a934c85dca5ab1c32f05c0977f62e186",
|
||
|
"sha256": "24ef449322f28f87b702834f1a1aac003f885db6d68757ff29fad3ddba6c7b88",
|
||
|
}
|
||
|
|
||
|
assert file_hashes == target_hashes, "Hash mismatch"
|
||
|
|
||
|
|
||
|
def x64_dbg_label(addr, name, prefix=None):
|
||
|
global x64_dbg_script
|
||
|
if isinstance(addr, int):
|
||
|
addr = hex(addr)
|
||
|
if prefix:
|
||
|
x64_dbg_script.append(f'lbl {addr},"{prefix}.{name}"')
|
||
|
else:
|
||
|
x64_dbg_script.append(f'lbl {addr},"{name}"')
|
||
|
|
||
|
|
||
|
def rz_cmd(cmd):
|
||
|
global rz, rzcmds
|
||
|
rzcmds.append(cmd)
|
||
|
return rz.cmd(cmd)
|
||
|
|
||
|
|
||
|
def rz_cmdj(cmd):
|
||
|
global rz, rzcmds
|
||
|
rzcmds.append(cmd)
|
||
|
return rz.cmdj(cmd)
|
||
|
|
||
|
|
||
|
def rz_cmdJ(cmd):
|
||
|
global rz, rzcmds
|
||
|
rzcmds.append(cmd)
|
||
|
return rz.cmdJ(cmd)
|
||
|
|
||
|
|
||
|
t_start = datetime.today()
|
||
|
|
||
|
|
||
|
def analysis(full=False):
|
||
|
print("[*] Running analysis")
|
||
|
steps = []
|
||
|
if full:
|
||
|
steps = [
|
||
|
"e anal.dataref = true",
|
||
|
# "e anal.esil = true",
|
||
|
"e anal.jmp.after = true",
|
||
|
"e anal.jmp.indir = true",
|
||
|
"e anal.loads = true",
|
||
|
"e anal.pushret = true",
|
||
|
"e anal.refstr = true",
|
||
|
"e anal.strings = true",
|
||
|
"e anal.vinfun = true",
|
||
|
"e asm.anal = true",
|
||
|
]
|
||
|
if full:
|
||
|
steps += ["aaaa"]
|
||
|
else:
|
||
|
steps += ["aaa"]
|
||
|
for ac in steps:
|
||
|
print(f"[*] Running '{ac}'")
|
||
|
rz_cmd(f"{ac} 2>NUL")
|
||
|
|
||
|
|
||
|
with open(os.path.join(script_path, "config.yml")) as cfg:
|
||
|
print("[*] Loading config")
|
||
|
config = type("Config", (object,), yaml.load(cfg, Loader=yaml.SafeLoader))
|
||
|
|
||
|
for line in config.script.strip().splitlines():
|
||
|
rz_cmd(line)
|
||
|
|
||
|
analysis(False)
|
||
|
|
||
|
for addr, comment in config.comments.items():
|
||
|
rz_cmd(f"CC {comment} @ {hex(addr)}")
|
||
|
|
||
|
for t in config.types:
|
||
|
rz_cmd(f'"td {t}"')
|
||
|
|
||
|
for addr, name in config.flags.items():
|
||
|
x64_dbg_label(addr, name, "loc")
|
||
|
rz_cmd(f"f loc.{name} 4 {hex(addr)}")
|
||
|
|
||
|
|
||
|
for addr, func in config.functions.items():
|
||
|
name, sig = func.get("name"), func.get("signature")
|
||
|
if name:
|
||
|
x64_dbg_label(addr, name, "fcn")
|
||
|
rz_cmd(f"afr fcn.{name} {hex(addr)}")
|
||
|
rz_cmd(f"afn fcn.{name} {hex(addr)}")
|
||
|
if sig:
|
||
|
sig = sig.replace(name, "fcn." + name)
|
||
|
rz_cmd(f'"afs {sig}" @{hex(addr)}')
|
||
|
|
||
|
|
||
|
def vtables():
|
||
|
ret = {}
|
||
|
print("[*] Analyzing VTables")
|
||
|
vtables = rz_cmdJ("avj")
|
||
|
for c in tqdm(vtables, ascii=tqdm_ascii):
|
||
|
methods = []
|
||
|
name = config.VMTs.get(c.offset, f"{c.offset:08x}")
|
||
|
x64_dbg_label(c.offset, name, "vmt")
|
||
|
rz_cmd(f"f vmt.{name} 4 {hex(c.offset)}")
|
||
|
for idx, m in enumerate(tqdm(c.methods, ascii=tqdm_ascii, leave=False)):
|
||
|
methods.append(hex(m.offset))
|
||
|
x64_dbg_label(m.offset, f"{name}.{idx}", "fcn.vmt")
|
||
|
rz_cmd(f"afr fcn.vmt.{name}.{idx} {hex(m.offset)} 2>NUL")
|
||
|
ret[hex(c.offset)] = methods
|
||
|
return ret
|
||
|
|
||
|
|
||
|
def c_callbacks():
|
||
|
print("[*] Parsing C Callbacks")
|
||
|
funcs = {}
|
||
|
res = rz_cmd("/r fcn.register_c_callback ~CALL[1]").splitlines()
|
||
|
for addr in tqdm(res, ascii=tqdm_ascii):
|
||
|
rz_cmd(f"s {addr}")
|
||
|
rz_cmd(f"so -3")
|
||
|
func, name = rz_cmdJ(f"pdj 2")
|
||
|
func = func.refs[0].addr
|
||
|
name = rz_cmd(f"psz @{hex(name.refs[0].addr)}").strip()
|
||
|
rz_cmd(f"afr fcn.callbacks.{name} {hex(func)} 2>NUL")
|
||
|
x64_dbg_label(func, f"{name}", "fcn.callbacks")
|
||
|
funcs[name] = hex(func)
|
||
|
return funcs
|
||
|
|
||
|
|
||
|
def assertions():
|
||
|
assertions = {}
|
||
|
for (n_args, a_addr) in [
|
||
|
(3, "fcn.throw_assertion_1"),
|
||
|
(4, "fcn.throw_assertion_2"),
|
||
|
]:
|
||
|
print(f"[*] Parsing C assertions for {a_addr}")
|
||
|
res = rz_cmd(f"/r {a_addr} ~CALL[1]").splitlines()
|
||
|
print()
|
||
|
for line in tqdm(res, ascii=tqdm_ascii):
|
||
|
addr = line.strip()
|
||
|
rz_cmd(f"s {addr}")
|
||
|
rz_cmd(f"so -{n_args}")
|
||
|
dis = rz_cmdJ(f"pij {n_args}")
|
||
|
if n_args == 4:
|
||
|
line, date, file, msg = dis
|
||
|
elif n_args == 3:
|
||
|
date = None
|
||
|
line, file, msg = dis
|
||
|
try:
|
||
|
file = rz_cmd(f"psz @{file.refs[0].addr}").strip()
|
||
|
msg = rz_cmd(f"psz @{msg.refs[0].addr}").strip()
|
||
|
if date:
|
||
|
date = rz_cmd(f"psz @{date.refs[0].addr}").strip()
|
||
|
line = line.val
|
||
|
file = file.replace("\\\\", "\\")
|
||
|
assertions.setdefault(file, [])
|
||
|
assertions[file].append(
|
||
|
{"line": line, "date": date, "addr": addr, "msg": msg}
|
||
|
)
|
||
|
except:
|
||
|
pass
|
||
|
for path in assertions:
|
||
|
assertions[path].sort(key=lambda v: v["line"])
|
||
|
return assertions
|
||
|
|
||
|
|
||
|
def bb_refs(addr):
|
||
|
ret = {}
|
||
|
res = rz_cmd(f"/r {addr} ~fcn[0,1]").splitlines()
|
||
|
print()
|
||
|
for ent in tqdm(res, ascii=tqdm_ascii):
|
||
|
func, hit = ent.split()
|
||
|
ret[hit] = {"asm": [], "func": func}
|
||
|
for ins in rz_cmdJ(f"pdbj @{hit}"):
|
||
|
ret[hit]["asm"].append(ins.disasm)
|
||
|
return ret
|
||
|
|
||
|
|
||
|
def world():
|
||
|
print("[*] Parsing World offsets")
|
||
|
return bb_refs("loc.P_World")
|
||
|
|
||
|
|
||
|
def render():
|
||
|
print("[*] Parsing D3D_Device offsets")
|
||
|
return bb_refs("loc.P_D3D8_Dev")
|
||
|
|
||
|
|
||
|
def py_mods():
|
||
|
print("[*] Parsing Python modules")
|
||
|
res = rz_cmd("/r fcn.Py_InitModule ~CALL[1]").splitlines()
|
||
|
print()
|
||
|
py_mods = {}
|
||
|
for call_loc in tqdm(res, ascii=tqdm_ascii):
|
||
|
rz_cmd(f"s {call_loc}")
|
||
|
rz_cmd(f"so -3")
|
||
|
args = rz_cmdJ("pdj 3")
|
||
|
refs = []
|
||
|
if not all(arg.type == "push" for arg in args):
|
||
|
continue
|
||
|
for arg in args:
|
||
|
refs.append(hex(arg.val))
|
||
|
doc, methods, name = refs
|
||
|
doc = rz_cmd(f"psz @{doc}").strip()
|
||
|
name = rz_cmd(f"psz @{name}").strip()
|
||
|
rz_cmd(f"s {methods}")
|
||
|
rz_cmd(f"f py.{name} 4 {methods}")
|
||
|
x64_dbg_label(methods, f"{name}", "py")
|
||
|
py_mods[name] = {"methods_addr": methods, "doc": doc, "methods": {}}
|
||
|
while True:
|
||
|
m_name, m_func, _, m_doc = [v.value for v in rz_cmdJ(f"pfj xxxx")]
|
||
|
if m_name == 0:
|
||
|
break
|
||
|
m_name, m_func, m_doc = map(hex, (m_name, m_func, m_doc))
|
||
|
m_name = rz_cmd(f"psz @{m_name}").strip()
|
||
|
rz_cmd(f"f py.{name}.{m_name}.__doc__ 4 {m_doc}")
|
||
|
if int(m_doc, 16) != 0:
|
||
|
x64_dbg_label(m_doc, f"{name}.{m_name}.__doc__", "py")
|
||
|
m_doc = rz_cmd(f"psz @{m_doc}").strip()
|
||
|
else:
|
||
|
m_doc = None
|
||
|
py_mods[name]["methods"][m_name] = {"addr": m_func, "doc": m_doc}
|
||
|
rz_cmd(f"afr py.{name}.{m_name} {m_func} 2>NUL")
|
||
|
x64_dbg_label(m_func, f"{name}.{m_name}", "fcn.py")
|
||
|
rz_cmd("s +16")
|
||
|
return py_mods
|
||
|
|
||
|
|
||
|
def game_vars():
|
||
|
ret = {}
|
||
|
print("[*] Parsing Game variables")
|
||
|
res = rz_cmd("/r fcn.setup_game_vars ~CALL[1]").splitlines()
|
||
|
print()
|
||
|
for line in tqdm(res, ascii=tqdm_ascii):
|
||
|
addr = line.strip()
|
||
|
rz_cmd(f"s {addr}")
|
||
|
args = rz_cmd("pdj -5") # seek and print disassembly
|
||
|
if not args:
|
||
|
continue
|
||
|
args = json.loads(args)
|
||
|
args_a = []
|
||
|
push_cnt = 0
|
||
|
for arg in args[::-1]:
|
||
|
if arg["type"] not in ["push", "mov"]:
|
||
|
continue
|
||
|
if arg["type"] == "push":
|
||
|
push_cnt += 1
|
||
|
args_a.append(arg)
|
||
|
if push_cnt == 3:
|
||
|
break
|
||
|
if len(args_a) != 4:
|
||
|
continue
|
||
|
if not all("val" in v for v in args_a):
|
||
|
continue
|
||
|
addr, name, _, desc = [v["val"] for v in args_a]
|
||
|
name = rz_cmd(f"psz @{hex(name)}").strip()
|
||
|
desc = rz_cmd(f"psz @{hex(desc)}").strip()
|
||
|
addr = hex(addr)
|
||
|
rz_cmd(f"f loc.gvar.{name} 4 {addr}")
|
||
|
x64_dbg_label(addr, f"{name}", "loc.gvar")
|
||
|
ret[addr] = {"name": name, "desc": desc}
|
||
|
return ret
|
||
|
|
||
|
|
||
|
ret = {}
|
||
|
# world, render
|
||
|
for func in ["game_vars", "c_callbacks", "py_mods", "assertions", "vtables"]:
|
||
|
ret[func] = globals()[func]()
|
||
|
|
||
|
analysis(True)
|
||
|
|
||
|
with open(json_path, "w") as of:
|
||
|
json.dump(ret, of, indent=4)
|
||
|
|
||
|
print("[+] Wrote scrap_dissect.json")
|
||
|
|
||
|
with open(x64_dbg_script_path, "w") as of:
|
||
|
of.write("\n".join(x64_dbg_script))
|
||
|
|
||
|
print("[+] Wrote scrap_dissect.x32dbg.txt")
|
||
|
|
||
|
with open(rz_script_path, "w") as of:
|
||
|
wcmds = []
|
||
|
for cmd in rzcmds:
|
||
|
if cmd == "avj":
|
||
|
continue
|
||
|
record = True
|
||
|
for start in ["p", "/", "s"]:
|
||
|
if cmd.strip('"').startswith(start):
|
||
|
record = False
|
||
|
if record:
|
||
|
wcmds.append(cmd)
|
||
|
of.write("\n".join(wcmds))
|
||
|
|
||
|
print("[+] Wrote scrap_dissect.rz")
|
||
|
|
||
|
|
||
|
def start_program(cmdl, **kwargs):
|
||
|
if os.name == "nt":
|
||
|
return SP.Popen(["cmd", "/c", "start"] + cmdl, **kwargs)
|
||
|
else:
|
||
|
return SP.Popen(cmdl, **kwargs)
|
||
|
|
||
|
|
||
|
print("[+] Analysis took:", datetime.today() - t_start)
|
||
|
rz.cmd("Ps Scrap.rzdb")
|
||
|
exit()
|
||
|
|
||
|
print("[+] Executing Cutter")
|
||
|
try:
|
||
|
start_program(
|
||
|
["cutter", "-A", "0", "-i", rz_script_path, scrap_exe],
|
||
|
cwd=scrapland_folder,
|
||
|
shell=False,
|
||
|
)
|
||
|
except FileNotFoundError:
|
||
|
print("[-] cutter not installed, falling back to rz")
|
||
|
start_program(
|
||
|
["rz", "-i", rz_script_path, scrap_exe], cwd=scrapland_folder, shell=False
|
||
|
)
|