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