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) return self def end(self): """Return the offset or address of the end of the payload.""" return self.base + len(self) # IndexTbl abstract methods def __copy__(self): """Return copy of object with shared data entries.""" return Payload(self.base, self.__entries__) def __iter__(self): """Iterate over data entries.""" return iter(self.__entries__) 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