diff options
-rw-r--r-- | sploit/payload/__init__.py | 1 | ||||
-rw-r--r-- | sploit/payload/payload.py | 281 | ||||
-rw-r--r-- | sploit/payload/payload_entry.py | 99 |
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) |