From c7ed242cbfd060e61dc5b73a64e2485988e93c10 Mon Sep 17 00:00:00 2001 From: Malfurious Date: Fri, 7 Mar 2025 02:32:30 -0500 Subject: payload: Move to types package Move the payload modules into the types subpackage, since the Payload system largely functions as a domain-specific data type for nsploit. This removes the payload subpackage. Signed-off-by: Malfurious --- nsploit/payload/__init__.py | 2 - nsploit/payload/payload.py | 224 --------------------------------------- nsploit/payload/payload_entry.py | 113 -------------------- nsploit/tech/fmtstring.py | 4 +- nsploit/tech/ret2dlresolve.py | 4 +- nsploit/tech/rop.py | 4 +- nsploit/types/__init__.py | 2 + nsploit/types/payload.py | 224 +++++++++++++++++++++++++++++++++++++++ nsploit/types/payload_entry.py | 113 ++++++++++++++++++++ 9 files changed, 345 insertions(+), 345 deletions(-) delete mode 100644 nsploit/payload/__init__.py delete mode 100644 nsploit/payload/payload.py delete mode 100644 nsploit/payload/payload_entry.py create mode 100644 nsploit/types/payload.py create mode 100644 nsploit/types/payload_entry.py diff --git a/nsploit/payload/__init__.py b/nsploit/payload/__init__.py deleted file mode 100644 index d4b9be7..0000000 --- a/nsploit/payload/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .payload import * -from .payload_entry import * diff --git a/nsploit/payload/payload.py b/nsploit/payload/payload.py deleted file mode 100644 index 295e3c8..0000000 --- a/nsploit/payload/payload.py +++ /dev/null @@ -1,224 +0,0 @@ -from nsploit.arch import itob -from nsploit.payload.payload_entry import PayloadEntry -from nsploit.types.indextbl import IndexTbl -from nsploit.types.index_entry import IndexEntry -from nsploit.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)}" - memo = {} - - 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, memo) - data = str(self.__bytesof(i, memo)) - 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.""" - memo = {} - x = [ self.__bytesof(i, memo) 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.""" - memo = {} - x = [ self.__lenof(i, memo) for i in range(len(self.__entries__)) ] - return sum(x) - - 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, memo): - """Return a tuple (addr of value, literal value) for index.""" - value = self.__entries__[index] - addr = self.__addrof(index, memo) - if isinstance(value, IndexEntry): - value @= addr - return value, value - return addr, value - - def __addrof(self, index, memo): - """Return address (base + offset) for index.""" - index = self.__entries__.key2idx(index) - try: - return memo[index] - except KeyError: - sizes = [ self.__lenof(i, memo) for i in range(index) ] - addr = self.base + sum(sizes) - memo[index] = addr - return addr - - def __lenof(self, index, memo): - """Return element length for index.""" - _, value = self.__valueof(index, memo) - if isinstance(value, PayloadEntry): - return value.payload_len(self) - return len(value) - - def __bytesof(self, index, memo): - """Return byte output for index.""" - _, value = self.__valueof(index, memo) - 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) - - try: - # Confirm value supports our required operations - len(value) - bytes(value) - except TypeError as ex: - raise TypeError(f"Payload: Bad type {type(value)} given") from ex - - return value diff --git a/nsploit/payload/payload_entry.py b/nsploit/payload/payload_entry.py deleted file mode 100644 index 31eb4f8..0000000 --- a/nsploit/payload/payload_entry.py +++ /dev/null @@ -1,113 +0,0 @@ -from nsploit.arch import arch, itob -from nsploit.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_len(self, payload): - """ - Called to compute size of this entry. - - Implement this method to calculate the length of this dynamic payload - entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - - def payload_bytes(self, payload): - """ - Called to generate bytes for this entry. - - Implement this method to generate the binary output for this dynamic - payload entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - -# Concrete payload entry definitions - -class pointer(PayloadEntry): - """Generate an integer which tracks the address of another payload field.""" - - def __init__(self, target=None, math=None): - self.target = target - self.math = math - - def payload_len(self, payload): - return arch.wordsize - - def payload_bytes(self, payload): - if self.target is None: - addr = self.base - else: - addr = payload[self.target] - if callable(self.math): - addr = self.math(addr) - return itob(addr) - -class padlen(PayloadEntry): - """Generate padding to reach a target payload length.""" - - def __init__(self, size, data=None): - self.size = size - self.data = data - - def payload_len(self, payload): - return self.size - (self.base - payload.base) - - def payload_bytes(self, payload): - size = self.payload_len(payload) - 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) - -class padabs(padlen): - """Generate padding to reach a target absolute address.""" - - def payload_len(self, payload): - return self.size - self.base - -class padrel(padlen): - """Generate a fixed length of padding (aka: length relative to self).""" - - def payload_len(self, payload): - return self.size - -class padalign(padlen): - """Generate padding to reach next aligned address.""" - - def __init__(self, size=None, data=None, reference=0): - self.size = size - self.data = data - self.reference = reference - - def payload_len(self, payload): - size = self.size or arch.alignment - return (self.reference - 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_len(self, payload): - return self.size or arch.wordsize diff --git a/nsploit/tech/fmtstring.py b/nsploit/tech/fmtstring.py index 1ec4609..6ac74c5 100644 --- a/nsploit/tech/fmtstring.py +++ b/nsploit/tech/fmtstring.py @@ -30,8 +30,8 @@ See `man 3 printf` for more details. """ from nsploit.arch import arch, btoi, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padrel +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padrel _FMTSTR_MAGIC = b"\xcd" diff --git a/nsploit/tech/ret2dlresolve.py b/nsploit/tech/ret2dlresolve.py index 8446ead..4e9aff4 100644 --- a/nsploit/tech/ret2dlresolve.py +++ b/nsploit/tech/ret2dlresolve.py @@ -73,11 +73,11 @@ Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB """ from nsploit.arch import arch, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen, pointer from nsploit.rev.r2 import run_cmd from nsploit.tech.gadhint import GadHint from nsploit.tech.rop import ROP +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padlen, pointer _JMP_SLOT = 0x07 diff --git a/nsploit/tech/rop.py b/nsploit/tech/rop.py index c5b3ef8..a2c348e 100644 --- a/nsploit/tech/rop.py +++ b/nsploit/tech/rop.py @@ -1,9 +1,9 @@ from graphlib import TopologicalSorter from nsploit.arch import arch, btoi, itob -from nsploit.payload.payload import Payload -from nsploit.payload.payload_entry import padalign, padlen from nsploit.tech.gadhint import GadHint +from nsploit.types.payload import Payload +from nsploit.types.payload_entry import padalign, padlen _POP_MAGIC = 0xdead _SPM_MAGIC = b"\x69" diff --git a/nsploit/types/__init__.py b/nsploit/types/__init__.py index a618162..9f2fb30 100644 --- a/nsploit/types/__init__.py +++ b/nsploit/types/__init__.py @@ -1,3 +1,5 @@ from .indextbl import * from .index_entry import * from .lict import * +from .payload import * +from .payload_entry import * diff --git a/nsploit/types/payload.py b/nsploit/types/payload.py new file mode 100644 index 0000000..2f522b4 --- /dev/null +++ b/nsploit/types/payload.py @@ -0,0 +1,224 @@ +from nsploit.arch import itob +from nsploit.types.indextbl import IndexTbl +from nsploit.types.index_entry import IndexEntry +from nsploit.types.lict import Lict +from nsploit.types.payload_entry import PayloadEntry + +_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)}" + memo = {} + + 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, memo) + data = str(self.__bytesof(i, memo)) + 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.""" + memo = {} + x = [ self.__bytesof(i, memo) 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.""" + memo = {} + x = [ self.__lenof(i, memo) for i in range(len(self.__entries__)) ] + return sum(x) + + 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, memo): + """Return a tuple (addr of value, literal value) for index.""" + value = self.__entries__[index] + addr = self.__addrof(index, memo) + if isinstance(value, IndexEntry): + value @= addr + return value, value + return addr, value + + def __addrof(self, index, memo): + """Return address (base + offset) for index.""" + index = self.__entries__.key2idx(index) + try: + return memo[index] + except KeyError: + sizes = [ self.__lenof(i, memo) for i in range(index) ] + addr = self.base + sum(sizes) + memo[index] = addr + return addr + + def __lenof(self, index, memo): + """Return element length for index.""" + _, value = self.__valueof(index, memo) + if isinstance(value, PayloadEntry): + return value.payload_len(self) + return len(value) + + def __bytesof(self, index, memo): + """Return byte output for index.""" + _, value = self.__valueof(index, memo) + 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) + + try: + # Confirm value supports our required operations + len(value) + bytes(value) + except TypeError as ex: + raise TypeError(f"Payload: Bad type {type(value)} given") from ex + + return value diff --git a/nsploit/types/payload_entry.py b/nsploit/types/payload_entry.py new file mode 100644 index 0000000..31eb4f8 --- /dev/null +++ b/nsploit/types/payload_entry.py @@ -0,0 +1,113 @@ +from nsploit.arch import arch, itob +from nsploit.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_len(self, payload): + """ + Called to compute size of this entry. + + Implement this method to calculate the length of this dynamic payload + entry. self.base is set to the current entry address or offset. + """ + raise NotImplementedError + + def payload_bytes(self, payload): + """ + Called to generate bytes for this entry. + + Implement this method to generate the binary output for this dynamic + payload entry. self.base is set to the current entry address or offset. + """ + raise NotImplementedError + +# Concrete payload entry definitions + +class pointer(PayloadEntry): + """Generate an integer which tracks the address of another payload field.""" + + def __init__(self, target=None, math=None): + self.target = target + self.math = math + + def payload_len(self, payload): + return arch.wordsize + + def payload_bytes(self, payload): + if self.target is None: + addr = self.base + else: + addr = payload[self.target] + if callable(self.math): + addr = self.math(addr) + return itob(addr) + +class padlen(PayloadEntry): + """Generate padding to reach a target payload length.""" + + def __init__(self, size, data=None): + self.size = size + self.data = data + + def payload_len(self, payload): + return self.size - (self.base - payload.base) + + def payload_bytes(self, payload): + size = self.payload_len(payload) + 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) + +class padabs(padlen): + """Generate padding to reach a target absolute address.""" + + def payload_len(self, payload): + return self.size - self.base + +class padrel(padlen): + """Generate a fixed length of padding (aka: length relative to self).""" + + def payload_len(self, payload): + return self.size + +class padalign(padlen): + """Generate padding to reach next aligned address.""" + + def __init__(self, size=None, data=None, reference=0): + self.size = size + self.data = data + self.reference = reference + + def payload_len(self, payload): + size = self.size or arch.alignment + return (self.reference - 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_len(self, payload): + return self.size or arch.wordsize -- cgit v1.2.3