ScrapHacks/r2_analyze.py

325 lines
9.7 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
import yaml
r2cmds = []
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))
r2_script_path=os.path.join(scrapland_folder, "scrap_dissect.r2")
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!"
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")
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():
r2_cmd(line)
analysis(False)
for addr,comment in config.comments.items():
r2_cmd(f"CC {comment} @ {hex(addr)}")
for t in config.types:
r2_cmd(f'"td {t}"')
for addr, name in config.flags.items():
x64_dbg_label(addr,name,"loc")
r2_cmd(f"f loc.{name} 4 {hex(addr)}")
for addr, name in config.functions.items():
x64_dbg_label(addr,name,"fcn")
r2_cmd(f"afr fcn.{name} {hex(addr)}")
for addr,sig in config.function_signatures:
r2_cmd(f'"afs {config.function_signatures[addr]}" @{hex(addr)}')
def vtables():
ret = {}
print("[*] Analyzing VTables")
vtables = r2_cmdJ("avj")
for c in tqdm(vtables, ascii=True):
methods = []
name=config.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(r2_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',r2_script_path,scrap_exe],cwd=scrapland_folder,shell=False)
except FileNotFoundError:
print("[-] cutter not installed, falling back to r2")
start_program(['r2','-i',r2_script_path,scrap_exe],cwd=scrapland_folder,shell=False)