diff options
-rw-r--r-- | sploit/payload/rop.py | 245 |
1 files changed, 113 insertions, 132 deletions
diff --git a/sploit/payload/rop.py b/sploit/payload/rop.py index 54226b4..30467de 100644 --- a/sploit/payload/rop.py +++ b/sploit/payload/rop.py @@ -1,78 +1,68 @@ -""" -ROP chain generation utilities - -This module contains tools for automating basic return-oriented-programming -workloads, such as loading register values and calling into arbitrary functions -or syscalls. The tools are currently designed to work on x86 (32 or 64 bit) -and ARM (32 bit only). - -The main appeal of the ROP class is the ability to abstract away the manual -construction of ROP chain data, and instead make declarative statements like -"call this function with these arguments." The ROP class will also utilize its -supplied binary objects to automatically find and use trivial gadgets. - -The user is able to provide annotations for more complicated gadgets, which help -instruct the class how to incorporate them into a ROP chain. This is done with -the GadHint dataclass. GadHint objects are provided to a ROP instance by -including them in the Symtbl of one of the binary objects it is constructed with. -If applicable, a user-supplied gadget will take precedence over automatic gadget -searching. - -See the GadHint class to learn more about the descriptive attributes that are -supported. -""" - from graphlib import TopologicalSorter from sploit.arch import arch, btoi, itob from sploit.payload.gadhint import GadHint from sploit.payload.payload import Payload +from sploit.payload.payload_entry import padalign, padlen -class ROP(Payload): - """ - ROP-enabled payload builder - - POP_MAGIC (int): Magic value used for pop instructions where no specific - value is required by the user. +_POP_MAGIC = 0xdead +_SPM_MAGIC = b"\x69" +_ERROR_MAGIC = 0xbaadc0de - SPM_MAGIC (bytes): Magic value to fill the stack with when the best - available cleaning gadget is larger than is necessary. - - objects (list[ELF]): The binary objects this ROP instance will consider - for gadget searching. +class ROP: + """ + ROP chain generation tool + + This class contains methods for automating basic return-oriented programming + workloads, such as loading register values and calling into arbitrary + functions or syscalls. The tools are currently designed to work on x86 + (32 or 64 bit) and ARM (32 bit only). + + The main appeal of the ROP class is the ability to abstract away the manual + construction of ROP chain data, and instead make declarative statements + like "call this function with these arguments". The ROP class will also + utilize its supplied binary objects to automatically find and use trivial + gadgets. + + The user is able to provide annotations for more complicated gadgets, which + help instruct the class how to incorporate them into a ROP chain. This is + done with the GadHint dataclass. GadHint objects are provided to a ROP + instance by including them in the Symtbl of one of the binary objects it is + constructed with. If applicable, a user-supplied gadget will take + precedence over automatic gadget searching. See the GadHint module to learn + more about the descriptive attributes that are supported. + + objects (list[ELF]): The binary objects this ROP instance will consider for + gadget searching. If one of these is the target executable binary, it + should appear first in the list. safe_syscalls (bool): If True, require that automatically found syscall instructions are immediately followed by a return instruction. align_calls (bool): If True, ensure that the stack return address into function calls is aligned according to the architecture alignment property. - Knowledge of alignment is taken from the instance Symtbl's base value. clean_stack (bool): If True, attempt to locate a cleaning gadget to "pop" stack data that is leftover from a function call. Required if attempting to make multiple calls that involve stack-based arguments. """ - POP_MAGIC = 0xdead - SPM_MAGIC = b'\x69' - def __init__(self, *objects, safe_syscalls=True, align_calls=True, - clean_stack=True, **symbols): - """Initialize new ROP builder instance.""" - super().__init__(**symbols) + clean_stack=True): + """Construct new ROP builder.""" self.objects = objects self.safe_syscalls = safe_syscalls self.align_calls = align_calls self.clean_stack = clean_stack - def gadgets(self, *regexes, cont=False): + def search_gadgets(self, *regexes, cont=False): """Return a list of matching gadgets, considering all objects.""" results = [] for obj in self.objects: results += obj.gadgets(*regexes, cont=cont) return results - def gadget(self, *regexes): + def search_gadget(self, *regexes): """Return the first matching gadget, considering all objects.""" for obj in self.objects: try: @@ -82,75 +72,70 @@ class ROP(Payload): raise LookupError( f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") - def assign(self, *, sym=None, **sets): + def gadget(self, gadget): """ - Insert a ROP chain to control given registers. + Return a generic ROP payload. + + gadget (GadHint): Annotated gadget to prepare a chain from. + """ + return self.__build_chain(gadget, {}) + + def assign(self, **sets): + """ + Return a ROP payload to control given registers. **sets (str:int): Keyword arguments specify register assignments to perform with this ROP chain. Argument names correspond to register names. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ - gadget = GadHint(0, requirements=sets) - return self._start_chain(gadget, sym=self._name("assign", sym)) + return self.gadget(GadHint(requirements=sets)) - def call(self, func, *params, sym=None): + def call(self, func, *params): """ - Insert a ROP chain to call function. + Return a ROP payload to call function. func (int): Entry address of function to call. *params (int): Remaining positional args are passed to func. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ register_params = dict(zip(arch.funcargs, params)) stack_params = params[len(register_params):] gadget = GadHint(func, requirements=register_params, stack=stack_params, align=self.align_calls) - return self._start_chain(gadget, sym=self._name("call", sym)) + return self.gadget(gadget) - def syscall(self, *params, sym=None): + def syscall(self, *params): """ - Insert a ROP chain to call kernel. + Return a ROP payload to call kernel. *params (int): The first argument is the syscall number. Remaining positional arguments are passed to the syscall. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ if len(params) > len(arch.kernargs): raise TypeError("ROP: Too many arguments passed to syscall. " - "Target architecture supports up to {len(arch.kernargs)-1}.") + f"Target architecture supports up to {len(arch.kernargs)-1}.") register_params = dict(zip(arch.kernargs, params)) - gadget = self._get_gadget("syscall", {}).with_requirements(register_params) - return self._start_chain(gadget, sym=self._name("syscall", sym)) + sc = self.__get_gadget("syscall", {}) + return self.gadget(sc.with_requirements(register_params)) - def memcpy(self, dst, src, *, sym=None): + def memcpy(self, dst, src): """ - Insert a ROP chain to write data into memory. + Return a ROP payload to write data into memory. dst (int): The destination memory address. src (bytes): The content to write. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ - gadgets = [] + data = Payload() for idx in range(0, len(src), arch.wordsize): - g = self._get_write(dst + idx, btoi(src[idx:idx+arch.wordsize])) - gadgets.append(g) - return self._start_chain(*gadgets, sym=self._name("memcpy", sym)) + word = btoi(src[idx:idx+arch.wordsize]) + data(self.gadget(self.__get_write(dst+idx, word))) + return data - def _get_hints(self): + def __get_hints(self): """Return all user-supplied gadget hints.""" return [h for obj in self.objects for _,h in obj.sym if type(h) is GadHint] - def _discover_requirements(self, seen, graph, current): + def __discover_requirements(self, seen, graph, current): """ Populate gadget dependency graph. @@ -170,13 +155,13 @@ class ROP(Payload): # Requiring a register to store different values may require the # use of multiple gadgets. if (r, v) not in seen: - gadget = self._get_gadget(r, current.requirements) + gadget = self.__get_gadget(r, current.requirements) # Add gadget's requirements to the dependency graph. # We say that each requirement is a 'successor' to this # current gadget 'r', so that the chain builder will satisfy # 'r' first. This prevents the fulfillment of 'r' from - # colbbering targets it requires, as the builder will satisfy + # clobbering targets it requires, as the builder will satisfy # them afterward. for x in gadget.requirements: graph.add(x, r) @@ -189,9 +174,9 @@ class ROP(Payload): # Mark node as visited seen.add((r, v)) - self._discover_requirements(seen, graph, gadget) + self.__discover_requirements(seen, graph, gadget) - def _get_gadget(self, target, sets): + def __get_gadget(self, target, sets): """ Get context-specific gadget. @@ -204,7 +189,7 @@ class ROP(Payload): """ # First, consider user-provided hints before automatically locating # gadgets. - for hint in self._get_hints(): + for hint in self.__get_hints(): # Setup additional requirements based on hint's register moves. # If a mov target is in sets, require to set the src to the 'sets' # value. @@ -229,17 +214,17 @@ class ROP(Payload): # Automatically locate simple gadgets if target == "ret": - return GadHint(self.gadget(arch.ret)) + return GadHint(self.search_gadget(arch.ret)) if target == "syscall": insns = [arch.syscall, arch.ret] if self.safe_syscalls else [arch.syscall] - return GadHint(self.gadget(*insns), syscall=True) + return GadHint(self.search_gadget(*insns), syscall=True) # target == register insns = [ i.format(target) for i in arch.popgad ] - return GadHint(self.gadget(*insns), pops=[target], spm=arch.wordsize) + return GadHint(self.search_gadget(*insns), pops=[target]) - def _get_clean(self, size): + def __get_clean(self, size): """ Get a stack cleaning gadget that moves sp by _at least_ size. @@ -249,11 +234,11 @@ class ROP(Payload): # the user likely hasn't annotated the GadHint properly. Returning a # larger move than requested is fine, since the chain builder can insert # junk to be popped. - for hint in self._get_hints(): + for hint in self.__get_hints(): if hint.spm >= size and hint.spm > 0: return hint - results = self.gadgets(*arch.cleangad) + results = self.search_gadgets(*arch.cleangad) table = { int(g.asm[0].group(1), 0): g for g in results } sizes = sorted([ x for x in table.keys() if x >= size ]) @@ -263,7 +248,7 @@ class ROP(Payload): raise LookupError( f"ROP: Need to define a stack move gadget of at least {size}") - def _get_write(self, dst, src): + def __get_write(self, dst, src): """ Get a memory write gadget, injected with requirements for user data. @@ -273,7 +258,7 @@ class ROP(Payload): # If any exist, take the first write provided by user hints, assuming # the user's intent to specifically use _this_ write. Follow-on gadgets # to setup the dst and src registers must be findable. - for hint in self._get_hints(): + for hint in self.__get_hints(): if hint.writes: d, s = list(hint.writes.items())[0] return hint.with_requirements({d:dst, s:src}) @@ -281,7 +266,7 @@ class ROP(Payload): # Only take an automatic write gadget if we can prove up front that its # requirements can be met, otherwise move on. A later search result may # pass the test. - results = self.gadgets(*arch.writegad) + results = self.search_gadgets(*arch.writegad) for gad in results: d = gad.asm[0].group("dst") @@ -290,52 +275,45 @@ class ROP(Payload): try: # Assert requirements are met. gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) - self._discover_requirements(set(), TopologicalSorter(), gadget) + self.__discover_requirements(set(), TopologicalSorter(), gadget) return gadget except: pass raise LookupError("ROP: Need to define gadgets for memory write / deps") - def _start_chain(self, *gadgets, sym=None): - """ - Insert a generic ROP chain. - - *gadgets (GadHint): Annotated gadgets to prepare a chain from. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. + def __build_chain(self, gadget, sets): """ - stack = Payload(base=self.end()) - for g in gadgets: - self._build_chain(stack, g, {}) - return self.bin(stack(), sym=self._name("gadget", sym)) - - def _build_chain(self, stack, gadget, sets): - """ - Generate chain data for a given ROP gadget. + Generate ROP chain data for a given gadget. This function recursively builds a ROP chain for the given gadget and - its requirements, storing data in the 'stack' object. + its requirements, returning the result as a Payload. - stack (Payload): Stack data being constructed. - gadget (GadHint): Current gadget we are processing. + gadget (GadHint): Current gadget to process. sets (dict{str:int}): The set of other register requirements we are trying to fulfill in parallel. """ # Form a to-do-list of registers from our immediate requirements, # attempting to order them such that we avoid overwriting/conflicting - # values (this may not be possible). + # values. This may not be possible, in which case graph.static_order() + # will raise an exception. reqs = gadget.requirements graph = TopologicalSorter({ r:set() for r in reqs }) - self._discover_requirements(set(), graph, gadget) + self.__discover_requirements(set(), graph, gadget) to_do_list = [ x for x in graph.static_order() if x in reqs ] + chain = Payload() + # Start chain by satisfying to-do-list requirements. + if len(to_do_list) > 0: + chain.requirements = Payload() + while len(to_do_list) > 0: - g = self._get_gadget(to_do_list[0], reqs) - self._build_chain(stack, g, reqs) + r = to_do_list[0] + g = self.__get_gadget(r, reqs) + c = self.__build_chain(g, reqs) + chain.requirements[f"{r}_{reqs[r]}"] = c # This gadget may satisfy multiple items in the to-do-list. # Specifically, all of its pop and mov targets, and any load @@ -351,33 +329,36 @@ class ROP(Payload): if gadget.offset != 0: # Stack alignment if required. if gadget.align: - align = -stack.end() % arch.alignment - stack.rep(itob(self._get_gadget("ret", {})), align) + ret = self.__get_gadget("ret", {}) + chain.alignment = padalign(0, itob(ret)) # "Return address" entry into this gadget. - stack.ret(gadget.offset) + chain.gadget = gadget.offset - # The gadget's "inner stack data" will be values to be popped - # and additional junk data to be deallocated by the gadget itself. - sp_dest = len(stack) + gadget.spm - stack.int(*[ sets.get(p, self.POP_MAGIC) for p in gadget.pops ]) - if gadget.spm > 0: - stack.pad(sp_dest, self.SPM_MAGIC) + # The gadget's "inner stack data" will be values to be popped and + # additional junk data to be deallocated by the gadget itself. + if gadget.pops or gadget.spm > 0: + chain.inner = Payload() + chain.inner(*[ sets.get(p, _POP_MAGIC) for p in gadget.pops ]) + if gadget.spm > 0: + chain.inner.pad = padlen(gadget.spm, _SPM_MAGIC) # The gadget's "outer stack data" will be the additional values # explicitly specified by the gadget. Append a separate gadget # to clean up these values. - if len(gadget.stack) > 0: + if gadget.stack: size = len(gadget.stack) * arch.wordsize if self.clean_stack: - clean = self._get_clean(size) - stack.ret(clean) - sp_dest = len(stack) + clean.spm + clean = self.__get_clean(size) + chain.cleanup = clean.offset + pad = padlen(clean.spm, _SPM_MAGIC) else: - ret = self._get_gadget("ret", {}) - stack.ret(ret) - sp_dest = len(stack) + size + chain.cleanup = _ERROR_MAGIC + pad = None + + chain.outer = Payload() + chain.outer(*gadget.stack) + if pad: chain.outer.pad = pad - stack.int(*gadget.stack) - stack.pad(sp_dest, self.SPM_MAGIC) + return chain |