diff options
author | Malfurious <m@lfurio.us> | 2025-01-01 07:22:26 -0500 |
---|---|---|
committer | Malfurious <m@lfurio.us> | 2025-01-01 07:22:26 -0500 |
commit | 70c7c16a157f0e2056d0b96b96f6e13c83841bc3 (patch) | |
tree | 5f6d84642fc8b0aa89a32ef17f4b374605c7e089 | |
parent | f01ec45e773291c3659a1dcaf8cd9a51ece19823 (diff) | |
parent | 438c66673f7daca0fdc2d23b1a4fd39517528576 (diff) | |
download | nsploit-70c7c16a157f0e2056d0b96b96f6e13c83841bc3.tar.gz nsploit-70c7c16a157f0e2056d0b96b96f6e13c83841bc3.zip |
Merge branch 'indextbl'
This branch is a major semantic redesign of Symtbl and Payload. These
two classes are now implemented as derivitives of the newly refactored
IndexTbl mechanism.
Necessary cascading changes have been made to keep these tools in
working order.
* indextbl:
payload: rop: Update for new Payload class
Update ROP gadget types to extend IndexEntry
payload: Refactor as a concrete IndexTbl
lict: Add new list-dictionary hybrid type
symtbl: Refactor abstract IndexTbl interface
-rw-r--r-- | sploit/payload/__init__.py | 1 | ||||
-rw-r--r-- | sploit/payload/gadhint.py | 43 | ||||
-rw-r--r-- | sploit/payload/payload.py | 281 | ||||
-rw-r--r-- | sploit/payload/payload_entry.py | 99 | ||||
-rw-r--r-- | sploit/payload/rop.py | 245 | ||||
-rw-r--r-- | sploit/rev/gadget.py | 23 | ||||
-rw-r--r-- | sploit/symtbl.py | 112 | ||||
-rw-r--r-- | sploit/types/__init__.py | 3 | ||||
-rw-r--r-- | sploit/types/index_entry.py | 44 | ||||
-rw-r--r-- | sploit/types/indextbl.py | 103 | ||||
-rw-r--r-- | sploit/types/lict.py | 202 |
11 files changed, 826 insertions, 330 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/gadhint.py b/sploit/payload/gadhint.py index 9b077fe..1bef9f0 100644 --- a/sploit/payload/gadhint.py +++ b/sploit/payload/gadhint.py @@ -1,12 +1,15 @@ +import copy from dataclasses import dataclass, field + from sploit.rev.gadget import Gadget +from sploit.types.index_entry import IndexEntry @dataclass -class GadHint: +class GadHint(IndexEntry): """ User-annotated gadget description object - gadget (Gadget|int): The gadget being annotated. May be a Gadget object or + base (Gadget|int): The gadget being annotated. May be a Gadget object or an offset as an int. pops (list[str]): The registers popped by this gadget, in order of @@ -20,10 +23,10 @@ class GadHint: Keys are destination register names, values are immediate values. The order given is insignificant. - writes (dict{str:str}): The register-to-memory moves (stores) made by this - gadget. Keys are destination register names (expected to hold memory - locations), values are source register names (expected to hold direct - values). The order given is insignificant. + writes (dict{str:str}): The register-to-memory stores made by this gadget. + Keys are the destination register names (which hold memory addresses), + values are source register names (which hold values to-be-stored). The + order given is insignificant. requirements (dict{str:int}): The register state that is required before this gadget should be executed. Keys are register names, values are the @@ -43,7 +46,7 @@ class GadHint: should not be accounted for. A value of zero is taken as "unspecified". """ - gadget: int = 0 + base: int = 0 pops: list = field(default_factory=list) movs: dict = field(default_factory=dict) imms: dict = field(default_factory=dict) @@ -57,21 +60,7 @@ class GadHint: @property def offset(self): """Return gadget offset as an integer.""" - return int(self.gadget) - - def __index__(self): - """Convert object to integer using offset value.""" - return self.offset - - def __add__(self, x): - """Return new object with adjusted offset.""" - return GadHint(self.gadget + x, self.pops, self.movs, self.imms, - self.writes, self.requirements, self.stack, self.align, - self.syscall, self.spm) - - def __sub__(self, x): - """Return new object with adjusted offset.""" - return self + (-x) + return int(self.base) def with_requirements(self, reqs): """Return new object with additional requirements.""" @@ -81,9 +70,9 @@ class GadHint: f"GadHint: Conflicting gadget requirements: " f"{self.requirements}, {reqs}") - return GadHint(self.gadget, self.pops, self.movs, self.imms, - self.writes, self.requirements | reqs, self.stack, - self.align, self.syscall, self.spm) + new = copy.deepcopy(self) + new.requirements |= reqs + return new def __repr__(self): """Return human-readable GadHint.""" @@ -92,8 +81,8 @@ class GadHint: return f", {name}={prop}" return "" - s = hex(self.gadget) - s = f"Gadget({s})" if type(self.gadget) is Gadget else s + s = hex(self.base) + s = f"Gadget({s})" if isinstance(self.base, Gadget) else s s += fmt("pops", self.pops) s += fmt("movs", self.movs) s += fmt("imms", self.imms) 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) diff --git a/sploit/payload/rop.py b/sploit/payload/rop.py index 54226b4..30467de 100644 --- a/sploit/payload/rop.py +++ b/sploit/payload/rop.py @@ -1,78 +1,68 @@ -""" -ROP chain generation utilities - -This module contains tools for automating basic return-oriented-programming -workloads, such as loading register values and calling into arbitrary functions -or syscalls. The tools are currently designed to work on x86 (32 or 64 bit) -and ARM (32 bit only). - -The main appeal of the ROP class is the ability to abstract away the manual -construction of ROP chain data, and instead make declarative statements like -"call this function with these arguments." The ROP class will also utilize its -supplied binary objects to automatically find and use trivial gadgets. - -The user is able to provide annotations for more complicated gadgets, which help -instruct the class how to incorporate them into a ROP chain. This is done with -the GadHint dataclass. GadHint objects are provided to a ROP instance by -including them in the Symtbl of one of the binary objects it is constructed with. -If applicable, a user-supplied gadget will take precedence over automatic gadget -searching. - -See the GadHint class to learn more about the descriptive attributes that are -supported. -""" - from graphlib import TopologicalSorter from sploit.arch import arch, btoi, itob from sploit.payload.gadhint import GadHint from sploit.payload.payload import Payload +from sploit.payload.payload_entry import padalign, padlen -class ROP(Payload): - """ - ROP-enabled payload builder - - POP_MAGIC (int): Magic value used for pop instructions where no specific - value is required by the user. +_POP_MAGIC = 0xdead +_SPM_MAGIC = b"\x69" +_ERROR_MAGIC = 0xbaadc0de - SPM_MAGIC (bytes): Magic value to fill the stack with when the best - available cleaning gadget is larger than is necessary. - - objects (list[ELF]): The binary objects this ROP instance will consider - for gadget searching. +class ROP: + """ + ROP chain generation tool + + This class contains methods for automating basic return-oriented programming + workloads, such as loading register values and calling into arbitrary + functions or syscalls. The tools are currently designed to work on x86 + (32 or 64 bit) and ARM (32 bit only). + + The main appeal of the ROP class is the ability to abstract away the manual + construction of ROP chain data, and instead make declarative statements + like "call this function with these arguments". The ROP class will also + utilize its supplied binary objects to automatically find and use trivial + gadgets. + + The user is able to provide annotations for more complicated gadgets, which + help instruct the class how to incorporate them into a ROP chain. This is + done with the GadHint dataclass. GadHint objects are provided to a ROP + instance by including them in the Symtbl of one of the binary objects it is + constructed with. If applicable, a user-supplied gadget will take + precedence over automatic gadget searching. See the GadHint module to learn + more about the descriptive attributes that are supported. + + objects (list[ELF]): The binary objects this ROP instance will consider for + gadget searching. If one of these is the target executable binary, it + should appear first in the list. safe_syscalls (bool): If True, require that automatically found syscall instructions are immediately followed by a return instruction. align_calls (bool): If True, ensure that the stack return address into function calls is aligned according to the architecture alignment property. - Knowledge of alignment is taken from the instance Symtbl's base value. clean_stack (bool): If True, attempt to locate a cleaning gadget to "pop" stack data that is leftover from a function call. Required if attempting to make multiple calls that involve stack-based arguments. """ - POP_MAGIC = 0xdead - SPM_MAGIC = b'\x69' - def __init__(self, *objects, safe_syscalls=True, align_calls=True, - clean_stack=True, **symbols): - """Initialize new ROP builder instance.""" - super().__init__(**symbols) + clean_stack=True): + """Construct new ROP builder.""" self.objects = objects self.safe_syscalls = safe_syscalls self.align_calls = align_calls self.clean_stack = clean_stack - def gadgets(self, *regexes, cont=False): + def search_gadgets(self, *regexes, cont=False): """Return a list of matching gadgets, considering all objects.""" results = [] for obj in self.objects: results += obj.gadgets(*regexes, cont=cont) return results - def gadget(self, *regexes): + def search_gadget(self, *regexes): """Return the first matching gadget, considering all objects.""" for obj in self.objects: try: @@ -82,75 +72,70 @@ class ROP(Payload): raise LookupError( f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") - def assign(self, *, sym=None, **sets): + def gadget(self, gadget): """ - Insert a ROP chain to control given registers. + Return a generic ROP payload. + + gadget (GadHint): Annotated gadget to prepare a chain from. + """ + return self.__build_chain(gadget, {}) + + def assign(self, **sets): + """ + Return a ROP payload to control given registers. **sets (str:int): Keyword arguments specify register assignments to perform with this ROP chain. Argument names correspond to register names. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ - gadget = GadHint(0, requirements=sets) - return self._start_chain(gadget, sym=self._name("assign", sym)) + return self.gadget(GadHint(requirements=sets)) - def call(self, func, *params, sym=None): + def call(self, func, *params): """ - Insert a ROP chain to call function. + Return a ROP payload to call function. func (int): Entry address of function to call. *params (int): Remaining positional args are passed to func. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ register_params = dict(zip(arch.funcargs, params)) stack_params = params[len(register_params):] gadget = GadHint(func, requirements=register_params, stack=stack_params, align=self.align_calls) - return self._start_chain(gadget, sym=self._name("call", sym)) + return self.gadget(gadget) - def syscall(self, *params, sym=None): + def syscall(self, *params): """ - Insert a ROP chain to call kernel. + Return a ROP payload to call kernel. *params (int): The first argument is the syscall number. Remaining positional arguments are passed to the syscall. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ if len(params) > len(arch.kernargs): raise TypeError("ROP: Too many arguments passed to syscall. " - "Target architecture supports up to {len(arch.kernargs)-1}.") + f"Target architecture supports up to {len(arch.kernargs)-1}.") register_params = dict(zip(arch.kernargs, params)) - gadget = self._get_gadget("syscall", {}).with_requirements(register_params) - return self._start_chain(gadget, sym=self._name("syscall", sym)) + sc = self.__get_gadget("syscall", {}) + return self.gadget(sc.with_requirements(register_params)) - def memcpy(self, dst, src, *, sym=None): + def memcpy(self, dst, src): """ - Insert a ROP chain to write data into memory. + Return a ROP payload to write data into memory. dst (int): The destination memory address. src (bytes): The content to write. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. """ - gadgets = [] + data = Payload() for idx in range(0, len(src), arch.wordsize): - g = self._get_write(dst + idx, btoi(src[idx:idx+arch.wordsize])) - gadgets.append(g) - return self._start_chain(*gadgets, sym=self._name("memcpy", sym)) + word = btoi(src[idx:idx+arch.wordsize]) + data(self.gadget(self.__get_write(dst+idx, word))) + return data - def _get_hints(self): + def __get_hints(self): """Return all user-supplied gadget hints.""" return [h for obj in self.objects for _,h in obj.sym if type(h) is GadHint] - def _discover_requirements(self, seen, graph, current): + def __discover_requirements(self, seen, graph, current): """ Populate gadget dependency graph. @@ -170,13 +155,13 @@ class ROP(Payload): # Requiring a register to store different values may require the # use of multiple gadgets. if (r, v) not in seen: - gadget = self._get_gadget(r, current.requirements) + gadget = self.__get_gadget(r, current.requirements) # Add gadget's requirements to the dependency graph. # We say that each requirement is a 'successor' to this # current gadget 'r', so that the chain builder will satisfy # 'r' first. This prevents the fulfillment of 'r' from - # colbbering targets it requires, as the builder will satisfy + # clobbering targets it requires, as the builder will satisfy # them afterward. for x in gadget.requirements: graph.add(x, r) @@ -189,9 +174,9 @@ class ROP(Payload): # Mark node as visited seen.add((r, v)) - self._discover_requirements(seen, graph, gadget) + self.__discover_requirements(seen, graph, gadget) - def _get_gadget(self, target, sets): + def __get_gadget(self, target, sets): """ Get context-specific gadget. @@ -204,7 +189,7 @@ class ROP(Payload): """ # First, consider user-provided hints before automatically locating # gadgets. - for hint in self._get_hints(): + for hint in self.__get_hints(): # Setup additional requirements based on hint's register moves. # If a mov target is in sets, require to set the src to the 'sets' # value. @@ -229,17 +214,17 @@ class ROP(Payload): # Automatically locate simple gadgets if target == "ret": - return GadHint(self.gadget(arch.ret)) + return GadHint(self.search_gadget(arch.ret)) if target == "syscall": insns = [arch.syscall, arch.ret] if self.safe_syscalls else [arch.syscall] - return GadHint(self.gadget(*insns), syscall=True) + return GadHint(self.search_gadget(*insns), syscall=True) # target == register insns = [ i.format(target) for i in arch.popgad ] - return GadHint(self.gadget(*insns), pops=[target], spm=arch.wordsize) + return GadHint(self.search_gadget(*insns), pops=[target]) - def _get_clean(self, size): + def __get_clean(self, size): """ Get a stack cleaning gadget that moves sp by _at least_ size. @@ -249,11 +234,11 @@ class ROP(Payload): # the user likely hasn't annotated the GadHint properly. Returning a # larger move than requested is fine, since the chain builder can insert # junk to be popped. - for hint in self._get_hints(): + for hint in self.__get_hints(): if hint.spm >= size and hint.spm > 0: return hint - results = self.gadgets(*arch.cleangad) + results = self.search_gadgets(*arch.cleangad) table = { int(g.asm[0].group(1), 0): g for g in results } sizes = sorted([ x for x in table.keys() if x >= size ]) @@ -263,7 +248,7 @@ class ROP(Payload): raise LookupError( f"ROP: Need to define a stack move gadget of at least {size}") - def _get_write(self, dst, src): + def __get_write(self, dst, src): """ Get a memory write gadget, injected with requirements for user data. @@ -273,7 +258,7 @@ class ROP(Payload): # If any exist, take the first write provided by user hints, assuming # the user's intent to specifically use _this_ write. Follow-on gadgets # to setup the dst and src registers must be findable. - for hint in self._get_hints(): + for hint in self.__get_hints(): if hint.writes: d, s = list(hint.writes.items())[0] return hint.with_requirements({d:dst, s:src}) @@ -281,7 +266,7 @@ class ROP(Payload): # Only take an automatic write gadget if we can prove up front that its # requirements can be met, otherwise move on. A later search result may # pass the test. - results = self.gadgets(*arch.writegad) + results = self.search_gadgets(*arch.writegad) for gad in results: d = gad.asm[0].group("dst") @@ -290,52 +275,45 @@ class ROP(Payload): try: # Assert requirements are met. gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) - self._discover_requirements(set(), TopologicalSorter(), gadget) + self.__discover_requirements(set(), TopologicalSorter(), gadget) return gadget except: pass raise LookupError("ROP: Need to define gadgets for memory write / deps") - def _start_chain(self, *gadgets, sym=None): - """ - Insert a generic ROP chain. - - *gadgets (GadHint): Annotated gadgets to prepare a chain from. - - sym (str): If given, sym is the symbol name used to refer to the - inserted data. + def __build_chain(self, gadget, sets): """ - stack = Payload(base=self.end()) - for g in gadgets: - self._build_chain(stack, g, {}) - return self.bin(stack(), sym=self._name("gadget", sym)) - - def _build_chain(self, stack, gadget, sets): - """ - Generate chain data for a given ROP gadget. + Generate ROP chain data for a given gadget. This function recursively builds a ROP chain for the given gadget and - its requirements, storing data in the 'stack' object. + its requirements, returning the result as a Payload. - stack (Payload): Stack data being constructed. - gadget (GadHint): Current gadget we are processing. + gadget (GadHint): Current gadget to process. sets (dict{str:int}): The set of other register requirements we are trying to fulfill in parallel. """ # Form a to-do-list of registers from our immediate requirements, # attempting to order them such that we avoid overwriting/conflicting - # values (this may not be possible). + # values. This may not be possible, in which case graph.static_order() + # will raise an exception. reqs = gadget.requirements graph = TopologicalSorter({ r:set() for r in reqs }) - self._discover_requirements(set(), graph, gadget) + self.__discover_requirements(set(), graph, gadget) to_do_list = [ x for x in graph.static_order() if x in reqs ] + chain = Payload() + # Start chain by satisfying to-do-list requirements. + if len(to_do_list) > 0: + chain.requirements = Payload() + while len(to_do_list) > 0: - g = self._get_gadget(to_do_list[0], reqs) - self._build_chain(stack, g, reqs) + r = to_do_list[0] + g = self.__get_gadget(r, reqs) + c = self.__build_chain(g, reqs) + chain.requirements[f"{r}_{reqs[r]}"] = c # This gadget may satisfy multiple items in the to-do-list. # Specifically, all of its pop and mov targets, and any load @@ -351,33 +329,36 @@ class ROP(Payload): if gadget.offset != 0: # Stack alignment if required. if gadget.align: - align = -stack.end() % arch.alignment - stack.rep(itob(self._get_gadget("ret", {})), align) + ret = self.__get_gadget("ret", {}) + chain.alignment = padalign(0, itob(ret)) # "Return address" entry into this gadget. - stack.ret(gadget.offset) + chain.gadget = gadget.offset - # The gadget's "inner stack data" will be values to be popped - # and additional junk data to be deallocated by the gadget itself. - sp_dest = len(stack) + gadget.spm - stack.int(*[ sets.get(p, self.POP_MAGIC) for p in gadget.pops ]) - if gadget.spm > 0: - stack.pad(sp_dest, self.SPM_MAGIC) + # The gadget's "inner stack data" will be values to be popped and + # additional junk data to be deallocated by the gadget itself. + if gadget.pops or gadget.spm > 0: + chain.inner = Payload() + chain.inner(*[ sets.get(p, _POP_MAGIC) for p in gadget.pops ]) + if gadget.spm > 0: + chain.inner.pad = padlen(gadget.spm, _SPM_MAGIC) # The gadget's "outer stack data" will be the additional values # explicitly specified by the gadget. Append a separate gadget # to clean up these values. - if len(gadget.stack) > 0: + if gadget.stack: size = len(gadget.stack) * arch.wordsize if self.clean_stack: - clean = self._get_clean(size) - stack.ret(clean) - sp_dest = len(stack) + clean.spm + clean = self.__get_clean(size) + chain.cleanup = clean.offset + pad = padlen(clean.spm, _SPM_MAGIC) else: - ret = self._get_gadget("ret", {}) - stack.ret(ret) - sp_dest = len(stack) + size + chain.cleanup = _ERROR_MAGIC + pad = None + + chain.outer = Payload() + chain.outer(*gadget.stack) + if pad: chain.outer.pad = pad - stack.int(*gadget.stack) - stack.pad(sp_dest, self.SPM_MAGIC) + return chain diff --git a/sploit/rev/gadget.py b/sploit/rev/gadget.py index a2564c0..cc69723 100644 --- a/sploit/rev/gadget.py +++ b/sploit/rev/gadget.py @@ -1,35 +1,24 @@ from dataclasses import dataclass, field +from sploit.types.index_entry import IndexEntry @dataclass -class Gadget: +class Gadget(IndexEntry): """ Basic gadget description object - offset (int): The location this gadget is found at. What `offset` is - relative to depends on context. + base (int): The location this gadget is found at. What `base` is relative + to depends on context. asm (list[re.Match]): A list of assembly instructions matched by the gadget search query. """ - offset: int = 0 + base: int = 0 asm: list = field(default_factory=list) - def __index__(self): - """Convert object to integer using offset value.""" - return self.offset - - def __add__(self, x): - """Return new object with adjusted offset.""" - return Gadget(self.offset + x, self.asm) - - def __sub__(self, x): - """Return new object with adjusted offset.""" - return self + (-x) - def __repr__(self): """Return human-readable Gadget.""" - s = hex(self.offset) + s = hex(self.base) if len(self.asm) > 0: asm = "; ".join([ m.string for m in self.asm ]) s += f", '{asm}'" diff --git a/sploit/symtbl.py b/sploit/symtbl.py index a471958..86800f5 100644 --- a/sploit/symtbl.py +++ b/sploit/symtbl.py @@ -73,112 +73,88 @@ with the Symtbl base address. print(s.a, s.b, s.c, s.d) # "998 999 1000 1001" """ +from sploit.types.indextbl import IndexTbl +from sploit.types.index_entry import IndexEntry + def Symtbl(*, base=0, **symbols): """ Create a new Symtbl object. Return an empty Symtbl or, optionally, one initialized with the given - symbol values. Arguments _must_ be keyword arguments. + symbol values. Arguments must be keyword arguments. Users should call this function instead of attempting to construct the Symtbl class. Construction is implemented via a normal function to prevent any argument name from conflicting with __init__'s bound instance parameter. """ - self = SymtblImpl({}, 0, base) + self = SymtblImpl(base, 0, dict()) for k, v in symbols.items(): self[k] = v return self -class SymtblImpl: +class SymtblImpl(IndexTbl): """Symtbl implementation class""" - def __init__(self, entries, adjust, base): + def __init__(self, base, adjust, entries): """Construct Symtbl from instance data.""" + super().__init__(base) + object.__setattr__(self, "__adjust__", adjust) object.__setattr__(self, "__entries__", entries) - object.__setattr__(self, "__adjust__", adjust) - object.__setattr__(self, "base", base) - def __index__(self): - """Convert object to integer using base value.""" - return self.base + def __repr__(self): + """Return human-readable Symtbl.""" + FMT = "\n{:<20} {:<20}" + s = f"{len(self)} symbols @ {hex(self)}" - def __matmul__(self, base): - """Create remapped version of object at absolute base.""" - return SymtblImpl(self.__entries__, self.__adjust__, int(base)) + if len(self) > 0: + s += FMT.format("ADDRESS", "SYMBOL") - def __add__(self, offset): - """Create remapped version of object at relative base.""" - return self @ (self.base + offset) + for key, value in self: + key = f"[{key}]" if isinstance(value, IndexEntry) else key + s += FMT.format(hex(value), key) - def __sub__(self, offset): - """Create remapped version of object at relative base.""" - return self @ (self.base - offset) + return s def __rshift__(self, offset): - """Create symbol adjusted version of object.""" - return SymtblImpl(self.__entries__, self.__adjust__ + int(offset), self.base) + """Return symbol-adjusted version of object.""" + adjust = self.__adjust__ + int(offset) + return SymtblImpl(self.base, adjust, self.__entries__) def __lshift__(self, offset): - """Create symbol adjusted version of object.""" + """Return symbol-adjusted version of object.""" return self >> (-offset) def __mod__(self, offset): - """Create symbol rebased version of object.""" + """Return symbol-rebased version of object.""" return self >> (self.base - offset) - def __getattr__(self, symbol): - """Return symbol offset or subtable via pseudo-attribute.""" - return self[symbol] + # IndexTbl abstract methods - def __setattr__(self, symbol, value): - """Set symbol offset or subtable via pseudo-attribute.""" - self[symbol] = value + def __copy__(self): + """Return copy of object with shared symbol entries.""" + return SymtblImpl(self.base, self.__adjust__, self.__entries__) - def __delattr__(self, symbol): - """Unset symbol via pseudo-attribute.""" - del self[symbol] + def __iter__(self): + """Iterate over table items, sorted by offsets.""" + it = { k: self[k] for k in self.__entries__}.items() + return iter(sorted(it, key=lambda x: int(x[1]))) def __len__(self): """Return number of defined symbols.""" return len(self.__entries__) - def __getitem__(self, symbol): - """Return symbol offset, subtable, or translated offset via subscript.""" - if symbol == "base": - return self.base - offset = self.__entries__[symbol] if type(symbol) is str else symbol + def __getindex__(self, index): + """Return symbol value or translated offset.""" + if isinstance(index, (int, IndexEntry)): offset = index + else: offset = self.__entries__[index] return offset + (self.base + self.__adjust__) - def __setitem__(self, symbol, value): - """Set symbol offset or subtable via subscript.""" - if symbol == "base": - object.__setattr__(self, "base", int(value)) - elif symbol in dir(self): - raise KeyError(f"Symtbl: key is reserved: {symbol}") - elif type(symbol) is not str: - raise TypeError(f"Symtbl: key must be a string: {symbol}") - else: - self.__entries__[symbol] = value - (self.base + self.__adjust__) - - def __delitem__(self, symbol): - """Unset symbol via subscript.""" - del self.__entries__[symbol] + def __setindex__(self, index, value): + """Set symbol value.""" + if isinstance(index, (int, IndexEntry)): + raise TypeError(f"Symtbl: Unsupported key type: {type(index)}") + self.__entries__[index] = value - (self.base + self.__adjust__) - def __iter__(self): - """Iterate over table entries as key:value tuples, like dict.items().""" - return iter(sorted({ k: self[k] for k in self.__entries__ }.items(), key=lambda v: int(v[1]))) - - def __contains__(self, symbol): - """Test symbol name membership in table.""" - return symbol in self.__entries__ - - def __repr__(self): - """Return human-readable Symtbl.""" - FMT = "\n{:<20} {:<20}" - s = f"{len(self)} symbols @ {hex(self)}" - if len(self) > 0: - s += FMT.format("ADDRESS", "SYMBOL") - for symbol, offset in self: - disp = f"[{symbol}]" if type(offset) is not int else symbol - s += FMT.format(hex(offset), disp) - return s + def __delindex__(self, index): + """Delete symbol.""" + del self.__entries__[index] diff --git a/sploit/types/__init__.py b/sploit/types/__init__.py new file mode 100644 index 0000000..a618162 --- /dev/null +++ b/sploit/types/__init__.py @@ -0,0 +1,3 @@ +from .indextbl import * +from .index_entry import * +from .lict import * diff --git a/sploit/types/index_entry.py b/sploit/types/index_entry.py new file mode 100644 index 0000000..a03ab92 --- /dev/null +++ b/sploit/types/index_entry.py @@ -0,0 +1,44 @@ +import copy + +class IndexEntry: + """ + Generic IndexTbl entry object + + IndexEntry is intended to be subclassed to create interesting types that are + compatible with IndexTbl directories. IndexEntry gives objects a baseline + int-like personality. + + IndexEntry objects are convertable to int via int(), bin(), hex(), etc. This + integer value is manipulated via the object's "base" property, and a few + operators are implemented to provide nicer syntax for this as well. The use + of operators generally yield distinct copies of the original object. + + The property name "base" is used since it has semantic meaning for the + IndexTbl class, which is itself an extension of this class. + + base (int): Index integer value + """ + + base = 0 + + def __init__(self, base=0): + """Construct index with the given base value.""" + self.base = base + + def __index__(self): + """Convert index to base integer value.""" + return int(self.base) + + def __matmul__(self, base): + """Create new object with the given base value.""" + new = copy.copy(self) + new.base = base + return new + + def __add__(self, add): + """Create new object with the given relative base value.""" + return self @ (self.base + add) + + def __sub__(self, sub): + """Create new object with the given relative base value.""" + return self @ (self.base - sub) diff --git a/sploit/types/indextbl.py b/sploit/types/indextbl.py new file mode 100644 index 0000000..4f57a59 --- /dev/null +++ b/sploit/types/indextbl.py @@ -0,0 +1,103 @@ +from abc import abstractmethod +from collections.abc import Collection + +from sploit.types.index_entry import IndexEntry + +class IndexTbl(IndexEntry, Collection): + """ + Abstract Index Table + + IndexTbl is a common interface to an abstracted key-value store. The + storage mechanism as well as lookup semantics are defined by concrete + implementations. "Index" in this case is more akin to a directory index or + book index than a strictly numeric array index. + + In general, concrete tables may store values of any or multiple different + types. In particular, tables should give special accommodation for values + of type IndexEntry. These objects usually represent "rich" versions of the + nominal data types the table expects to contain. Implementation repr() + methods usually also annotate which members are IndexEntries. + + IndexTbl extends from IndexEntry, and so has a base value which represents + the "base index" of the table. The meaning of this depends on the + implementation. This inheritance also means that tables are generally + expected to be nestable. + + IndexTbls allow indices to be accessed via attribute or subscript notation. + This is probably the key characteristic feature of the class. The class + namespace is kept as clean as possible to make for the fewest collisions + between index names and other (real) class attributes. Note that there are + abstract methods, required to be overridden, which implement the index + access. These methods are only called for normal indices, not the table + base. + + Because this class overrides attribute access, normal automatic object + copying is broken. Because of this, implementations must also provide a + definition for the __copy__() method as well. + """ + + @abstractmethod + def __getindex__(self, index): + """Lookup and retrieve index value.""" + raise NotImplementedError + + @abstractmethod + def __setindex__(self, index, value): + """Lookup and set index value.""" + raise NotImplementedError + + @abstractmethod + def __delindex__(self, index): + """Lookup and delete index value.""" + raise NotImplementedError + + @abstractmethod + def __copy__(self): + """Create a copy of this IndexTbl object.""" + raise NotImplementedError + + def __contains__(self, index): + """Test the existence of the given index.""" + try: + self.__getindex__(index) + except KeyError: + return False + else: + return True + + # Attribute access methods + + def __getattr__(self, index): + """Get value via attribute.""" + return self[index] + + def __setattr__(self, index, value): + """Set value via attribute.""" + self[index] = value + + def __delattr__(self, index): + """Delete value via attribute.""" + del self[index] + + # Subscript/item access methods + + def __getitem__(self, index): + """Get value via subscript.""" + if index == "base": + return self.base + return self.__getindex__(index) + + def __setitem__(self, index, value): + """Set value via subscript.""" + if index == "base": + object.__setattr__(self, "base", value) + elif index in dir(self): + raise KeyError(f"IndexTbl: Index is reserved: {index}") + else: + self.__setindex__(index, value) + + def __delitem__(self, index): + """Delete value via subscript.""" + if index == "base": + raise KeyError("IndexTbl: May not delete index: base") + self.__delindex__(index) diff --git a/sploit/types/lict.py b/sploit/types/lict.py new file mode 100644 index 0000000..ab6cb1f --- /dev/null +++ b/sploit/types/lict.py @@ -0,0 +1,202 @@ +from collections.abc import MutableSequence, MutableMapping + +class Lict(MutableSequence, MutableMapping): + """ + List / dictionary hybrid container + + Lict attempts to provide an API for a list which supports optional element + keys in addition to normal positional indices. For Lict, index types are + int and slice. Keys may be any other type except for None, as None + indicates an unkeyed value. + + Nearly all of the operations you'd except to perform on list or dict are + implemented here. However, in cases of conflict, the list behavior is + usually preferred (for example: iter(Lict) iterates over values instead of + keys). In general, Licts are slightly more list-like, since keys are + optional, but sequence is required. + + Licts can be constructed from any iterable, including other Licts. When + constructing from mappings, similar heuristics as dict's are used to parse + key values. + + In addition to keys and indices, slices may be used to return, modify, or + delete a portion of a Lict. The start and end fields of a slice may be + either keys or indices, however the step field must be an integer as usual. + + When assigning to a non-existent key, the new element is sequentially + inserted at the end of the Lict. + """ + + def __init__(self, values=None): + """Construct new Lict, optionally populated by the given iterable.""" + self.__keys = [] + self.__vals = [] + if values is not None: + self.extend(values) + + def __repr__(self): + """Return human-readable Lict.""" + s = "" + for i in range(len(self)): + if self.__keys[i] is not None: + s += f"{repr(self.__keys[i])}: " + s += f"{repr(self.__vals[i])}, " + return f"{{[{s[:-2]}]}}" + + def __copy__(self): + """Return shallow copy of object.""" + return self.copy() + + def copy(self): + """Return shallow copy of object.""" + return Lict(self) + + def key2idx(self, arg): + """ + Return value of index type for the given input. + + For keys, return the corresponding index, or raise KeyError. + For slices, return a slice with components converted to index type. + + If arg is already an index (or None) it is returned as-is with no + assertions made. + """ + if isinstance(arg, slice): + return slice(self.key2idx(arg.start), self.key2idx(arg.stop), arg.step) + if isinstance(arg, int) or arg is None: + return arg + try: + return self.__keys.index(arg) + except ValueError as ex: + raise KeyError(f"Lict: Key does not exist: {arg}") from ex + + def idx2key(self, arg): + """ + Return value of key type for the given input. + + For indices, return the corresponding key, None, or raise IndexError. + For slices, return a slice with components converted to key type. + + If arg is already a key type (or None) it is returned as-is with no + assertions made. + """ + if isinstance(arg, slice): + return slice(self.idx2key(arg.start), self.idx2key(arg.stop), arg.step) + if not isinstance(arg, int) or arg is None: + return arg + return self.__keys[arg] + + def haskey(self, arg): + """ + Test existence of key in Lict object. + + `x in Lict` only tests for the _value_ x in Lict. This is consistent + with list behavior. This method is provided to test keys as well. + Raises TypeError on any index type. + """ + if arg is None: + return False + if isinstance(arg, (int, slice)): + raise TypeError(f"Lict: Unsupported key type: {type(arg)}") + return arg in self.__keys + + def __assign_slice(self, i, value): + """Update Lict values according to element slice.""" + value = Lict(value) + tmp = self.copy() + tmp.__keys[i] = value.__keys + tmp.__vals[i] = value.__vals + + check_keys = [ x for x in tmp.__keys if x is not None ] + if len(check_keys) != len(set(check_keys)): + raise ValueError("Lict: Slice assignment results in duplicate keys") + + self.__keys = tmp.__keys + self.__vals = tmp.__vals + + # collections.abc abstract methods + + def __len__(self): + """Return number of elements in Lict.""" + assert len(self.__keys) == len(self.__vals) + return len(self.__keys) + + def __getitem__(self, arg): + """Return value for given index, key, or slice.""" + i = self.key2idx(arg) + if isinstance(i, slice): + return Lict(zip(self.__keys[i], self.__vals[i])) + return self.__vals[i] + + def __setitem__(self, arg, value): + """Set value for given index, key, or slice.""" + try: + i = self.key2idx(arg) + except KeyError: + self.append(value, arg) + else: + if isinstance(i, slice): + self.__assign_slice(i, value) + else: + self.__vals[i] = value + + def __delitem__(self, arg): + """Delete value for given index, key, or slice.""" + i = self.key2idx(arg) + del self.__keys[i] + del self.__vals[i] + + def insert(self, where, value, key=None): + """Insert value into Lict. Optionally apply the given key.""" + if self.haskey(key): + raise KeyError(f"Lict: Key is not unique: {key}") + i = self.key2idx(where) + self.__keys.insert(i, key) + self.__vals.insert(i, value) + + # Sequence overrides + + def append(self, value, key=None): + """Append value to Lict. Optionally apply the given key.""" + self.insert(len(self), value, key) + + def extend(self, values): + """Append all values in given iterable to the Lict.""" + try: values = [ [k, v] for k, v in values.items() ] + except: + try: values = [ [k, v] for k, v in iter(values) ] + except: values = [ [None, v] for v in iter(values) ] + for k, v in values: + self.append(v, k) + + def reverse(self): + """Reverse the sequence of Lict in-place. Keys follow values.""" + self.__keys.reverse() + self.__vals.reverse() + + # Mapping overrides + + def get(self, key, default=None): + """Return value for given key, or default if unable.""" + try: return self[key] + except: return default + + def popitem(self): + """Pop a key-value pair from the Lict and return it.""" + return (self.__keys.pop(), self.__vals.pop()) + + def items(self): + """Return an iterable of key-value pairs.""" + return list(zip(self.__keys, self.__vals)) + + def keys(self): + """Return an iterable of the Lict's keys.""" + return [ x for x in self.__keys if x is not None ] + + def values(self): + """Return an iterable of the Lict's values.""" + return list(self.__vals) + + def update(self, values): + """Method is unsupported.""" + raise NotImplementedError |