forked from ReScrap/ScrapHacks
- Update NOTES with new findings - Add Cutter link to README - Add ASMJIT, ASMTK and Zydis to CMake - Make DX8 setting cofigurable via ScrapHacks REPL - Add scaffolding for build hook trampolines using asmjit - Add on the fly assembling of code to REPL - Clean up command structure - Add memory RWX to REPL - Add stack dumping to REPL - Add Gamevar dumping to REPL - Add hook check to overlay commands (don't work if DX8 not hooked) - Allow nested command definitions for cleaner REPL - AllocConsole() as early as possible - shuffle some code around for cleanup - Add GameVar, PakEntry and HashIndex structures
530 lines
16 KiB
Python
530 lines
16 KiB
Python
import r2pipe
|
|
import os
|
|
import json
|
|
from datetime import datetime
|
|
import subprocess as SP
|
|
from tqdm import tqdm
|
|
from pprint import pprint
|
|
import os
|
|
import sys
|
|
|
|
r2cmds = []
|
|
x64_dbg_script=[]
|
|
scrap_exe = os.path.abspath(sys.argv[1])
|
|
folder = os.path.abspath(os.path.dirname(scrap_exe))
|
|
script_path=os.path.join(folder, "scrap_dissect.r2")
|
|
x64_dbg_script_path=os.path.join(folder, "scrap_dissect.x32dbg.txt")
|
|
json_path=os.path.join(folder, "scrap_dissect.json")
|
|
|
|
assert os.path.isfile(scrap_exe), "File not found!"
|
|
r2 = r2pipe.open(scrap_exe)
|
|
file_hashes = r2.cmdj("itj")
|
|
target_hashes = {
|
|
"sha1": "d2dde960e8eca69d60c2e39a439088b75f0c89fa",
|
|
"md5": "a934c85dca5ab1c32f05c0977f62e186",
|
|
}
|
|
|
|
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 r2_cmd(cmd):
|
|
global r2, r2cmds
|
|
r2cmds.append(cmd)
|
|
return r2.cmd(cmd)
|
|
|
|
|
|
def r2_cmdj(cmd):
|
|
global r2, r2cmds
|
|
r2cmds.append(cmd)
|
|
return r2.cmdj(cmd)
|
|
|
|
|
|
def r2_cmdJ(cmd):
|
|
global r2, r2cmds
|
|
r2cmds.append(cmd)
|
|
return r2.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",
|
|
]
|
|
steps+=["aaaaa"]
|
|
for ac in steps:
|
|
print(f"[*] Running '{ac}'")
|
|
r2_cmd(f"{ac} 2>NUL")
|
|
|
|
# 0x7fac20
|
|
# 0x7fac19
|
|
# 0x7faa4c
|
|
# 0x7fac1c # activate viewer
|
|
# 0x84d400 # lib preloaded
|
|
|
|
# 0x413ee0
|
|
|
|
# 0x7d2094 refcnt
|
|
|
|
comments= {
|
|
0x6113f9:"Check if Window exists"
|
|
}
|
|
|
|
flags = {
|
|
0x7FE944: "P_World",
|
|
0x7FBE4C: "P_Vars",
|
|
0x79C698: "Py_Mods",
|
|
0x852914: "P_D3D8_Dev",
|
|
0x7FCC00: "N_Paks_opened",
|
|
0x7fcbec: "Hash_Index_Size",
|
|
0x7fcbf0: "P_Hash_Index",
|
|
0x7fcc08: "Lst_File",
|
|
0x7fcc04: "Pak_Locked",
|
|
0x7fc1b0: "Pak_Index",
|
|
0x84cb64: "P_ConHandler",
|
|
0x801e10: "num_arrows",
|
|
0x7fac84: "P_Callbacks",
|
|
0x80b2cc: "P_ActClassList",
|
|
0x807a20: "P_Scorer",
|
|
0x80a398: "P_SoundSys",
|
|
0x84cb58: "H_RichEd",
|
|
0x84cb4c: "P_HWND_Console",
|
|
0x80cb40: "Console_Win_Buffer",
|
|
0x84d400: "Lib_preloaded",
|
|
0x7fac1c: "Activate_Viewer",
|
|
0x8b18f0: "P_Models",
|
|
0x8b18f4: "P_Scenes",
|
|
0x8b18f8: "P_ActiveModels",
|
|
0x803bc0: "net_is_server",
|
|
0x8045e4: "net_is_master",
|
|
0x8038a8: "net_is_client",
|
|
0x7fadd8: "is_python",
|
|
0x7fc084: "pak_lock",
|
|
0x7fbe7c: "current_language",
|
|
}
|
|
|
|
VMTs = {
|
|
0x78d4d8: "Py_entity",
|
|
0x78cc6c: "World",
|
|
0x78b680: "FilePak_1",
|
|
0x78b6a4: "FilePak_2",
|
|
0x78b638: "AbstractFile",
|
|
0x78b4d8: "App",
|
|
0x78b480: "Window",
|
|
0x78b5c0: "File",
|
|
0x78b65c: "FileMem",
|
|
0x78b6d0: "IDevice_1",
|
|
0x78b6f4: "IDevice_2",
|
|
0x78b6fc: "IDevice_Kb",
|
|
0x78b720: "IDevice_Mouse",
|
|
0x78b74c: "IDevice_Joy",
|
|
0x7933ac: "3d_Gfx",
|
|
0x7933a0: "NodeFX",
|
|
}
|
|
|
|
types = [
|
|
"struct PyMethodDef { char *ml_name; void *ml_meth; int ml_flags; char *ml_doc;};",
|
|
"struct GameVar { struct GameVar* next; const char* name; const char* desc; uint64_t d_type; void* value; void* def_value; };",
|
|
"struct HT_Entry { void* data; const char* key; struct HT_Entry* next;};",
|
|
"struct PakEntry { unsigned char* filename; bool locked; void* data; uint32_t seek;};",
|
|
"struct HashIndexEntry { uint32_t offset; uint32_t size; uint32_t status; const char* name; struct HashIndexEntry* next; };",
|
|
"struct HashIndex { uint32_t size; struct HashIndexEntry** data; };",
|
|
"struct HashTableEntry { void* data; const char *key; struct HashTableEntry* next; };",
|
|
"struct HashTable { uint32_t size; struct HashTableEntry** data; };",
|
|
]
|
|
|
|
func_sigs = {
|
|
0x5A8390: "int py_exec(const char* script);",
|
|
0x5BB9D0: "int PyArg_ParseTuple(void* PyObj, char* format, ...);",
|
|
0x413ee0: "int dbg_log(const char* fmt,...);",
|
|
0x4134C0: "int write_log(unsigned int color, const char* msg);",
|
|
0x47C1E0: "int ht_hash_ent_list(const char* str);",
|
|
0x404BB0: "int ht_hash_ent(const char* str);",
|
|
0x4016F0: "int reg_get_val(const char* value);",
|
|
0x414280: "int prepare_html_log(const char* filename);",
|
|
0x6597d0: "bool read_ini_entry(void* dest,const char* key, const char* section);",
|
|
0x5A8FB0: "void* Py_InitModule(const char* name,void* methods);",
|
|
0x5E3800: "int fopen_from_pak(const char* filename);",
|
|
0x419950: "int fopen_2(const char* filename);",
|
|
0x41AB50: "int open_pak(const char* filename, int unk_1,void* unk_ptr);",
|
|
0x404460: "int register_c_callback(const char* name,void* func);",
|
|
0x414070: "void throw_assertion_2(const char* check,const char* file,const char* date, unsigned int line);",
|
|
0x5FBC50: "void throw_assertion_1(const char* check,const char* file, unsigned int line);",
|
|
0x5BC140: "static char* convertsimple1(void *arg, char **p_format, void *p_va);",
|
|
0x5E3800: "int32_t fopen_from_pak(const char* filename,const char* mode);",
|
|
0x5a90f0: "void* Py_BuildValue(const char* format, ...);"
|
|
}
|
|
|
|
functions = {
|
|
0x6B1C70: "strcmp",
|
|
0x5BB9D0: "PyArg_ParseTuple",
|
|
0x5DD510: "init_engine_3d",
|
|
0x401180: "create_window",
|
|
0x401240: "create_main_window",
|
|
0x4016F0: "reg_get_val",
|
|
0x4134C0: "write_log",
|
|
0x414280: "prepare_html_log",
|
|
0x418220: "get_version_info",
|
|
0x4137E0: "write_html_log",
|
|
0x402190: "handle_console_input",
|
|
0x5F9520: "handle_render_console_input",
|
|
0x404A50: "find_entity",
|
|
0x47C1E0: "ht_hash_ent_list",
|
|
0x404BB0: "ht_hash_ent",
|
|
0x404460: "register_c_callback",
|
|
0x417470: "load_game",
|
|
0x5E3800: "fopen_from_pak",
|
|
0x5e3500: "fopen",
|
|
0x403370: "init_debug",
|
|
0x401770: "init",
|
|
0x4026D0: "init_py",
|
|
0x405B40: "init_py_sub",
|
|
0x5A8FB0: "Py_InitModule",
|
|
0x41AB50: "open_pak",
|
|
0x5A8390: "py_exec",
|
|
0x414570: "setup_game_vars",
|
|
0x5FBC50: "throw_assertion_1",
|
|
0x414070: "throw_assertion_2",
|
|
0x5F7000: "read_ini",
|
|
0x650F80: "load_sm3",
|
|
0x6665A0: "load_m3d_1",
|
|
0x666900: "load_m3d_2",
|
|
0x479B20: "world_constructor",
|
|
0x479B40: "init_world",
|
|
0x402510: "deinit_world",
|
|
0x479870: "make_world",
|
|
0x602A70: "render_frame",
|
|
0x6B738C: "handle_exception",
|
|
0x5B9E70: "py_getattr",
|
|
0x413ee0: "dbg_log",
|
|
0x5f75e0: "init_d3d",
|
|
0x63a2f0: "gdi_draw_line",
|
|
0x5e3250: "read_stream",
|
|
0x5e3bb0: "read_stream_wrapper",
|
|
0x50b9b0: "init_scorer",
|
|
0x582e10: "init_action_class_list",
|
|
0x528910: "init_sound_sys",
|
|
0x5268d0: "try_init_sound_sys",
|
|
0x404280: "cPyFunction_set_func",
|
|
0x414680: "load_config",
|
|
0x414810: "save_config",
|
|
0x4f42a0: "close_server_socket",
|
|
0x4f4d10: "close_server",
|
|
0x4f48e0: "close_client",
|
|
0x4f4fb0: "is_server",
|
|
0x4f4a10: "is_client",
|
|
0x4fac50: "is_master",
|
|
0x526910: "close_sound_sys",
|
|
0x526520: "shutdown_sound_sys",
|
|
0x5dd700: "close_3d_engine",
|
|
0x5a7320: "close_window",
|
|
0x5dff20: "set_exception_handler",
|
|
0x5a7f20: "get_console_wnd",
|
|
0x5a73a0: "show_console",
|
|
0x666c60: "read_m3d",
|
|
0x417df0: "snprintf",
|
|
0x5fc930: "printf",
|
|
0x6597d0: "read_ini_entry",
|
|
0x5fc0a0: "engine_debug_log",
|
|
0x5a7440: "create_console_window",
|
|
0x6114e0: "setup_window",
|
|
0x404420: "clear_functions",
|
|
0x405ca0: "close_py_subsys",
|
|
0x50bcb0: "close_scorer",
|
|
0x479b20: "close_world",
|
|
0x582e70: "close_action_class",
|
|
0x50b6a0: "get_scorer",
|
|
0x50ea20: "scorer_parse_type",
|
|
0x636580: "list_models",
|
|
0x5a90f0: "Py_BuildValue",
|
|
0x41c5a0: "has_lst_file",
|
|
0x5a8e90: "py_error",
|
|
0x5a9890: "get_module_dict",
|
|
0x5c7bb0: "get_current_thread",
|
|
0x5aa140: "preload_lib",
|
|
0x413c10: "sprintf",
|
|
0x405850: "check_is_python",
|
|
0x47bf90: "setup_ent_list",
|
|
0x474f80: "ent_list_get_set",
|
|
}
|
|
|
|
# 0x853954 ??? some obj ptr
|
|
|
|
# [0x7fbe98]
|
|
|
|
# [0x853954]+0x2a3cc debug flag, checked in 0x006113a0 called from 0x005dd5ea
|
|
cfg="""
|
|
e asm.cmt.right = true
|
|
e cmd.stack = true
|
|
e scr.utf8 = true
|
|
e asm.describe = false
|
|
e graph.cmtright = true
|
|
e cfg.sandbox = false
|
|
e cfg.newtab = true
|
|
e cfg.fortunes.type = tips,fun,creepy,nsfw
|
|
e dbg.status = true
|
|
e pdb.autoload = true
|
|
e emu.str = true
|
|
e asm.flags.offset = true
|
|
""".strip().splitlines()
|
|
for line in cfg:
|
|
r2_cmd(line)
|
|
|
|
analysis(False)
|
|
|
|
for addr,comment in comments.items():
|
|
r2_cmd(f"CC {comment} @ {hex(addr)}")
|
|
|
|
for t in types:
|
|
r2_cmd(f'"td {t}"')
|
|
|
|
for addr, name in flags.items():
|
|
x64_dbg_label(addr,name,"loc")
|
|
r2_cmd(f"f loc.{name} 4 {hex(addr)}")
|
|
|
|
for addr, name in functions.items():
|
|
x64_dbg_label(addr,name,"fcn")
|
|
r2_cmd(f"afr fcn.{name} {hex(addr)}")
|
|
if addr in func_sigs:
|
|
r2_cmd(f'"afs {func_sigs[addr]}" @{hex(addr)}')
|
|
|
|
|
|
def vtables():
|
|
ret = {}
|
|
print("[*] Analyzing VTables")
|
|
vtables = r2_cmdJ("avj")
|
|
for c in tqdm(vtables, ascii=True):
|
|
methods = []
|
|
name=VMTs.get(c.offset,f"{c.offset:08x}")
|
|
x64_dbg_label(c.offset,name,"vmt")
|
|
r2_cmd(f"f vmt.{name} 4 {hex(c.offset)}")
|
|
for idx,m in enumerate(tqdm(c.methods, ascii=True, leave=False)):
|
|
methods.append(hex(m.offset))
|
|
x64_dbg_label(m.offset,f"{name}.{idx}","fcn.vmt")
|
|
r2_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 = r2_cmd("/r fcn.register_c_callback ~CALL[1]").splitlines()
|
|
for addr in tqdm(res, ascii=True):
|
|
r2_cmd(f"s {addr}")
|
|
r2_cmd(f"so -3")
|
|
func, name = r2_cmdJ(f"pdj 2")
|
|
func = func.refs[0].addr
|
|
name = r2_cmd(f"psz @{hex(name.refs[0].addr)}").strip()
|
|
r2_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 [
|
|
(4, "fcn.throw_assertion_1"),
|
|
(3, "fcn.throw_assertion_2"),
|
|
]:
|
|
print(f"[*] Parsing C assertions for {a_addr}")
|
|
res = r2_cmd(f"/r {a_addr} ~CALL[1]").splitlines()
|
|
print()
|
|
for line in tqdm(res, ascii=True):
|
|
addr = line.strip()
|
|
r2_cmd(f"s {addr}")
|
|
r2_cmd(f"so -{n_args}")
|
|
dis=r2_cmdJ(f"pij {n_args}")
|
|
if n_args == 4:
|
|
file, msg, date, line = dis
|
|
elif n_args == 3:
|
|
date = None
|
|
file, msg, line = dis
|
|
try:
|
|
file = r2_cmd(f"psz @{file.refs[0].addr}").strip()
|
|
msg = r2_cmd(f"psz @{msg.refs[0].addr}").strip()
|
|
if date:
|
|
date = r2_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 = r2_cmd(f"/r {addr} ~fcn[0,1]").splitlines()
|
|
print()
|
|
for ent in res:
|
|
func, hit = ent.split()
|
|
ret[hit] = {"asm": [], "func": func}
|
|
for ins in r2_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 = r2_cmd("/r fcn.Py_InitModule ~CALL[1]").splitlines()
|
|
print()
|
|
py_mods = {}
|
|
for call_loc in tqdm(res, ascii=True):
|
|
r2_cmd(f"s {call_loc}")
|
|
r2_cmd(f"so -3")
|
|
args = r2_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 = r2_cmd(f"psz @{doc}").strip()
|
|
name = r2_cmd(f"psz @{name}").strip()
|
|
r2_cmd(f"s {methods}")
|
|
r2_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 r2_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 = r2_cmd(f"psz @{m_name}").strip()
|
|
r2_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 = r2_cmd(f"psz @{m_doc}").strip()
|
|
else:
|
|
m_doc=None
|
|
py_mods[name]["methods"][m_name] = {"addr": m_func, "doc": m_doc}
|
|
r2_cmd(f"afr py.{name}.{m_name} {m_func} 2>NUL")
|
|
x64_dbg_label(m_func,f"{name}.{m_name}","fcn.py")
|
|
r2_cmd("s +16")
|
|
return py_mods
|
|
|
|
|
|
def game_vars():
|
|
ret = {}
|
|
print("[*] Parsing Game variables")
|
|
res = r2_cmd("/r fcn.setup_game_vars ~CALL[1]").splitlines()
|
|
print()
|
|
for line in tqdm(res, ascii=True):
|
|
addr = line.strip()
|
|
r2_cmd(f"s {addr}")
|
|
args = r2_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 = r2_cmd(f"psz @{hex(name)}").strip()
|
|
desc = r2_cmd(f"psz @{hex(desc)}").strip()
|
|
addr = hex(addr)
|
|
r2_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 = dict(
|
|
game_vars=game_vars(),
|
|
c_callbacks=c_callbacks(),
|
|
py_mods=py_mods(),
|
|
assertions=assertions(),
|
|
vtables=vtables(),
|
|
world=world(),
|
|
render=render(),
|
|
)
|
|
|
|
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(script_path, "w") as of:
|
|
wcmds = []
|
|
for cmd in r2cmds:
|
|
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.r2")
|
|
|
|
r2.quit()
|
|
|
|
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)
|
|
|
|
|
|
print("[+] Executing Cutter")
|
|
try:
|
|
start_program(['cutter','-A','0','-i',script_path,scrap_exe],cwd=folder,shell=False)
|
|
except FileNotFoundError:
|
|
print("[-] cutter not installed, falling back to r2")
|
|
start_program(['r2','-i',script_path,scrap_exe],cwd=folder,shell=False) |