ScrapHacks/r2_analyze.py

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)