summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sploit/payload/__init__.py1
-rw-r--r--sploit/payload/payload.py281
-rw-r--r--sploit/payload/payload_entry.py99
3 files changed, 295 insertions, 86 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/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)