ScrapHacks/rz_analyze.py
Daniel Seiller 7afdfb5869 Lots of changes, expand to read
- Add notes folder with MDBook documentation (the NOTES.md file was getting kind of large)
- Add rz_analyze.py, does the same a r2_analyze.py just with Rizin instead of radare2 so the project can be loaded in Cutter (*and* it's faster)
- Add Scrap.rzdb, Rizin database for the Scrap.exe executable
- Add Scrapper_rs, Rust version of .packed extractor and repacker
- replace helplib.txt with helplib.md
- add Py_Docs folder which contains generated documentation for the binary python modules built into Scrap.exe
2021-01-20 23:53:14 +01:00

343 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
)