summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMalfurious <m@lfurio.us>2023-03-15 17:12:32 -0400
committerdusoleil <howcansocksbereal@gmail.com>2023-03-15 17:49:22 -0400
commit873cf63768302bab81b06987803e9d108e3ceebb (patch)
tree614ef870ac32b995196b10cf95d54824bd15a392
parentc9f5d7113c6f977fb31fd7699bd2d5a5869954ad (diff)
downloadsploit-873cf63768302bab81b06987803e9d108e3ceebb.tar.gz
sploit-873cf63768302bab81b06987803e9d108e3ceebb.zip
rev: Update rop gadget search functionality
Development on the rop chain builder has produced this upgrade to our gadget search facility. The primary advantages in this version are increased flexibility and runtime performance. It is now easier to find specific 'stray' instructions (not immediately followed by a ret) since we search from every position in the data returned by r2. If you _do_ want a ret, just specify it in your input regexes. For this reason, a dedicated function for locating a simple 'ret' gadget is no longer present - elf.gadget("ret") is the equivalent. A major change in this version is that we now obtain and operate on r2's JSON representation of the gadget data. We now only reach out to r2 once to get all information for a binary (which is cached) and the actual 'search' is implemented in Python. This provides a significant performance speedup in cases where we need many gadgets from one binary, as r2 doesn't need to inspect the entire file each time. Additional caching is done on specific search results, so that 100% redundant searches are returned immediately. Access to the raw JSON data is made available through a new function rop_json(), but is not exposed in the ELF interface, since it seems like a niche need. Search results are returned via Gadget objects (or a list thereof), which contain regular expression Match objects for each assembly instruction found in the gadget. This allows the caller to retrieve the values contained in regular expression capture groups if present. Also, anecdotally, the search functionality in r2 has seemed to return false negatives for some queries in the past, whereas I haven't noticed similar cases with this implementation yet. Signed-off-by: Malfurious <m@lfurio.us> Signed-off-by: dusoleil <howcansocksbereal@gmail.com>
-rw-r--r--sploit/rev/elf.py11
-rw-r--r--sploit/rev/r2.py82
2 files changed, 61 insertions, 32 deletions
diff --git a/sploit/rev/elf.py b/sploit/rev/elf.py
index 78f4e47..d173897 100644
--- a/sploit/rev/elf.py
+++ b/sploit/rev/elf.py
@@ -44,11 +44,8 @@ class ELF:
def retaddr(self, caller, callee):
return [c.ret_addr for c in r2.get_call_returns(self.path, caller, callee)]
- def retgad(self):
- return r2.ret_gadget(self.path)
+ def gadgets(self, *regexes, cont=False):
+ return r2.rop_gadgets(self.path, *regexes, cont=cont)
- def gad(self, gad):
- return [g.addr for g in r2.rop_gadget(self.path, gad)]
-
- def egad(self, gad):
- return r2.rop_gadget_exact(self.path, gad).addr
+ def gadget(self, *regexes):
+ return r2.rop_gadget(self.path, *regexes)
diff --git a/sploit/rev/r2.py b/sploit/rev/r2.py
index 7fe57d8..f2650da 100644
--- a/sploit/rev/r2.py
+++ b/sploit/rev/r2.py
@@ -1,10 +1,13 @@
from sploit.arch import arch
from sploit.log import ilog
+from sploit.rev.gadget import Gadget
from sploit.symtbl import Symtbl
from sploit.util import run_cmd_cached
-import re
from collections import namedtuple as nt
+from functools import cache
+import json
+import re
def run_cmd(binary,cmd):
return run_cmd_cached(['r2','-q','-c',cmd,'-e','scr.color=false',binary])
@@ -56,30 +59,59 @@ def get_locals(binary,func):
out = {var[1]:-(int(var[0],0)-arch.wordsize) for var in out}
return Symtbl(sbp=0, **out)
-def ret_gadget(binary):
- ilog(f'Searching for a ret gadget in {binary} with r2...')
-
- cmd_ret = '/R/ ret~ret'
- out = run_cmd(binary,cmd_ret)
- out = out[0]
- out = re.split(r'\s+',out)
- out = out[1]
- return int(out,0)
-
-def rop_gadget(binary,gad):
- ilog(f'Searching for "{gad}" gadgets in {binary} with r2...')
-
- cmd_gad = f'"/R/q {gad}"'
- out = run_cmd(binary,cmd_gad)
- Gad = nt("Gad", "addr asm")
- out = [Gad(int(gad[:gad.find(':')],0),gad[gad.find(':')+2:]) for gad in out]
- return out
-
-def rop_gadget_exact(binary,gad):
- gads = rop_gadget(binary,gad)
- for g in gads:
- if g.asm[:-1].replace('; ',';') == gad:
- return g
+def rop_json(binary):
+ # Gadget JSON schema:
+ # [
+ # {
+ # retaddr: int
+ # size: int
+ # opcodes: [
+ # {
+ # offset: int
+ # size: int
+ # opcode: string
+ # type: string
+ # }
+ # ]
+ # }
+ # ]
+ return json.loads("\n".join(run_cmd(binary, "/Rj")))
+
+@cache
+def rop_gadgets(binary, *regexes, cont=False):
+ ilog(f"Searching {binary} for {'; '.join(regexes)} gadgets with r2...")
+ gadgets = rop_json(binary)
+ results = []
+
+ for gadget in gadgets:
+ opcodes = gadget['opcodes']
+ end_idx = len(opcodes) - len(regexes)
+
+ for start_idx in range(end_idx + 1):
+ idx = start_idx
+ size = end_idx - idx
+ regexes_use = (regexes + (".*",) * size) if cont else regexes
+
+ offset = opcodes[idx]['offset']
+ matches = []
+
+ for regex in regexes_use:
+ match = re.fullmatch(regex, opcodes[idx]['opcode'])
+ if not match:
+ break
+ matches.append(match)
+ idx += 1
+
+ if len(matches) == len(regexes_use):
+ results.append(Gadget(offset, matches))
+
+ return results
+
+def rop_gadget(binary, *regexes):
+ results = rop_gadgets(binary, *regexes)
+ if len(results) == 0:
+ raise LookupError(f"Could not find gadget for: {'; '.join(regexes)}")
+ return results[0]
def get_call_returns(binary,xref_from,xref_to):
ilog(f'Getting return addresses of calls from {hex(xref_from)} to {hex(xref_to)} in {binary} with r2...')