diff options
Diffstat (limited to 'sploit/payload/payload.py')
-rw-r--r-- | sploit/payload/payload.py | 281 |
1 files changed, 195 insertions, 86 deletions
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 |