mirror of
https://github.com/radareorg/esilsolve.git
synced 2026-11-07 06:25:59 -10:00
add better support for r2frida, add ability to init state from frida hook
This commit is contained in:
1 parent
16564f7a93
commit
68800dca69
7 files changed
+194
-18
No files matched your search
@@ -214,7 +214,7 @@ class ESILRegisters:
|
||||
return new_reg
|
||||
|
||||
def __contains__(self, key: str):
|
||||
return self._registers.__contains__(key)
|
||||
return (key in self._registers or key in self.aliases)
|
||||
|
||||
def clone(self):
|
||||
clone = self.__class__(self.reg_info, self.aliases, self.pure_symbolic)
|
||||
|
||||
+27
-2
@@ -25,7 +25,7 @@ class ESILSolver:
|
||||
self.kwargs = kwargs
|
||||
self.debug = kwargs.get("debug", False)
|
||||
self.trace = kwargs.get("trace", False)
|
||||
self.lazy = kwargs.get("lazy", False)
|
||||
self.lazy = kwargs.get("lazy", False)
|
||||
self.pcode = kwargs.get("pcode", False)
|
||||
|
||||
self.states = []
|
||||
@@ -154,6 +154,10 @@ class ESILSolver:
|
||||
""" End the execution """
|
||||
self.stop = True
|
||||
|
||||
# just for r2frida, continue
|
||||
def resume(self):
|
||||
self.r2api.frida_continue()
|
||||
|
||||
# get rets from initial state to stop at
|
||||
def default_avoid(self, state: ESILState):
|
||||
pc = state.registers["PC"].as_long()
|
||||
@@ -237,7 +241,28 @@ class ESILSolver:
|
||||
# seek to function and init vm
|
||||
self.r2api.seek(addr)
|
||||
self.init_vm()
|
||||
return self.init_state()
|
||||
state = self.init_state()
|
||||
# state.registers["PC"] = addr
|
||||
|
||||
return state
|
||||
|
||||
def frida_state(self, addr: Address) -> ESILState:
|
||||
"""
|
||||
Create an ESILState with PC at address from r2frida
|
||||
|
||||
:param addr: Name of symbol or address to begin execution
|
||||
|
||||
>>> state = esilsolver.frida_state("validate")
|
||||
|
||||
"""
|
||||
|
||||
if type(addr) == str:
|
||||
addr = self.r2api.get_address(addr)
|
||||
|
||||
self.r2api.frida_init(addr)
|
||||
state = self.init_state()
|
||||
|
||||
return state
|
||||
|
||||
def reset(self, state: ESILState = None):
|
||||
"""
|
||||
|
||||
+95
-6
@@ -1,5 +1,11 @@
|
||||
import r2pipe
|
||||
import binascii
|
||||
import threading
|
||||
|
||||
try:
|
||||
import frida
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
class R2API:
|
||||
""" API for interacting with r2 through r2pipe """
|
||||
@@ -18,11 +24,35 @@ class R2API:
|
||||
|
||||
self.instruction_cache = {}
|
||||
self.cache_num = 64
|
||||
self.sleep = 0.1
|
||||
self.ccs = {}
|
||||
|
||||
self.register_info = None
|
||||
self.get_register_info()
|
||||
self.info = None
|
||||
self.get_info()
|
||||
|
||||
info = self.r2p.cmdj("ij")
|
||||
try:
|
||||
self.frida = info["core"]["file"].startswith("frida:")
|
||||
except:
|
||||
self.frida = False
|
||||
|
||||
self.frida_sess = None
|
||||
self.script = None
|
||||
self.frida_sess_init()
|
||||
|
||||
def frida_sess_init(self):
|
||||
if self.frida:
|
||||
info = self.r2p.cmdj("ij")
|
||||
self.pid = int(self.r2p.cmd("\\dp"))
|
||||
|
||||
self.device = frida.get_local_device()
|
||||
for dev in frida.enumerate_devices():
|
||||
if info["core"]["file"].startswith("frida://%s" % dev.id):
|
||||
self.device = dev
|
||||
|
||||
self.frida_sess = self.device.attach(self.pid)
|
||||
|
||||
def get_info(self):
|
||||
if self.info == None:
|
||||
@@ -103,11 +133,11 @@ class R2API:
|
||||
|
||||
# theres no arj all function to get all the regs as json so i made this
|
||||
# i should just make a pull request for r2
|
||||
def get_all_registers(self):
|
||||
def get_all_registers(self, thread=None):
|
||||
reg_dict = {}
|
||||
|
||||
for reg in self.all_regs:
|
||||
val_str = self.r2p.cmd("aer %s" % reg).strip().split(" = ")[-1]
|
||||
val_str = self.r2p.cmd("aer %s" % reg).strip().split(" = ")[-1]
|
||||
if val_str[:2] != "0x":
|
||||
val_str = "0x0"
|
||||
|
||||
@@ -115,8 +145,32 @@ class R2API:
|
||||
|
||||
return reg_dict
|
||||
|
||||
def init_vm(self):
|
||||
self.r2p.cmd("aei; aeim")
|
||||
def init_vm(self, thread=None):
|
||||
if not self.frida:
|
||||
self.r2p.cmd("aeim")
|
||||
else:
|
||||
reg_dict = {}
|
||||
reg_dicts = self.r2p.cmdj("\drj")
|
||||
|
||||
for rd in reg_dicts:
|
||||
if thread == None or thread == rd["id"]:
|
||||
reg_dict = rd["context"]
|
||||
break
|
||||
|
||||
# .\dr* should do this but doesn't always work
|
||||
for reg in reg_dict:
|
||||
self.set_reg_value(reg, int(reg_dict[reg], 16))
|
||||
|
||||
self.r2p.cmd("aei; aeip") # set PC
|
||||
|
||||
def frida_init(self, addr):
|
||||
self.disass(addr) # cache unhooked instrs
|
||||
|
||||
reg_dict = self.frida_context(addr)
|
||||
|
||||
# .\dr* should do this but doesn't always work
|
||||
for reg in reg_dict:
|
||||
self.set_reg_value(reg, int(reg_dict[reg], 16))
|
||||
|
||||
def emu(self, instr):
|
||||
self.r2p.cmd("ae %s" % instr["esil"])
|
||||
@@ -140,7 +194,42 @@ class R2API:
|
||||
return self.ccs[func]
|
||||
|
||||
def get_address(self, func):
|
||||
return self.r2p.cmdj("pdj 1 @ %s" % str(func))[0]["offset"]
|
||||
if not self.frida:
|
||||
return self.r2p.cmdj("pdj 1 @ %s" % str(func))[0]["offset"]
|
||||
else:
|
||||
return int(self.r2p.cmd("\isa %s" % str(func)), 16)
|
||||
|
||||
def analyze(self, level=3): # level 7 solves ctfs automatically
|
||||
self.r2p.cmd("a"*level)
|
||||
self.r2p.cmd("a"*level)
|
||||
|
||||
def frida_continue(self):
|
||||
if not self.frida:
|
||||
return
|
||||
|
||||
self.r2p.cmd("\\dc")
|
||||
|
||||
if self.script != None:
|
||||
self.script.post({"type": "continue"})
|
||||
self.script.unload()
|
||||
self.script = None
|
||||
|
||||
def frida_context(self, addr):
|
||||
# super jank
|
||||
func = '''send(this.context);recv('continue',function(){}).wait()'''
|
||||
script_data = '''Interceptor.attach(ptr('0x%x'),function(){%s})''' \
|
||||
% (addr, func)
|
||||
|
||||
self.script = self.frida_sess.create_script(script_data)
|
||||
|
||||
context = {}
|
||||
event = threading.Event()
|
||||
def on_context(message, data):
|
||||
if message["type"] == "send":
|
||||
context.update(message["payload"])
|
||||
event.set()
|
||||
|
||||
self.script.on('message', on_context)
|
||||
self.script.load()
|
||||
|
||||
event.wait()
|
||||
return context
|
||||
@@ -0,0 +1,14 @@
|
||||
from esilsolve import ESILSolver
|
||||
import z3
|
||||
|
||||
esilsolver = ESILSolver("test/tests/validate", pcode=True)
|
||||
state = esilsolver.call_state(0x1760)
|
||||
|
||||
addr = 0x100000
|
||||
state.registers["r0"] = addr
|
||||
flag = z3.BitVec("flag", 16*8)
|
||||
state.memory[addr] = flag
|
||||
|
||||
state = esilsolver.run(target=0x00001840, avoid=[0x00001854])
|
||||
flag_str = state.evaluate_buffer(flag).replace(b"\x00", b"")
|
||||
print("FLAG: %s " % flag_str)
|
||||
@@ -0,0 +1,24 @@
|
||||
from esilsolve import ESILSolver
|
||||
import z3
|
||||
|
||||
# start the ESILSolver instance by attaching r2frida (replace id with yours)
|
||||
esilsolver = ESILSolver("frida://133ebc680e67c885e7f04621481d8a0229bef371/iOSCrackMe")
|
||||
|
||||
validate = esilsolver.r2api.get_address("validate")
|
||||
# initialize state with context from hook, app is suspended
|
||||
state = esilsolver.frida_state(validate)
|
||||
|
||||
# initialize symbolic bytes of solution
|
||||
# and constrain them to be /[a-zA-Z]/
|
||||
code = z3.BitVec("code", 16*8)
|
||||
state.constrain_bytes(code, "[a-zA-Z]")
|
||||
addr = state.registers["A0"].as_long()
|
||||
state.memory[addr] = code
|
||||
|
||||
state = esilsolver.run(validate+0x210, avoid=[validate+0x218])
|
||||
solution = state.evaluate_buffer(code)
|
||||
print("CODE: '%s'" % solution.decode())
|
||||
|
||||
# write solution into proper place
|
||||
# esilsolver.r2api.write(addr, solution)
|
||||
esilsolver.resume() # resume suspended app
|
||||
Executable
BIN
Binary file not shown.
+33
-9
@@ -5,8 +5,6 @@ try:
|
||||
r2p = None
|
||||
|
||||
import os, sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
import esilsolve
|
||||
import r2pipe
|
||||
import z3
|
||||
@@ -15,10 +13,12 @@ try:
|
||||
import colorama
|
||||
|
||||
r2p = r2pipe.open()
|
||||
except ImportError:
|
||||
#print("esplugin could not be loaded")
|
||||
except ImportError as e:
|
||||
#print("esplugin could not be loaded: " + str(e))
|
||||
pass
|
||||
|
||||
add_shortcuts = True
|
||||
|
||||
class ESILSolvePlugin:
|
||||
|
||||
def __init__(self, r2p):
|
||||
@@ -28,6 +28,8 @@ class ESILSolvePlugin:
|
||||
# aesx - Analysis: Emulation / Symbolic eXecution
|
||||
"aesx?" : self.print_help,
|
||||
"aesxi" : self.handle_init,
|
||||
"aesxif": self.handle_init,
|
||||
"aesxfc": self.handle_continue,
|
||||
"aesxs" : self.handle_set_symbolic,
|
||||
"aesxsb": self.handle_set_symbolic,
|
||||
"aesxsc": self.handle_set_symbolic,
|
||||
@@ -52,6 +54,13 @@ class ESILSolvePlugin:
|
||||
"aesxws": self.handle_state_set
|
||||
}
|
||||
|
||||
if add_shortcuts:
|
||||
new = {}
|
||||
for c in self.commands:
|
||||
new["X"+c[4:]] = self.commands[c]
|
||||
|
||||
self.commands.update(new)
|
||||
|
||||
self.symbols = {}
|
||||
self.esinstance = None
|
||||
self.initialized = False
|
||||
@@ -78,13 +87,14 @@ class ESILSolvePlugin:
|
||||
|
||||
lines = [
|
||||
usage,
|
||||
["aesxi", " [debug] [lazy]", "Initialize the ESILSolve instance and VM"],
|
||||
["aesxi", "[f] [debug] [lazy]", "Initialize the ESILSolve instance and VM"],
|
||||
["aesxs", "[bc] reg|addr [name] [length]", "Set symbolic value in register or memory"],
|
||||
["aesxv", " reg|addr value", "Set concrete value in register or memory"],
|
||||
["aesxc", " sym value", "Constrain symbol to be value, min, max, regex"],
|
||||
["aesxc", "[+-]", "Push / pop the constraint context"],
|
||||
["aesxx", "[ec] expr value", "Execute ESIL expression and evaluate/constrain the result"],
|
||||
["aesxr", "[ac] target [avoid x,y,z]", "Run symbolic execution until target address, avoiding x,y,z"],
|
||||
["aesxf", "[c]", "Resume r2frida after symex is finished"],
|
||||
["aesxe", "[j] sym1 [sym2] [...]", "Evaluate symbol in current state"],
|
||||
["aesxb", "[j] sym1 [sym2] [...]", "Evaluate buffer in current state"],
|
||||
["aesxd", "[j] [reg1] [reg2] [...]", "Dump register values / ASTs"],
|
||||
@@ -99,14 +109,25 @@ class ESILSolvePlugin:
|
||||
lazy = "lazy" in args
|
||||
|
||||
self.esinstance = esilsolve.ESILSolver(self.r2p, debug=debug, lazy=lazy)
|
||||
core = self.r2p.cmdj("ij")["core"]
|
||||
if "referer" not in core and "frida:" != core["file"][:7]:
|
||||
self.esinstance.r2api.init_vm()
|
||||
|
||||
self.state = self.esinstance.init_state()
|
||||
if args[0][-1] != "f":
|
||||
thread = None
|
||||
if len(args) > 1 and is_int(args[1]):
|
||||
thread = to_int(args[1])
|
||||
|
||||
self.esinstance.r2api.init_vm(thread)
|
||||
self.state = self.esinstance.init_state()
|
||||
|
||||
else:
|
||||
pc = to_int(self.r2p.cmd("s"))
|
||||
self.state = self.esinstance.frida_state(pc)
|
||||
|
||||
self.initialized = True
|
||||
self.symbols = {}
|
||||
|
||||
def handle_continue(self, args):
|
||||
self.esinstance.resume()
|
||||
|
||||
def handle_apply(self, args):
|
||||
if not self.initialized:
|
||||
self.print("error: need to initialize first")
|
||||
@@ -420,6 +441,9 @@ def to_int(s):
|
||||
return int(s)
|
||||
|
||||
def is_int(s):
|
||||
if s[:1] == "-":
|
||||
s = s[1:]
|
||||
|
||||
if s.isdigit() or s[:2] == "0x":
|
||||
return True
|
||||
|
||||
|
||||
Reference in new issue
Block a user