diff options
Diffstat (limited to 'sploit')
-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 |