summaryrefslogtreecommitdiffstats
path: root/sploit/payload
diff options
context:
space:
mode:
authorMalfurious <m@lfurio.us>2025-01-01 07:22:26 -0500
committerMalfurious <m@lfurio.us>2025-01-01 07:22:26 -0500
commit70c7c16a157f0e2056d0b96b96f6e13c83841bc3 (patch)
tree5f6d84642fc8b0aa89a32ef17f4b374605c7e089 /sploit/payload
parentf01ec45e773291c3659a1dcaf8cd9a51ece19823 (diff)
parent438c66673f7daca0fdc2d23b1a4fd39517528576 (diff)
downloadnsploit-70c7c16a157f0e2056d0b96b96f6e13c83841bc3.tar.gz
nsploit-70c7c16a157f0e2056d0b96b96f6e13c83841bc3.zip
Merge branch 'indextbl'
This branch is a major semantic redesign of Symtbl and Payload. These two classes are now implemented as derivitives of the newly refactored IndexTbl mechanism. Necessary cascading changes have been made to keep these tools in working order. * indextbl: payload: rop: Update for new Payload class Update ROP gadget types to extend IndexEntry payload: Refactor as a concrete IndexTbl lict: Add new list-dictionary hybrid type symtbl: Refactor abstract IndexTbl interface
Diffstat (limited to 'sploit/payload')
-rw-r--r--sploit/payload/__init__.py1
-rw-r--r--sploit/payload/gadhint.py43
-rw-r--r--sploit/payload/payload.py281
-rw-r--r--sploit/payload/payload_entry.py99
-rw-r--r--sploit/payload/rop.py245
5 files changed, 424 insertions, 245 deletions
diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py
index 78769b4..d65b0f0 100644
--- a/sploit/payload/__init__.py
+++ b/sploit/payload/__init__.py
@@ -1,3 +1,4 @@
from .gadhint import *
from .payload import *
+from .payload_entry import *
from .rop import *
diff --git a/sploit/payload/gadhint.py b/sploit/payload/gadhint.py
index 9b077fe..1bef9f0 100644
--- a/sploit/payload/gadhint.py
+++ b/sploit/payload/gadhint.py
@@ -1,12 +1,15 @@
+import copy
from dataclasses import dataclass, field
+
from sploit.rev.gadget import Gadget
+from sploit.types.index_entry import IndexEntry
@dataclass
-class GadHint:
+class GadHint(IndexEntry):
"""
User-annotated gadget description object
- gadget (Gadget|int): The gadget being annotated. May be a Gadget object or
+ 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
@@ -20,10 +23,10 @@ class GadHint:
Keys are destination register names, values are immediate values. The order
given is insignificant.
- writes (dict{str:str}): The register-to-memory moves (stores) made by this
- gadget. Keys are destination register names (expected to hold memory
- locations), values are source register names (expected to hold direct
- 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
@@ -43,7 +46,7 @@ class GadHint:
should not be accounted for. A value of zero is taken as "unspecified".
"""
- gadget: int = 0
+ base: int = 0
pops: list = field(default_factory=list)
movs: dict = field(default_factory=dict)
imms: dict = field(default_factory=dict)
@@ -57,21 +60,7 @@ class GadHint:
@property
def offset(self):
"""Return gadget offset as an integer."""
- return int(self.gadget)
-
- def __index__(self):
- """Convert object to integer using offset value."""
- return self.offset
-
- def __add__(self, x):
- """Return new object with adjusted offset."""
- return GadHint(self.gadget + x, self.pops, self.movs, self.imms,
- self.writes, self.requirements, self.stack, self.align,
- self.syscall, self.spm)
-
- def __sub__(self, x):
- """Return new object with adjusted offset."""
- return self + (-x)
+ return int(self.base)
def with_requirements(self, reqs):
"""Return new object with additional requirements."""
@@ -81,9 +70,9 @@ class GadHint:
f"GadHint: Conflicting gadget requirements: "
f"{self.requirements}, {reqs}")
- return GadHint(self.gadget, self.pops, self.movs, self.imms,
- self.writes, self.requirements | reqs, self.stack,
- self.align, self.syscall, self.spm)
+ new = copy.deepcopy(self)
+ new.requirements |= reqs
+ return new
def __repr__(self):
"""Return human-readable GadHint."""
@@ -92,8 +81,8 @@ class GadHint:
return f", {name}={prop}"
return ""
- s = hex(self.gadget)
- s = f"Gadget({s})" if type(self.gadget) is Gadget else s
+ 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)
diff --git a/sploit/payload/payload.py b/sploit/payload/payload.py
index cf105c6..a59eed2 100644
--- a/sploit/payload/payload.py
+++ b/sploit/payload/payload.py
@@ -1,94 +1,203 @@
-from sploit.arch import arch, itob
-from sploit.symtbl import Symtbl
+from sploit.arch import itob
+from sploit.payload.payload_entry import PayloadEntry
+from sploit.types.indextbl import IndexTbl
+from sploit.types.index_entry import IndexEntry
+from sploit.types.lict import Lict
+
+_REPR_DATA_LEN = 64
+
+class Payload(IndexTbl):
+ """
+ Binary payload builder
+
+ This class provides an API for fluently specifying structured payloads from
+ an assortment of input data. Payload "indices" are any bytes-like data,
+ which includes some supported IndexEntry types as well as nested Payloads.
+
+ Payload is an IndexTbl based on a Lict and features two main use-cases or
+ syntaxes for interacting with data.
+
+ The first method (the formal method) is through the use of normal index
+ access via attributes or subscripts. In this case, element keys are usually
+ given. When a new index is defined, it is inserted at the end of the
+ payload. Modifications to existing indices change the data in-place, and
+ this causes the content of the payload to shift around if the replaced data
+ is of a different length.
+
+ The second method (the quick method) is through the use of Payload's __call__
+ method. This is a general purpose "quick action" method that, among other
+ things, will insert data to the payload. If the Payload object is called
+ with 1 or more arguments, the values of these arguments are appended to the
+ payload in the order given. There is no way to specify keys using this
+ option, so the data simply occupies unkeyed elements in the underlying Lict.
+
+ In either case, the data inserted must be bytes-like. In some common cases,
+ the data will be coerced into bytes. See the method __prep_insertion for
+ details on how this is handled. See the PayloadEntry module for some
+ additional features.
+
+ When retrieving indices from the payload, instead of the element's value,
+ either the element offset or (for IndexEntries) the value based at that
+ offset is returned to you. If the payload has a non-zero base, this is
+ interpreted as the element's address in memory. This is useful for any
+ exploit that requires pointers to other crafted data.
+
+ The binary output of a payload is simply the binary output of each of its
+ elements, concatenated together - there are no gaps. If you need to space
+ or separate two elements, you need to insert padding bytes between them.
+ The element binary content is either the object itself (for bytes elements),
+ the output from `payload_bytes()` (for PayloadEntries), or the output from
+ `bytes(obj)` for everything else.
+
+ The following is a simple example using the Payload module to perform a
+ hypothetical stack buffer overrun "ret2win" with both of the build syntaxes:
+
+ # 100 bytes from the start of the buffer to the saved frame pointer
+ # call (return into) the function "pwned", given by an ELF Symtbl
+ # 3 arguments, which are given on the stack
+
+ # formal method
+ p = Payload()
+ p.smash = padlen(100)
+ p.fp = placeholder()
+ p.ret = elf.sym.pwned
+ p.ret2 = placeholder()
+ p.arg1 = 0
+ p.arg2 = 1
+ p.arg3 = 2
+ io.write(bytes(p))
+
+ # quick method
+ p = Payload()(padlen(100), placeholder())
+ p(elf.sym.pwned, placeholder(), 0, 1, 2)
+ io.write(p())
+ """
+
+ def __init__(self, base=0, entries=None):
+ """Construct new Payload with optional base and content."""
+ super().__init__(base)
+ if not isinstance(entries, Lict):
+ entries = Lict(entries)
+ object.__setattr__(self, "__entries__", entries)
+
+ def __repr__(self):
+ """Return human-readable Payload."""
+ FMT = "\n{:<20} {:<20} {:<20}"
+ s = f"{len(self.__entries__)} items, {len(self)} bytes @ {hex(self)}"
+
+ if len(self.__entries__) > 0:
+ s += FMT.format("ADDRESS", "SYMBOL", "DATA")
+
+ for i, value in enumerate(self.__entries__):
+ key = self.__entries__.idx2key(i)
+ key = "(unkeyed)" if key is None else str(key)
+ key = f"[{key}]" if isinstance(value, IndexEntry) else key
+
+ addr = self.__addrof(i)
+ data = str(self.__bytesof(i))
+ if len(data) > _REPR_DATA_LEN:
+ data = data[:_REPR_DATA_LEN] + " ..."
+
+ s += FMT.format(hex(addr), key, data)
+
+ return s
+
+ def __bytes__(self):
+ """Return calculated payload bytes."""
+ x = [ self.__bytesof(i) for i in range(len(self.__entries__)) ]
+ return b"".join(x)
+
+ def __call__(self, *args):
+ """
+ Payload quick-action call operator.
+
+ If called with arguments, append these values to the payload in the
+ order given. The payload (self) is returned for easily chaining calls.
+
+ If called without arguments, return the rendered payload content as if
+ `bytes(payload)` was called.
+ """
+ if len(args) == 0:
+ return bytes(self)
+
+ for value in args:
+ value = self.__prep_insertion(value, self.end())
+ self.__entries__.append(value)
-class Payload:
- MAGIC = b'\xef'
-
- def __init__(self, **kwargs):
- self.payload = b''
- self.sym = Symtbl(**kwargs)
- self.ctrs = {}
-
- def __len__(self):
- return len(self.payload)
-
- def __call__(self, badbytes=b''):
- found = [ hex(x) for x in set(self.payload).intersection(badbytes) ]
- if len(found) > 0:
- raise Exception(f'Payload: bad bytes in content: {found}')
- return self.payload
-
- def _name(self, kind, sym):
- if sym is not None: return sym
- try: ctr = self.ctrs[kind]
- except: ctr = 0
- self.ctrs[kind] = ctr + 1
- return f'{kind}_{ctr}'
-
- def _append(self, value, sym):
- (self.sym @ 0)[sym] = len(self)
- self.payload += value
- return self
-
- def _prepend(self, value, sym):
- self.sym >>= len(value)
- (self.sym @ 0)[sym] = 0
- self.payload = value + self.payload
return self
def end(self):
- return self.sym.base + len(self)
-
- def bin(self, *values, sym=None):
- return self._append(b''.join(values), sym=self._name('bin', sym))
-
- def str(self, *values, sym=None):
- values = [ v.encode() + b'\x00' for v in values ]
- return self.bin(*values, sym=self._name('str', sym))
-
- def int(self, *values, sym=None):
- values = [ itob(v) for v in values ]
- return self.bin(*values, sym=self._name('int', sym))
+ """Return the offset or address of the end of the payload."""
+ return self.base + len(self)
- def int8(self, *values, sym=None):
- values = [ itob(v, 1) for v in values ]
- return self.bin(*values, sym=self._name('int', sym))
+ # IndexTbl abstract methods
- def int16(self, *values, sym=None):
- values = [ itob(v, 2) for v in values ]
- return self.bin(*values, sym=self._name('int', sym))
+ def __copy__(self):
+ """Return copy of object with shared data entries."""
+ return Payload(self.base, self.__entries__)
- def int32(self, *values, sym=None):
- values = [ itob(v, 4) for v in values ]
- return self.bin(*values, sym=self._name('int', sym))
+ def __iter__(self):
+ """Iterate over data entries."""
+ return iter(self.__entries__)
- def int64(self, *values, sym=None):
- values = [ itob(v, 8) for v in values ]
- return self.bin(*values, sym=self._name('int', sym))
-
- def ret(self, *values, sym=None):
- return self.int(*values, sym=self._name('ret', sym))
-
- def sbp(self, *values, sym=None):
- if len(values) == 0:
- return self.rep(self.MAGIC, arch.wordsize, sym=self._name('sbp', sym))
- return self.int(*values, sym=self._name('sbp', sym))
-
- def rep(self, value, size, sym=None):
- return self.bin(self._rep_helper(value, size), sym=self._name('rep', sym))
-
- def pad(self, size, value=None, sym=None):
- return self.bin(self._pad_helper(size, value), sym=self._name('pad', sym))
-
- def pad_front(self, size, value=None, sym=None):
- return self._prepend(self._pad_helper(size, value), sym=self._name('pad', sym))
-
- def _rep_helper(self, value, size, *, explain=''):
- if size < 0:
- raise Exception(f'Payload: {explain}rep: available space is negative')
- if (size := size / len(value)) != int(size):
- raise Exception(f'Payload: {explain}rep: element does not divide the space evenly')
- return value * int(size)
-
- def _pad_helper(self, size, value):
- return self._rep_helper(value or arch.nopcode, size - len(self), explain='pad: ')
+ def __len__(self):
+ """Return the size of the payload content in bytes."""
+ return len(bytes(self))
+
+ def __getindex__(self, index):
+ """Return payload index value or address."""
+ value, _ = self.__valueof(index)
+ return value
+
+ def __setindex__(self, index, value):
+ """Set payload index value."""
+ try:
+ addr = self.__addrof(index)
+ except KeyError:
+ addr = self.end()
+ value = self.__prep_insertion(value, addr)
+ self.__entries__[index] = value
+
+ def __delindex__(self, index):
+ """Delete payload index."""
+ del self.__entries__[index]
+
+ # Payload helpers
+
+ def __valueof(self, index):
+ """Return a tuple: (addr of value, literal value) for index."""
+ value = self.__entries__[index]
+ addr = self.__addrof(index)
+ if isinstance(value, IndexEntry):
+ value @= addr
+ return value, value
+ return addr, value
+
+ def __addrof(self, index):
+ """Return address (base + offset) for index."""
+ index = self.__entries__.key2idx(index)
+ sizes = [ len(self.__bytesof(i)) for i in range(index) ]
+ return self.base + sum(sizes)
+
+ def __bytesof(self, index):
+ """Return byte output for index."""
+ _, value = self.__valueof(index)
+ if isinstance(value, PayloadEntry):
+ return value.payload_bytes(self)
+ return bytes(value)
+
+ def __prep_insertion(self, value, addr):
+ """Initialize or type coerce input value for payload insert."""
+ if isinstance(value, PayloadEntry):
+ value @= addr
+ value.payload_insert(self)
+ return value
+
+ if type(value) is str:
+ value = value.encode() + b"\x00"
+ elif type(value) is int:
+ value = itob(value)
+
+ # Confirm value has a functional conversion to bytes
+ bytes(value)
+ return value
diff --git a/sploit/payload/payload_entry.py b/sploit/payload/payload_entry.py
new file mode 100644
index 0000000..7088f83
--- /dev/null
+++ b/sploit/payload/payload_entry.py
@@ -0,0 +1,99 @@
+from sploit.arch import arch, itob
+from sploit.types.index_entry import IndexEntry
+
+_PLACEHOLDER_MAGIC = b"\xef"
+
+class PayloadEntry(IndexEntry):
+ """Base class for dynamic Payload entries"""
+
+ def __repr__(self):
+ """Return human-readable entry description."""
+ return f"{self.__class__.__name__}{self.__dict__}"
+
+ def payload_insert(self, payload):
+ """
+ Called on insert into a payload object.
+
+ Override this method to perform any initialization which requires a
+ reference to the payload object. self.base is set to the insertion
+ location.
+ """
+ pass
+
+ def payload_bytes(self, payload):
+ """
+ Called to generate bytes for this entry.
+
+ Override this method to generate and return the binary output for this
+ dynamic payload entry. self.base is set to the current entry address
+ or offset.
+ """
+ return b""
+
+# Concrete payload entry definitions
+
+class pointer(PayloadEntry):
+ """Generate an integer which is always a fixed offset from self.base."""
+
+ def __init__(self, target=None):
+ self.target = target
+
+ def payload_insert(self, payload):
+ if self.target is None:
+ self.target = self.base
+ self.target -= self.base
+
+ def payload_bytes(self, payload):
+ return itob(self.target + self.base)
+
+class padlen(PayloadEntry):
+ """Generate padding to reach a target payload length."""
+
+ def __init__(self, size, data=None):
+ self.size = size
+ self.data = data
+
+ def _gen_padding(self, size):
+ data = self.data or arch.nopcode
+ if size < 0:
+ raise ValueError("padding: Available space is negative")
+ if (size := size / len(data)) != int(size):
+ raise ValueError("padding: Element does not divide the space evenly")
+ return data * int(size)
+
+ def payload_bytes(self, payload):
+ return self._gen_padding(self.size - (self.base - payload.base))
+
+class padabs(padlen):
+ """Generate padding to reach a target absolute address."""
+
+ def payload_bytes(self, payload):
+ return self._gen_padding(self.size - self.base)
+
+class padrel(padlen):
+ """Generate a fixed length of padding (aka: length relative to self)."""
+
+ def payload_bytes(self, payload):
+ return self._gen_padding(self.size)
+
+class padalign(padlen):
+ """Generate padding to reach next aligned address."""
+
+ def __init__(self, size=None, data=None):
+ self.size = size
+ self.data = data
+
+ def payload_bytes(self, payload):
+ size = self.size or arch.alignment
+ return self._gen_padding(-self.base % size)
+
+class placeholder(padlen):
+ """Generate fixed length of magic bytes, one word length by default."""
+
+ def __init__(self, size=None):
+ self.size = size
+ self.data = _PLACEHOLDER_MAGIC
+
+ def payload_bytes(self, payload):
+ size = self.size or arch.wordsize
+ return self._gen_padding(size)
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