diff options
Diffstat (limited to 'sploit')
-rw-r--r-- | sploit/__init__.py | 6 | ||||
-rw-r--r-- | sploit/__main__.py | 77 | ||||
-rw-r--r-- | sploit/arch.py | 153 | ||||
-rw-r--r-- | sploit/comm/__init__.py | 1 | ||||
-rw-r--r-- | sploit/comm/comm.py | 205 | ||||
-rw-r--r-- | sploit/payload/__init__.py | 6 | ||||
-rw-r--r-- | sploit/payload/fmtstring.py | 178 | ||||
-rw-r--r-- | sploit/payload/gadhint.py | 98 | ||||
-rw-r--r-- | sploit/payload/payload.py | 224 | ||||
-rw-r--r-- | sploit/payload/payload_entry.py | 113 | ||||
-rw-r--r-- | sploit/payload/ret2dlresolve.py | 226 | ||||
-rw-r--r-- | sploit/payload/rop.py | 364 | ||||
-rw-r--r-- | sploit/rev/__init__.py | 4 | ||||
-rw-r--r-- | sploit/rev/elf.py | 204 | ||||
-rw-r--r-- | sploit/rev/gadget.py | 25 | ||||
-rw-r--r-- | sploit/rev/ldd.py | 13 | ||||
-rw-r--r-- | sploit/rev/r2.py | 139 | ||||
-rw-r--r-- | sploit/symtbl.py | 160 | ||||
-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 | ||||
-rw-r--r-- | sploit/until.py | 14 | ||||
-rw-r--r-- | sploit/util/__init__.py | 2 | ||||
-rw-r--r-- | sploit/util/cmd.py | 26 | ||||
-rw-r--r-- | sploit/util/log.py | 32 |
26 files changed, 0 insertions, 2622 deletions
diff --git a/sploit/__init__.py b/sploit/__init__.py deleted file mode 100644 index dc5943f..0000000 --- a/sploit/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from sploit.arch import * -from sploit.symtbl import * -from sploit.until import * - -from sploit.util import git_version as __git_version -__version__ = __git_version() diff --git a/sploit/__main__.py b/sploit/__main__.py deleted file mode 100644 index 5d53ca6..0000000 --- a/sploit/__main__.py +++ /dev/null @@ -1,77 +0,0 @@ -from argparse import ArgumentParser, REMAINDER -import gc -from os.path import isdir -import tempfile -import traceback - -from sploit.comm.comm import * -from sploit.util.log import * -from sploit import __version__ - -def print_banner(color, line1=__version__, line2='', line3=''): - ilog() - ilog(' ░▒█▀▀▀█░▒█▀▀█░▒█░░░░▒█▀▀▀█░▀█▀░▀▀█▀▀ ', end='', color=ALT) - ilog(line1, color=ALT) - ilog(' ░░▀▀▀▄▄░▒█▄▄█░▒█░░░░▒█░░▒█░▒█░░░▒█░░ ', end='', color=color) - ilog(line2, color=ALT) - ilog(' ░▒█▄▄▄█░▒█░░░░▒█▄▄█░▒█▄▄▄█░▄█▄░░▒█░░ ', end='', color=ALT) - ilog(line3, color=ALT) - ilog() - -def main(): - parser = ArgumentParser(description='Execute Sploit script against target') - parser.add_argument('script', help='Exploit script to run') - parser.add_argument('target', nargs=REMAINDER, help='Target cmdline or pipes directory') - args = parser.parse_args() - - if len(args.target) == 0: - with tempfile.TemporaryDirectory() as tmpdir: - pipe(args.script, tmpdir) - elif len(args.target) == 1 and isdir(args.target[0]): - pipe(args.script, args.target[0]) - else: - target(args.script, args.target) - -def pipe(script, tmpdir): - print_banner(ERROR, line3='Pipe Mode') - while True: - try: - p = Pipes(tmpdir) - except KeyboardInterrupt: - break - runscript(script, Comm(p)) - del p - -def target(script, target): - print_banner(STATUS, line3='Subprocess Mode') - runscript(script, Comm(Process(target))) - -def user_scope(comm): - import sploit as lib - scope = { name: getattr(lib, name) for name in dir(lib) } - scope['__version__'] = __version__ - scope['print'] = elog - scope['io'] = comm - return scope - -def runscript(script, comm): - try: - ilog("Running Script...") - code = compile(open(script).read(), script, 'exec') - exec(code, user_scope(comm)) - ilog("Script Finished!") - return - except KeyboardInterrupt: - pass - except: - ilog(traceback.format_exc(), end='', color=ERROR) - finally: - comm.shutdown() - comm.readall() - gc.collect() - - ilog("Script Ended Early!", color=WARNING) - - -if __name__ == "__main__": - main() diff --git a/sploit/arch.py b/sploit/arch.py deleted file mode 100644 index a9dea61..0000000 --- a/sploit/arch.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -Architecture-aware utilities and global architecture config - -It is common within sploit and for users of sploit to need different behavior -depending on the architecture of the target. This module encapsulates those -behaviors and bases them on a global architecture that is also configured here. - -Users can set the global arch with arch.set() and all of the methods in this -module will honor it. An architecture can be defined through the Arch dataclass -and there are also several predefined architecture constants that can be used. -These are accessible by name at module scope. (i.e. sploit.arch.x86_64) - -arch (Arch): the architecture config that sploit will use whenever it needs to -know the architecture of the target - -DEFAULT_ARCH (Arch): the default architecture that arch is set to -""" - -from dataclasses import dataclass - -def __define_architectures(): - # All predefined architectures should be listed here - # These will also be added to the module's namespace - __arch_list = { - 'x86' : Arch('x86', 4, 'little', 16, 'ret', 'int 0x80', b'\x90', ['pop {}','ret'], [r'add esp, (\w+)','ret'], [r'mov dword \[(?P<dst>\w+)\], (?P<src>\w+)','ret'], [], ['eax','ebx','ecx','edx','esi','edi','ebp']), - 'x86_64' : Arch('x86', 8, 'little', 16, 'ret', 'syscall', b'\x90', ['pop {}','ret'], [r'add rsp, (\w+)','ret'], [r'mov qword \[(?P<dst>\w+)\], (?P<src>\w+)','ret'], ['rdi','rsi','rdx','rcx','r8','r9'], ['rax','rdi','rsi','rdx','r10','r8','r9']), - 'ARM' : Arch('arm', 4, 'little', 8, 'pop {pc}', 'svc 0', b'\xe1\xa0\x00\x00', ['pop {{{}, pc}}'], [r'add sp, sp, ([^r]\w*)','pop {pc}'], [r'str (?P<src>\w+), \[(?P<dst>\w+)\]','pop {pc}'], ['r0','r1','r2','r3'], ['r7','r0','r1','r2','r3','r4','r5']), - 'THUMB' : Arch('arm', 4, 'little', 8, 'pop {pc}', 'svc 0', b'\x46\xc0', ['pop {{{}, pc}}'], [r'add sp, sp, ([^r]\w*)','pop {pc}'], [r'str (?P<src>\w+), \[(?P<dst>\w+)\]','pop {pc}'], ['r0','r1','r2','r3'], ['r7','r0','r1','r2','r3','r4','r5']), - } - globals().update(__arch_list) - global __arch_lookup - __arch_lookup = {(a.arch_string, a.wordsize, a.endianness) : a for a in reversed(__arch_list.values())} - -@dataclass(frozen=True) -class Arch: - """ - Dataclass of information about a target architecture - - arch_string (str): string returned by r2 iI in the arch field - wordsize (int): the width, in bytes, of the natural unit of data - endianness (str): byte order. either "little" or "big" - alignment (int): the multiple, in bytes, that return addresses must exist - on the stack - ret (str): mnemonic for a "return" instruction - syscall (str): mnemonic for a "syscall" or "service call" instruction - nopcode (bytes): the exact bytes of a "do nothing" instruction - popgad (list[str]): ROP gadget template used to pop a value into a register - cleangad (list[str]): ROP gadget template used to remove values from the - stack - writegad (list[str]): ROP gadget template used to write data to memory - funcargs (list[str]): function argument registers used by the architecture - calling convention - kernargs (list[str]): kernel syscall argument registers - """ - - arch_string: str - wordsize: int - endianness: str - alignment: int - ret: str - syscall: str - nopcode: bytes - popgad: list - cleangad: list - writegad: list - funcargs: list - kernargs: list - - def set(self,new_arch): - """Copy the given Arch into this instance.""" - if type(new_arch) is not Arch: - raise TypeError(f'arch: new_arch must be an Arch: {new_arch}') - self.__dict__.update(new_arch.__dict__) -__define_architectures() - -DEFAULT_ARCH = x86_64 -arch = Arch(**DEFAULT_ARCH.__dict__) - -def lookup_arch(arch_string, wordsize, endianness): - """ - Return an Arch object with the matching search parameters. - - If a predefined Arch matches the specified fields, it will be returned. - Otherwise, None is returned. - - arch_string (str): The "arch" string returned from r2 iI. - wordsize (int): The natural width of an int in bytes. - endianness (str): The order of bytes in an int (either "little" or "big") - """ - return __arch_lookup.get((arch_string, wordsize, endianness)) - -def sint(i): - """Convert given int to signed int of arch.wordsize width.""" - return __int(i, True) - -def uint(i): - """Convert given int to unsigned int of arch.wordsize width.""" - return __int(i, False) - -def int8(i): - """Convert given int to signed 8 bit int.""" - return __int(i, True, 1) - -def int16(i): - """Convert given int to signed 16 bit int.""" - return __int(i, True, 2) - -def int32(i): - """Convert given int to signed 32 bit int.""" - return __int(i, True, 4) - -def int64(i): - """Convert given int to signed 64 bit int.""" - return __int(i, True, 8) - -def uint8(i): - """Convert given int to unsigned 8 bit int.""" - return __int(i, False, 1) - -def uint16(i): - """Convert given int to unsigned 16 bit int.""" - return __int(i, False, 2) - -def uint32(i): - """Convert given int to unsigned 32 bit int.""" - return __int(i, False, 4) - -def uint64(i): - """Convert given int to unsigned 64 bit int.""" - return __int(i, False, 8) - -def btoi(b, byteorder=None): - """Convert given byte array to an int.""" - byteorder = byteorder or arch.endianness - return int.from_bytes(b, byteorder, signed=False) - -def itob(i, width=None, byteorder=None): - """Convert given int to a byte array.""" - width = width or arch.wordsize - byteorder = byteorder or arch.endianness - return __int(i,False,width).to_bytes(width, byteorder, signed=False) - -def __int(i, signed=False, width=None): - # type conversion from int to int of given sign and width - i = int(i) - width = width or arch.wordsize - bits = 8 * width - if signed: - sign_bit = 1 << (bits - 1) - return (i & (sign_bit - 1)) - (i & sign_bit) - else: - mask = (1 << bits) - 1 - return i & mask diff --git a/sploit/comm/__init__.py b/sploit/comm/__init__.py deleted file mode 100644 index ffbc402..0000000 --- a/sploit/comm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .comm import * diff --git a/sploit/comm/comm.py b/sploit/comm/comm.py deleted file mode 100644 index 3bc448e..0000000 --- a/sploit/comm/comm.py +++ /dev/null @@ -1,205 +0,0 @@ -import subprocess -import tempfile -import os -import sys -import select - -from sploit.until import bind -from sploit.util.log import * - -class Comm: - logonread = True - logonwrite = False - flushonwrite = True - readonwrite = False - timeout = 250 # milliseconds - last = None # result of last discrete read - - def __init__(self, backend): - self.back = backend - - def shutdown(self): - try: - self.back.stdout.close() - except BrokenPipeError: - pass - - def read(self, size=-1): - if size < 0: - return self.readall_nonblock() - elif size == 0: - data = b'' - else: - data = self.back.stdin.read(size) - if(data == b''): - raise BrokenPipeError('Tried to read on broken pipe') - if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) - self.last = data - return data - - def readline(self): - data = self.back.stdin.readline() - if(data == b''): - raise BrokenPipeError('Tried to read on broken pipe') - if data.endswith(b'\n'): - data = data[:-1] - if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) - self.last = data - return data - - def readall(self): - data = b'' - try: - for line in self.back.stdin: - tolog = (line[:-1] if line.endswith(b'\n') else line) - if self.logonread : ilog(tolog, file=sys.stdout, color=NORMAL) - data += line - except KeyboardInterrupt: - pass - self.last = data - return data - - def readall_nonblock(self): - try: - data = b'' - os.set_blocking(self.back.stdin.fileno(), False) - poll = select.poll() - poll.register(self.back.stdin, select.POLLIN) - while True: - poll.poll(self.timeout) - d = self.readall() - if len(d) == 0: - self.last = data - return data - data += d - finally: - os.set_blocking(self.back.stdin.fileno(), True) - - def readuntil(self, pred, /, *args, **kwargs): - data = b'' - pred = bind(pred, *args, **kwargs) - l = self.logonread - self.logonread = False - try: - while(True): - data += self.read(1) - if(pred(data)): - break - finally: - self.logonread = l - if self.logonread : ilog(data, file=sys.stdout, color=NORMAL) - self.last = data - return data - - def readlineuntil(self, pred, /, *args, **kwargs): - dataarr = [] - pred = bind(pred, *args, **kwargs) - while(True): - dataarr.append(self.readline()) - if(pred(dataarr)): - break - self.last = dataarr - return dataarr - - def write(self, data): - self.back.stdout.write(data) - if self.flushonwrite : self.back.stdout.flush() - if self.logonwrite : ilog(data, file=sys.stdout, color=ALT) - if self.readonwrite : self.readall_nonblock() - - def writeline(self, data=b''): - self.write(data + b'\n') - - def interact(self): - stdin = sys.stdin.buffer - event = select.POLLIN - - def readall_stdin(): - try: - os.set_blocking(stdin.fileno(), False) - for line in stdin: - self.write(line) - finally: - os.set_blocking(stdin.fileno(), True) - - readtable = { - self.back.stdin.fileno(): self.readall_nonblock, - stdin.fileno(): readall_stdin, - } - - try: - ilog("<--Interact Mode-->") - l = self.logonread - self.logonread = True - - poll = select.poll() - poll.register(self.back.stdin, event) - poll.register(stdin, event) - - readtable[self.back.stdin.fileno()]() - while True: - for fd, e in poll.poll(self.timeout): - if not e & event: return - readtable[fd]() - except KeyboardInterrupt: - pass - finally: - self.logonread = l - ilog("<--Interact Mode Done-->") - -def popen(cmdline=''): - io = Comm((Process(cmdline.split()) if len(cmdline) > 0 else Pipes())) - io.readall_nonblock() - io.readonwrite = True - return io - -class Process: - def __init__(self, args): - ilog(f"Running: {' '.join(args)}") - self.proc = subprocess.Popen(args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - preexec_fn=lambda : os.setpgrp()) - ilog(f"PID: {self.proc.pid}") - self.stdin = self.proc.stdout - self.stdout = self.proc.stdin - - def __del__(self): - if getattr(self, 'proc', None) == None : return - if(self.proc.poll() != None): - return - try: - ilog("Waiting on Target Program to End...") - ilog("Press Ctrl+C to Forcefully Kill It...") - self.proc.wait() - except KeyboardInterrupt: - self.proc.kill() - -class Pipes: - def __init__(self, tmp=None): - if(tmp == None): - self.dir = tempfile.TemporaryDirectory() - dirname = self.dir.name - else: - if(not os.path.exists(tmp)): - os.mkdir(tmp) - dirname = tmp - self.pathin = os.path.join(dirname, "in") - self.pathout = os.path.join(dirname, "out") - os.mkfifo(self.pathin) - os.mkfifo(self.pathout) - ilog("Waiting on Target to Connect...", file=sys.stdout) - ilog(f"<{self.pathin} >{self.pathout}", file=sys.stdout) - self.stdout = open(self.pathin, "wb") - self.stdin = open(self.pathout, "rb") - ilog("Connected!") - - def __del__(self): - try: - if getattr(self,'stdout',None) : self.stdout.close() - if getattr(self,'stdin',None) : self.stdin.close() - except BrokenPipeError: - pass - if getattr(self,'pathin',None) and os.path.exists(self.pathin) : os.unlink(self.pathin) - if getattr(self,'pathout',None) and os.path.exists(self.pathout) : os.unlink(self.pathout) diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py deleted file mode 100644 index da47cc1..0000000 --- a/sploit/payload/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .fmtstring import * -from .gadhint import * -from .payload import * -from .payload_entry import * -from .ret2dlresolve import * -from .rop import * diff --git a/sploit/payload/fmtstring.py b/sploit/payload/fmtstring.py deleted file mode 100644 index 54da6f2..0000000 --- a/sploit/payload/fmtstring.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Exploit C-style format string vulnerabilities - -These techniques leverage functions such as printf, fprintf, sprintf, etc. when -run on unchecked user input to perform arbitrary memory read or write. This is -made possible by the unintended use of user input as the function's format -string argument, instead of an ordinary data argument. Attackers may inject -their own conversion specifiers, which act as operating instructions to the -function. Interesting formatters include: - - %p Read argument value. These are the values of function argument - registers and values from the stack. The value is printed as - hexadecimal (with leading "0x") and interprets values as unsigned - long (aka, same size as arch.wordsize). - - %s Read memory as asciiz string. Prints the data pointed to by - argument value. - - %c Read argument as 8-bit character, printing the interpreted character - value. This formatter is useful in combination with a field width - specifier in order to print a controlled number of bytes to the - output, which is meaningful to the next formatter. - - %n Write memory as integer. Prints no output, but writes the number of - characters printed so far to the location pointed to by the argument - pointer. A length modifier will control the bit-width of the - integer written. - -See `man 3 printf` for more details. -""" - -from sploit.arch import arch, btoi, itob -from sploit.payload.payload import Payload -from sploit.payload.payload_entry import padalign, padrel - -_FMTSTR_MAGIC = b"\xcd" - -def _make_fmtstr_payload(): - # A typical layout will look like this: - # b'%123c%10$hn%456c%11$hn\x00\x90\xde\xad\xbe\xef\xca\xfe\xba\xbe' - # ^ ^ ^ ^ ^ ^ - # fmt[0] fmt[1] nul | addrs[0] addrs[1] - # align - # - # Many examples found online will demo placing addresses at the front. Eg: - # b'\xde\xad\xbe\xef\xca\xfe\xba\xbe%123c%7$hn%456c%8$hn\x00' - # This has the benefit that %n positional values are simple to calculate - # (they just start at the payload position and increase by one). However, - # any NULL bytes in the addresses break the exploit, since printf will stop - # processing its string once a NULL is encountered. - # - # Moving addresses to the end mitigates this. Wordsize alignment is then - # necessary to give for valid argument positions. We also intentionally - # NULL terminate the format string portion of the payload to prevent printf - # from processing beyond formatters. - fp = Payload() - fp.fmt = Payload() - fp.null = b"\x00" - fp.align = padalign(arch.wordsize) - fp.addrs = Payload() - return fp - -def _fixup_positionals(fp, position, offset=None): - if offset is None: - offset = fp.addrs.base // arch.wordsize - - fixup = _make_fmtstr_payload() - fixup.addrs = fp.addrs - - for i, fmt in enumerate(fp.fmt): - pos = position + offset + i - fixup.fmt(fmt.decode().format(pos).encode()) - - # String formatting positional values may grow the format string so much - # as to cause the addrs offset to shift. Detect this and correct. - check = fixup.addrs.base // arch.wordsize - if offset != check: - return _fixup_positionals(fp, position, check) - - return fixup - -def fmtstr_dump(start=None, end=None): - """ - Return a format string payload which dumps annotated argument values. - - start (int): Starting argument position (default: 1) - end (int): Ending argument position (default: start + 20) - """ - if start is None: start = 1 - if end is None: end = start + 19 # inclusive, so 20 total arguments - - fp = Payload() - fp.magic = padrel(arch.wordsize, _FMTSTR_MAGIC) - fp.fmt = Payload() - fp.null = b"\x00" - - for pos in range(start, end+1): - if pos < len(arch.funcargs): - label = arch.funcargs[pos] - else: - offset = (pos - len(arch.funcargs)) * arch.wordsize - label = f"stack+{hex(offset)}" - - fp.fmt(f"({pos}$ {label}) %{pos}$p ".encode()) - - return fp - -def fmtstr_get(*positions, join=" "): - """ - Return a format string payload which prints specific argument values. - - positions (*int): Argument positions - join (str): Delimiter string - """ - fp = Payload() - fp.fmt = Payload() - fp.null = join - - for p in positions: - fp.fmt(f"{join}%{p}$p".encode()) - - return fp - -def fmtstr_read(position, address): - """ - Return a format string payload which reads data (as string, via %s). - - position (int): printf positional offset of payload on stack. - address (int): Address of data to read. - """ - fp = _make_fmtstr_payload() - fp.fmt(b"%{}$s") - fp.addrs(address) - return _fixup_positionals(fp, position) - -def fmtstr_write(position, _data, _value=None): - """ - Return a format string payload which writes data. - - One option for calling this function is to give a write destination in _data - as an integer, and the value to write in _value. - - Alternatively, _data may contain a dictionary, with write destinations as - keys and contents to write as values. _value is ignored in this case. - - In either case, the contents to write is generally expected to be bytes. - However, integers are converted automatically via itob(). - - position (int): printf positional offset of payload on stack. - _data (int|dict{int:bytes}): Write data (see above) - _value (int|bytes): Write value (see above) - """ - # Convert from 2-argument style to dictionary. - if type(_data) is int: - _data = { _data: _value } - - pairs = {} - - # Collect each 2-byte word to write. - for addr, value in _data.items(): - value = itob(value) if type(value) is int else bytes(value) - words = [ value[i:i+2] for i in range(0, len(value), 2) ] - words = { addr+(i*2): btoi(w) for i, w in enumerate(words) } - pairs.update(words) - - fp = _make_fmtstr_payload() - prev = 0 - - # Craft writes. - for addr, word in sorted(pairs.items(), key=lambda x: x[1]): - diff = word - prev - prev = word - - size = "" if diff == 0 else f"%{diff}c" - fp.fmt(f"{size}%{{}}$hn".encode()) - fp.addrs(addr) - - return _fixup_positionals(fp, position) diff --git a/sploit/payload/gadhint.py b/sploit/payload/gadhint.py deleted file mode 100644 index 1bef9f0..0000000 --- a/sploit/payload/gadhint.py +++ /dev/null @@ -1,98 +0,0 @@ -import copy -from dataclasses import dataclass, field - -from sploit.rev.gadget import Gadget -from sploit.types.index_entry import IndexEntry - -@dataclass -class GadHint(IndexEntry): - """ - User-annotated gadget description object - - 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 - occurrence. - - movs (dict{str:str}): The register-to-register moves made by this gadget. - Keys are destination register names, values are source register names. The - order given is insignificant. - - imms (dict{str:int}): The immediate-to-register loads made by this gadget. - Keys are destination register names, values are immediate 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 - required register values. - - stack (list[int]): A list of words to append to the stack following this - gadget. The first element given is nearest to the top of the stack and the - rest follow in order. - - align (bool): If True, this gadget expects the stack to be aligned prior - to entry. - - syscall (bool): If True, this gadget contains a syscall instruction. - - spm (int): "Stack pointer move" - The amount the stack pointer is adjusted - by this gadget. The effect of executing a terminating "return" instruction - should not be accounted for. A value of zero is taken as "unspecified". - """ - - base: int = 0 - pops: list = field(default_factory=list) - movs: dict = field(default_factory=dict) - imms: dict = field(default_factory=dict) - writes: dict = field(default_factory=dict) - requirements: dict = field(default_factory=dict) - stack: list = field(default_factory=list) - align: bool = False - syscall: bool = False - spm: int = 0 - - @property - def offset(self): - """Return gadget offset as an integer.""" - return int(self.base) - - def with_requirements(self, reqs): - """Return new object with additional requirements.""" - for k, v in reqs.items(): - if self.requirements.get(k, v) != v: - raise ValueError( - f"GadHint: Conflicting gadget requirements: " - f"{self.requirements}, {reqs}") - - new = copy.deepcopy(self) - new.requirements |= reqs - return new - - def __repr__(self): - """Return human-readable GadHint.""" - def fmt(name, prop): - if len(prop) > 0: - return f", {name}={prop}" - return "" - - 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) - s += fmt("writes", self.writes) - s += fmt("requirements", self.requirements) - s += fmt("stack", self.stack) - if self.align: - s += ", align" - if self.syscall: - s += ", syscall" - if self.spm > 0: - s += f", spm={self.spm}" - return f"GadHint({s})" diff --git a/sploit/payload/payload.py b/sploit/payload/payload.py deleted file mode 100644 index 2a9521f..0000000 --- a/sploit/payload/payload.py +++ /dev/null @@ -1,224 +0,0 @@ -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)}" - memo = {} - - if len(self.__entries__) > 0: - s += FMT.format("ADDRESS", "SYMBOL", "DATA") - - for i, value in enumerate(self.__entries__): - key = self.__entries__.idx2key(i) - key = "(unkeyed)" if key is None else str(key) - key = f"[{key}]" if isinstance(value, IndexEntry) else key - - addr = self.__addrof(i, memo) - data = str(self.__bytesof(i, memo)) - if len(data) > _REPR_DATA_LEN: - data = data[:_REPR_DATA_LEN] + " ..." - - s += FMT.format(hex(addr), key, data) - - return s - - def __bytes__(self): - """Return calculated payload bytes.""" - memo = {} - x = [ self.__bytesof(i, memo) for i in range(len(self.__entries__)) ] - return b"".join(x) - - def __call__(self, *args): - """ - Payload quick-action call operator. - - If called with arguments, append these values to the payload in the - order given. The payload (self) is returned for easily chaining calls. - - If called without arguments, return the rendered payload content as if - `bytes(payload)` was called. - """ - if len(args) == 0: - return bytes(self) - - for value in args: - value = self.__prep_insertion(value, self.end()) - self.__entries__.append(value) - - return self - - def end(self): - """Return the offset or address of the end of the payload.""" - return self.base + len(self) - - # IndexTbl abstract methods - - def __copy__(self): - """Return copy of object with shared data entries.""" - return Payload(self.base, self.__entries__) - - def __iter__(self): - """Iterate over data entries.""" - return iter(self.__entries__) - - def __len__(self): - """Return the size of the payload content in bytes.""" - memo = {} - x = [ self.__lenof(i, memo) for i in range(len(self.__entries__)) ] - return sum(x) - - def __getindex__(self, index): - """Return payload index value or address.""" - value, _ = self.__valueof(index, {}) - return value - - def __setindex__(self, index, value): - """Set payload index value.""" - try: - addr = self.__addrof(index, {}) - except KeyError: - addr = self.end() - value = self.__prep_insertion(value, addr) - self.__entries__[index] = value - - def __delindex__(self, index): - """Delete payload index.""" - del self.__entries__[index] - - # Payload helpers - - def __valueof(self, index, memo): - """Return a tuple (addr of value, literal value) for index.""" - value = self.__entries__[index] - addr = self.__addrof(index, memo) - if isinstance(value, IndexEntry): - value @= addr - return value, value - return addr, value - - def __addrof(self, index, memo): - """Return address (base + offset) for index.""" - index = self.__entries__.key2idx(index) - try: - return memo[index] - except KeyError: - sizes = [ self.__lenof(i, memo) for i in range(index) ] - addr = self.base + sum(sizes) - memo[index] = addr - return addr - - def __lenof(self, index, memo): - """Return element length for index.""" - _, value = self.__valueof(index, memo) - if isinstance(value, PayloadEntry): - return value.payload_len(self) - return len(value) - - def __bytesof(self, index, memo): - """Return byte output for index.""" - _, value = self.__valueof(index, memo) - if isinstance(value, PayloadEntry): - return value.payload_bytes(self) - return bytes(value) - - def __prep_insertion(self, value, addr): - """Initialize or type coerce input value for payload insert.""" - if isinstance(value, PayloadEntry): - value @= addr - value.payload_insert(self) - return value - - if type(value) is str: - value = value.encode() + b"\x00" - elif type(value) is int: - value = itob(value) - - try: - # Confirm value supports our required operations - len(value) - bytes(value) - except TypeError as ex: - raise TypeError(f"Payload: Bad type {type(value)} given") from ex - - return value diff --git a/sploit/payload/payload_entry.py b/sploit/payload/payload_entry.py deleted file mode 100644 index 2f8dbdd..0000000 --- a/sploit/payload/payload_entry.py +++ /dev/null @@ -1,113 +0,0 @@ -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_len(self, payload): - """ - Called to compute size of this entry. - - Implement this method to calculate the length of this dynamic payload - entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - - def payload_bytes(self, payload): - """ - Called to generate bytes for this entry. - - Implement this method to generate the binary output for this dynamic - payload entry. self.base is set to the current entry address or offset. - """ - raise NotImplementedError - -# Concrete payload entry definitions - -class pointer(PayloadEntry): - """Generate an integer which tracks the address of another payload field.""" - - def __init__(self, target=None, math=None): - self.target = target - self.math = math - - def payload_len(self, payload): - return arch.wordsize - - def payload_bytes(self, payload): - if self.target is None: - addr = self.base - else: - addr = payload[self.target] - if callable(self.math): - addr = self.math(addr) - return itob(addr) - -class padlen(PayloadEntry): - """Generate padding to reach a target payload length.""" - - def __init__(self, size, data=None): - self.size = size - self.data = data - - def payload_len(self, payload): - return self.size - (self.base - payload.base) - - def payload_bytes(self, payload): - size = self.payload_len(payload) - data = self.data or arch.nopcode - if size < 0: - raise ValueError("padding: Available space is negative") - if (size := size / len(data)) != int(size): - raise ValueError("padding: Element does not divide the space evenly") - return data * int(size) - -class padabs(padlen): - """Generate padding to reach a target absolute address.""" - - def payload_len(self, payload): - return self.size - self.base - -class padrel(padlen): - """Generate a fixed length of padding (aka: length relative to self).""" - - def payload_len(self, payload): - return self.size - -class padalign(padlen): - """Generate padding to reach next aligned address.""" - - def __init__(self, size=None, data=None, reference=0): - self.size = size - self.data = data - self.reference = reference - - def payload_len(self, payload): - size = self.size or arch.alignment - return (self.reference - self.base) % size - -class placeholder(padlen): - """Generate fixed length of magic bytes, one word length by default.""" - - def __init__(self, size=None): - self.size = size - self.data = _PLACEHOLDER_MAGIC - - def payload_len(self, payload): - return self.size or arch.wordsize diff --git a/sploit/payload/ret2dlresolve.py b/sploit/payload/ret2dlresolve.py deleted file mode 100644 index 8862e22..0000000 --- a/sploit/payload/ret2dlresolve.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Perform "Return to dlresolve" dynamic linker attack - -The ret2dlresolve technique is useful to defeat library ASLR against targets -with partial relro (or less) and where no useable data leaks are available. -This is specifically a workaround for ASLR of libraries such as libc, and -addresses within the target executable are expected to be known (non-pic or -otherwise). - -When a dynamic library call is performed normally, applications jump to code -stubs in the .plt section to perform the actual relocation. This process relies -on a couple of meta-data structures in the ELF object: - -Elf*_Rel: Contains a pointer to the corresponding GOT entry, which is used to -cache the real subroutine address for later calls, as well as an info field -describing the relocation. This info field contains a type subfield and an -index into the ELF's symbol table for the symbol to be relocated. - -Elf*_Sym: Contains all the data relevant to the symbol. For the purposes of the -exploit, only the symbol name field is utilized (the others are set to zeroes). -The name field is an offset into the ELF's string table, and the actual symbol -name string can be found at this offset. - -All of the data tables mentioned above are located by their corresponding -section in the ELF. The relocation process however does not perform any bounds -checks to ensure the runtime data structures actually come from these sections. -By forging custom structures, and ensuring they can be written into memory at -precise locations, an attacker can trick the resolver to link any library -function they desire by setting up the equivalent PLT function call via ROP. - -Read on for more background details: -http://phrack.org/issues/58/4.html -https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62 - -Structure definitions from your standard elf.h header: - -typedef struct { - Elf32_Word st_name; /* 4b Symbol name (string tbl index) */ - Elf32_Addr st_value; /* 4b Symbol value */ - Elf32_Word st_size; /* 4b Symbol size */ - unsigned char st_info; /* 1b Symbol type and binding */ - unsigned char st_other; /* 1b Symbol visibility */ - Elf32_Section st_shndx; /* 2b Section index */ -} Elf32_Sym; - -typedef struct { - Elf64_Word st_name; /* 4b Symbol name (string tbl index) */ - unsigned char st_info; /* 1b Symbol type and binding */ - unsigned char st_other; /* 1b Symbol visibility */ - Elf64_Section st_shndx; /* 2b Section index */ - Elf64_Addr st_value; /* 8b Symbol value */ - Elf64_Xword st_size; /* 8b Symbol size */ -} Elf64_Sym; - -typedef struct { - Elf32_Addr r_offset; /* 4b Address */ - Elf32_Word r_info; /* 4b Relocation type and symbol index */ -} Elf32_Rel; - -typedef struct { - Elf64_Addr r_offset; /* 8b Address */ - Elf64_Xword r_info; /* 8b Relocation type and symbol index */ -} Elf64_Rel; - -Elf32_Rel.r_info = 0xAAAAAABB - | | - | type - symidx - -Elf64_Rel.r_info = 0xAAAAAAAABBBBBBBB - | | - symidx type -""" - -from sploit.arch import arch, itob -from sploit.payload.gadhint import GadHint -from sploit.payload.payload import Payload -from sploit.payload.payload_entry import padalign, padlen, pointer -from sploit.payload.rop import ROP -from sploit.rev.r2 import run_cmd - -_JMP_SLOT = 0x07 - -def _symsize(): - # Size of Elf*_Sym, used for padding and indexing - if arch.wordsize == 4: return 16 - elif arch.wordsize == 8: return 24 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -def _relsize(): - # Size of Elf*_Rel, used only for indexing on 64bit (32bit uses offset) - if arch.wordsize == 4: return 1 - elif arch.wordsize == 8: return 24 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -def _infoshift(): - # Partition subfields of Elf*_Rel.r_info - if arch.wordsize == 4: return 8 - elif arch.wordsize == 8: return 32 - raise ValueError("Ret2dlresolve: Architecture wordsize unsupported") - -class Ret2dlresolve(ROP): - # Use constructor from ROP class - - def reloc(self, symbol_name): - """ - Generate relocation structures for the function with given symbol name. - - The returned data structures are packed into a single Payload object. - This payload must be written into the target's memory before attempting - to use it with Ret2dlresolve.call(). Furthermore, the chosen write - location must be assigned to the payload base property, so that internal - pointers may take on the appropriate values. - - See Ret2dlresolve.determine_address() for advice on choosing a write - location. - - symbol_name (str): Name of library function to link - """ - binary = self.objects[0] - symtab = binary.sym.sect['.dynsym'] - strtab = binary.sym.sect['.dynstr'] - - try: - jmprel = binary.sym.sect['.rel.plt'] - except KeyError: - jmprel = binary.sym.sect['.rela.plt'] - - # Elf*_Rel.r_info - info = lambda x: ((int(x - symtab) // _symsize()) << _infoshift()) | _JMP_SLOT - - # The sym structure is the most picky about its location in memory. So - # it is listed first in the main dlres struct, which can be placed at - # the desired location. - sym = Payload() - sym.name = pointer("symbol_string", lambda x: x - strtab) - sym.pad = padlen(_symsize(), b"\x00") - sym.symbol_string = symbol_name - - dlres = Payload() - dlres.symalign = padalign(_symsize(), reference=symtab) - dlres.sym = sym - dlres.relalign = padalign(_relsize(), reference=jmprel) - dlres.offset = pointer() - dlres.info = pointer("sym", info) - return dlres - - def determine_address(self, start=None, end=None, n=0): - """ - Determine recommended address for relocation structures. - - There are a couple considerations to make when determining the memory - locations. First of all, the location must be writable. More - importantly, since most items are referred to by an array index, the - structures themselves must be properly aligned, reference to the array - origins. - - The payload returned from Ret2dlresolve.reloc() has some of these - alignments built in, but one crucial one is not. The index implied by - Elf*_Sym's offset from the symtab base is the same index used to lookup - symbol version information, reference to versym. The data at this index - must constitute a valid version half-word (16-bits). This function - attempts to ensure that the coincident version info for any returned - value is the data \x00\x00. Getting this wrong can cause the dlresolve - routine to crash. - - start (int): Minimum address to recommend (default: .bss section address) - end (int): Maximum address to recommend (default: end of memory page) - n (int): Return the Nth useable address within the defined range - """ - binary = self.objects[0] - symtab = binary.sym.sect['.dynsym'] - versym = binary.sym.sect['.gnu.version'] - bss = binary.sym.sect['.bss'] - - if start is None: start = bss - if end is None: end = (start & ~0xfff) + 0x1000 - - zero_words = run_cmd(binary.path, "/x 0000") - zero_words = [ int(x.split(" ")[0], 0) for x in zero_words ] - - # Size of version entry is always 2 bytes. - veroff = [ x - versym for x in zero_words ] - idx = [ x//2 for x in veroff if x%2 == 0 ] - symoff = [ x * _symsize() for x in idx ] - addr = [ x + symtab for x in symoff ] - addr = [ x for x in addr if start <= x < end ] - - if len(addr) > n: - return addr[n] - - raise AssertionError("Ret2dlresolve: No suitable memory location") - - # Overrides ROP.call() - def call(self, reloc, *params): - """ - Return a ROP payload to call function via dynamic linker. - - reloc's base address must be set appropriately. - - reloc (Payload): Relocation payload obtained from Ret2dlresolve.reloc() - *params (int): Remaining positional args are passed to function. - """ - binary = self.objects[0] - plt = binary.sym.sect['.plt'] - - try: - jmprel = binary.sym.sect['.rel.plt'] - except KeyError: - jmprel = binary.sym.sect['.rela.plt'] - - register_params = dict(zip(arch.funcargs, params)) - stack_params = params[len(register_params):] - index = int(reloc.offset - jmprel) // _relsize() - - reqs = GadHint(requirements=register_params) - call = GadHint(index, stack=stack_params) - ret = GadHint(self.search_gadget(arch.ret)) - - chain = Payload() - try: chain.requirements = self.gadget(reqs).requirements - except KeyError: pass - chain.alignment = padalign(0, itob(ret)) - chain.plt = plt - chain.call = self.gadget(call) - return chain diff --git a/sploit/payload/rop.py b/sploit/payload/rop.py deleted file mode 100644 index 30467de..0000000 --- a/sploit/payload/rop.py +++ /dev/null @@ -1,364 +0,0 @@ -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 - -_POP_MAGIC = 0xdead -_SPM_MAGIC = b"\x69" -_ERROR_MAGIC = 0xbaadc0de - -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. - - 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. - """ - - def __init__(self, *objects, safe_syscalls=True, align_calls=True, - 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 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 search_gadget(self, *regexes): - """Return the first matching gadget, considering all objects.""" - for obj in self.objects: - try: - return obj.gadget(*regexes) - except: - pass - raise LookupError( - f"ROP: Need to define gadget symbol for {'; '.join(regexes)}") - - def gadget(self, gadget): - """ - 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. - """ - return self.gadget(GadHint(requirements=sets)) - - def call(self, func, *params): - """ - Return a ROP payload to call function. - - func (int): Entry address of function to call. - *params (int): Remaining positional args are passed to func. - """ - 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.gadget(gadget) - - def syscall(self, *params): - """ - Return a ROP payload to call kernel. - - *params (int): The first argument is the syscall number. Remaining - positional arguments are passed to the syscall. - """ - if len(params) > len(arch.kernargs): - raise TypeError("ROP: Too many arguments passed to syscall. " - f"Target architecture supports up to {len(arch.kernargs)-1}.") - - register_params = dict(zip(arch.kernargs, params)) - sc = self.__get_gadget("syscall", {}) - return self.gadget(sc.with_requirements(register_params)) - - def memcpy(self, dst, src): - """ - Return a ROP payload to write data into memory. - - dst (int): The destination memory address. - src (bytes): The content to write. - """ - data = Payload() - for idx in range(0, len(src), arch.wordsize): - word = btoi(src[idx:idx+arch.wordsize]) - data(self.gadget(self.__get_write(dst+idx, word))) - return data - - 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): - """ - Populate gadget dependency graph. - - This function recursively looks up gadgets to ensure all necessary - required gadgets can be found, and stores this information into the - given graph object. Established dependencies encode the order that the - chain builder should attempt to satisfy register requirements. - Dependency loops are detected by the TopologicalSorter. - - seen (set): Set of (register,value) tuples we have already discovered. - graph (TopologicalSorter): Dependency graph model object. - current (GadHint): Current gadget we are processing. - """ - for r, v in current.requirements.items(): - # We key on register name _and_ value because some gadgets may - # only be capable of storing specific values in a target register. - # 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) - - # 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 - # clobbering targets it requires, as the builder will satisfy - # them afterward. - for x in gadget.requirements: - graph.add(x, r) - - # Treat gadget's load immediates as pseudo-requirements for - # the sake of target ordering, following the same logic - # as above. - for x in gadget.imms: - graph.add(x, r) - - # Mark node as visited - seen.add((r, v)) - self.__discover_requirements(seen, graph, gadget) - - def __get_gadget(self, target, sets): - """ - Get context-specific gadget. - - target (str): Either "ret", "syscall", or the name of a register we - would like to modify. - - sets (dict{str:int}): The set of other register requirements we are - trying to fulfill in parallel. Values may affect the gadget we decide - to use. - """ - # First, consider user-provided hints before automatically locating - # gadgets. - 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. - addl_reqs = { src:sets[dst] for dst, src in hint.movs.items() if dst in sets } - hint = hint.with_requirements(addl_reqs) - - # Pops will be accounted for by the chain builder. - # Immediates will be handled by gadget ordering in chain builder. - # Writes are a non-issue here. - - if hint.syscall: - # Only consider syscalls if the target is syscall. - if target == "syscall": - return hint - elif target in hint.imms: - if hint.imms[target] == sets[target]: - return hint - elif target in hint.pops: - return hint - elif target in hint.movs: - return hint - - # Automatically locate simple gadgets - if target == "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.search_gadget(*insns), syscall=True) - - # target == register - insns = [ i.format(target) for i in arch.popgad ] - return GadHint(self.search_gadget(*insns), pops=[target]) - - def __get_clean(self, size): - """ - Get a stack cleaning gadget that moves sp by _at least_ size. - - size (int): Minimum stack pointer move. - """ - # spm values of zero (the default) can't be trusted, as in this case - # 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(): - if hint.spm >= size and hint.spm > 0: - return hint - - 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 ]) - - if len(sizes) > 0: - return GadHint(table[sizes[0]], spm=sizes[0]) - - raise LookupError( - f"ROP: Need to define a stack move gadget of at least {size}") - - def __get_write(self, dst, src): - """ - Get a memory write gadget, injected with requirements for user data. - - dst (int): The intended memory write location. - src (int): The intended value to write. - """ - # 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(): - if hint.writes: - d, s = list(hint.writes.items())[0] - return hint.with_requirements({d:dst, s:src}) - - # 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.search_gadgets(*arch.writegad) - - for gad in results: - d = gad.asm[0].group("dst") - s = gad.asm[0].group("src") - - try: - # Assert requirements are met. - gadget = GadHint(gad, writes={d: s}, requirements={d:dst, s:src}) - self.__discover_requirements(set(), TopologicalSorter(), gadget) - return gadget - except: - pass - - raise LookupError("ROP: Need to define gadgets for memory write / deps") - - def __build_chain(self, gadget, sets): - """ - Generate ROP chain data for a given gadget. - - This function recursively builds a ROP chain for the given gadget and - its requirements, returning the result as a Payload. - - 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, 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) - 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: - 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 - # immediates that match our requirements. Non-matching - # immediates will be handled by a later gadget. - imms = g.imms.keys() & reqs.keys() - imms = [ x for x in imms if g.imms[x] == reqs[x] ] - done = g.pops + list(g.movs) + imms - to_do_list = [ x for x in to_do_list if x not in done ] - - # Append chain data to execute this gadget, but respect offset == 0 - # as a way to disable this gadget (perform a NULL gadget). - if gadget.offset != 0: - # Stack alignment if required. - if gadget.align: - ret = self.__get_gadget("ret", {}) - chain.alignment = padalign(0, itob(ret)) - - # "Return address" entry into this gadget. - 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. - 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 gadget.stack: - size = len(gadget.stack) * arch.wordsize - - if self.clean_stack: - clean = self.__get_clean(size) - chain.cleanup = clean.offset - pad = padlen(clean.spm, _SPM_MAGIC) - else: - chain.cleanup = _ERROR_MAGIC - pad = None - - chain.outer = Payload() - chain.outer(*gadget.stack) - if pad: chain.outer.pad = pad - - return chain diff --git a/sploit/rev/__init__.py b/sploit/rev/__init__.py deleted file mode 100644 index 42e2f5b..0000000 --- a/sploit/rev/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .elf import * -from .gadget import * -from .ldd import * -from .r2 import * diff --git a/sploit/rev/elf.py b/sploit/rev/elf.py deleted file mode 100644 index b1479d6..0000000 --- a/sploit/rev/elf.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -Definition of the ELF class -""" - -from sploit.rev import ldd, r2 -from sploit.arch import lookup_arch -from itertools import zip_longest - -class ELF: - """ - Representation of an ELF binary file. - - This class is effectively a front-end for the r2 module. Through ELF, you - can get information about an ELF binary in a convenient, object-oriented - interface and automate some of your static analysis reverse engineering. - - Because much of the functionality of r2 is cached, much of the functionality - of ELF is also implicitly cached. Longer operations like retrieving the - symbol table or looking up gadgets will be faster on subsequent attempts. - This is mostly useful when sploit is run from the REPL or in Pipes mode - where this cache is preserved across script runs. - - Some of the behavior of this class is done upfront while other operations - are performed lazily. Retrieving symbols, binary info, security info, and - the list of library dependencies are all done upfront when the object is - constructed. - - path (str): Absolute file path to the underlying ELF file. - - sym (Symtbl): A collection of named address offsets exposed through the ELF. - - libs (dict{str:ELF}): A dictionary of ELFs representing linked library - dependencies of the current ELF. They are indexed by their base filename - (i.e. elf.libs["libc.so.6"]). The actual ELF is lazily constructed when it - is requested. Pretty printing of the libs dict is implemented. - - locals (->Symtbl): A psuedo-namespace to access Symtbls for the local - variables of functions. i.e. If a function existed in the elf called foo(), - you could get a Symtbl of its local variables with elf.locals.foo - - info (->str|int): A psuedo-namespace to access various info about the ELF - file. Printing elf.info will pretty-print this info in a tabulated form. - - info.type (str): The type of file. - - info.os (str): The os the binary was compiled for. - - info.baddr (int): The virtual base address of the binary. - - info.arch_string (str): A string given by r2 iI that helps identify the - architecture the binary was compiled for. - - info.wordsize (int): The natual width of an int on the architecture the - binary was compiled for. - - info.endianness (str): The byte order of an int on the architecture the - binary was compiled for. - - security (->bool|str): A psuedo-namespace to access security info about the - binary. Printing elf.security will pretty-print this info in a tabulated - form. - - security.stripped (bool): True if the binary was stripped of debugging - information, symbols, and strings. - - security.pic (bool): True if the binary's code is position independent. - - security.relro (str): The level of "Relocation Read-Only" that the binary - was compiled with. Pertains to if the Global Offset Table is read-only. - This is often "partial" or "full". - - security.relocs (bool): True if the binary uses dynamic runtime relocation. - - security.canary (bool): True if the binary uses stack canaries. - - security.nx (bool): True if the binary does not have stack execution - privileges. - - security.rpath (str): Runtime library lookup path. If there isn't one, this - will say "NONE". - - arch (Arch): On Construction, an ELF will automatically try to figure out if - it was compiled for one of sploit's predefined Arch's. If so, it will set it - here. Otherwise, this is None. - """ - - def __init__(self, path): - """ - Construct an ELF. - - path (str): The filepath to the ELF binary file. - """ - self.path = path - self.sym = r2.get_elf_symbols(self.path) - try: - libs = ldd.get_libraries(self.path) - except: - libs = {} - self.libs = self.__LIBS__(libs) - self.locals = self.__LOCALS__(self) - bininfo = r2.get_bin_info(self.path) - self.info = self.__BININFO__(bininfo) - self.security = self.__SECINFO__(bininfo) - self.arch = lookup_arch(self.info.arch_string, self.info.wordsize, self.info.endianness) - - def __repr__(self): - """Pretty-print a summary of the ELF.""" - s = 'ELF: ' - s += self.path - s += f'\n{len(self.sym)} symbols @ {hex(self.sym)}' - column_fmt = '\n{0:36}{1:36}' - border = '------------' - s += column_fmt.format(border,border) - s += column_fmt.format('Binary Info','Security Info') - s += column_fmt.format(border,border) - for line in zip_longest(str(self.info).split('\n'),str(self.security).split('\n'),fillvalue=''): - s += column_fmt.format(line[0],line[1]) - s += f'\n{border}' - s += '\nLibraries' - s += f'\n{border}' - s += '\n' - s += str(self.libs) - return s - - class __LIBS__(dict): - # Fancy magic dict of {filename:ELF} which will lazy load the ELF - def __init__(self, libs): - super().__init__({lib.name:lib.path for lib in libs.values() if lib.path}) - def __getitem__(self, lib): - get = super().__getitem__ - if(type(get(lib))==str):self[lib] = ELF(get(lib)) - return get(lib) - def __repr__(self): - s = '' - for name,lib in self.items(): - s += '\n' + str(name) + ' => ' + (lib if(type(lib)==str) else str(lib.path)) - return s.strip() - - class __LOCALS__: - # Fancy magic class that provides a psuedo-namespace to lookup locals for functions - def __init__(self, elf): - self.elf = elf - def __getattr__(self, sym): - return r2.get_locals(self.elf.path, getattr(self.elf.sym, sym)) - - class __BININFO__: - # Fancy magic class that provides a psuedo-namespace to get properties of the binary - def __init__(self, bininfo): - self.info = { - "type" : bininfo['bintype'], - "os" : bininfo['os'], - "baddr" : bininfo['baddr'], - "arch_string" : bininfo['arch'], - "wordsize" : bininfo['bits']//8, - "endianness" : bininfo['endian'], - } - def __getattr__(self, k): - return self.info[k] - def __repr__(self): - s = '' - for name,val in self.info.items(): - if name == 'baddr': val = hex(val) - s += '\n{0:14}{1}'.format(name,val) - return s.strip() - - class __SECINFO__(__BININFO__): - # Fancy magic class that provides a psuedo-namespace to get security properties of the binary - def __init__(self, bininfo): - self.info = { - "stripped" : bininfo['stripped'], - "pic" : bininfo['pic'], - "relro" : bininfo.get('relro',''), - "relocs" : bininfo['relocs'], - "canary" : bininfo['canary'], - "nx" : bininfo['nx'], - "rpath" : bininfo['rpath'], - } - - def retaddr(self, caller, callee): - """ - Returns a list of addresses where a function returns into another - function at. - - caller (int): Address of the calling function to be returned into. - - callee (int): Address of the function that was called and will return. - """ - return [c.ret_addr for c in r2.get_call_returns(self.path, caller, callee)] - - def gadgets(self, *regexes, cont=False): - """ - Returns a list of gadgets that match the given regex list. - - *regexes (str): All positional arguments are treated as regex strings - for the gadget search. - - cont (bool): If true, this function will return all of the assembly past - the found gadget up to the next return point. - """ - return [ self.sym[g] for g in r2.rop_gadgets(self.path, *regexes, cont=cont) ] - - def gadget(self, *regexes): - """Returns the first gadget found that matches the given regex list.""" - return self.sym[r2.rop_gadget(self.path, *regexes)] diff --git a/sploit/rev/gadget.py b/sploit/rev/gadget.py deleted file mode 100644 index cc69723..0000000 --- a/sploit/rev/gadget.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass, field -from sploit.types.index_entry import IndexEntry - -@dataclass -class Gadget(IndexEntry): - """ - Basic gadget description object - - 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. - """ - - base: int = 0 - asm: list = field(default_factory=list) - - def __repr__(self): - """Return human-readable Gadget.""" - s = hex(self.base) - if len(self.asm) > 0: - asm = "; ".join([ m.string for m in self.asm ]) - s += f", '{asm}'" - return f"Gadget({s})" diff --git a/sploit/rev/ldd.py b/sploit/rev/ldd.py deleted file mode 100644 index b773abf..0000000 --- a/sploit/rev/ldd.py +++ /dev/null @@ -1,13 +0,0 @@ -from sploit.util.cmd import run_cmd_cached -from sploit.util.log import ilog - -import re -from collections import namedtuple as nt - -def get_libraries(elf): - ilog(f'Retrieving linked libraries of {elf} with ldd...') - out = run_cmd_cached(['ldd',elf]) - out = [re.split(r'\s+',lib)[1:] for lib in out] - Lib = nt("Lib", "name path addr") - out = {l[0]:Lib(l[0],l[0] if l[0][0]=='/' else l[2] if l[1]=='=>' else None,l[-1]) for l in out} - return out diff --git a/sploit/rev/r2.py b/sploit/rev/r2.py deleted file mode 100644 index e81adc9..0000000 --- a/sploit/rev/r2.py +++ /dev/null @@ -1,139 +0,0 @@ -from sploit.arch import arch -from sploit.rev.gadget import Gadget -from sploit.symtbl import Symtbl -from sploit.util.cmd import run_cmd_cached -from sploit.util.log import ilog - -from collections import namedtuple as nt -from functools import cache -import json -import re - -def run_cmd(binary,cmd): - return run_cmd_cached(['r2','-q','-c',cmd,'-e','scr.color=false','-e','rop.len=10','-e','search.in=io.maps.x',binary]) - -def get_elf_symbols(elf): - ilog(f'Retrieving symbols of {elf} with r2...') - - base = get_bin_info(elf)['baddr'] - - sect = json.loads(run_cmd(elf,'iSj')[0]) - sect = {s['name']:s['vaddr'] for s in sect} - - syms = json.loads(run_cmd(elf,'isj')[0]) - syms = [s for s in syms if s['type'] in ['OBJ', 'FUNC', 'NOTYPE']] - - plt = [s for s in syms if s['is_imported']] - plt = {sym['realname']:sym['vaddr'] for sym in plt} - plt = Symtbl(base=sect.get('.plt',0), **plt) - - syms = [s for s in syms if not s['is_imported']] - syms = {sym['realname']:sym['vaddr'] for sym in syms} - syms = Symtbl(base=base, **syms) - - got = json.loads(run_cmd(elf,'irj')[0]) - got = {sym['name']:sym['vaddr'] for sym in got if 'name' in sym} - got = Symtbl(base=sect.get('.got',0), **got) - - strings = json.loads(run_cmd(elf,'izj')[0]) - strings = {s['string']:s['vaddr'] for s in strings} - strings = Symtbl(base=sect.get('.rodata',0), **strings) - - sect = Symtbl(**sect) - syms.sect = sect - syms.imp = plt - syms.rel = got - syms.str = strings - return syms - -def get_locals(binary,func): - ilog(f'Retrieving local stack frame of {hex(func)} in {binary} with r2...') - - addr = hex(func) - cmd_locals = f's {func};af;aafr;aaft;afvf' - out = run_cmd(binary,cmd_locals) - out = [re.split(r':?\s+',var) for var in out] - out = {var[1]:-(int(var[0],0)-arch.wordsize) for var in out} - return Symtbl(sbp=0, **out) - -def rop_json(binary): - # Gadget JSON schema: - # [ - # { - # retaddr: int - # size: int - # opcodes: [ - # { - # offset: int - # size: int - # opcode: string - # type: string - # } - # ] - # } - # ] - return json.loads("\n".join(run_cmd(binary, "/Rj"))) - -@cache -def rop_gadgets(binary, *regexes, cont=False): - ilog(f"Searching {binary} for {'; '.join(regexes)} gadgets with r2...") - gadgets = rop_json(binary) - results = [] - result_offsets = [] - base = get_bin_info(binary)['baddr'] - - for gadget in gadgets: - opcodes = gadget['opcodes'] - end_idx = len(opcodes) - len(regexes) - - for start_idx in range(end_idx + 1): - idx = start_idx - size = end_idx - idx - regexes_use = (regexes + (".*",) * size) if cont else regexes - - offset = opcodes[idx]['offset'] - base - if offset in result_offsets: - continue - - matches = [] - - for regex in regexes_use: - match = re.fullmatch(regex, opcodes[idx]['opcode']) - if not match: - break - matches.append(match) - idx += 1 - - if len(matches) == len(regexes_use): - results.append(Gadget(offset, matches)) - result_offsets.append(offset) - - return results - -def rop_gadget(binary, *regexes): - results = rop_gadgets(binary, *regexes) - if len(results) == 0: - raise LookupError(f"Could not find gadget for: {'; '.join(regexes)}") - return results[0] - -@cache -def get_call_returns(binary,xref_from,xref_to): - ilog(f'Getting return addresses of calls from {hex(xref_from)} to {hex(xref_to)} in {binary} with r2...') - - cmd_xrefs = f's {hex(xref_from)};af;axq' - xrefs = run_cmd(binary,cmd_xrefs) - xrefs = [re.split(r'\s+',x) for x in xrefs] - xrefs = [x for x in xrefs if int(x[2],0)==xref_to] - rets = [] - CallRet = nt("CallRet", "xref_from xref_to call_addr ret_addr") - for x in xrefs: - cmd_ret = f's {x[0]};so;s' - ret = run_cmd(binary,cmd_ret) - rets.append(CallRet(xref_from,xref_to,int(x[0],0),int(ret[0],0))) - return rets - -@cache -def get_bin_info(binary): - ilog(f'Retrieving binary and security info about {binary} with r2...') - - return json.loads(run_cmd(binary,'iIj')[0]) diff --git a/sploit/symtbl.py b/sploit/symtbl.py deleted file mode 100644 index 86800f5..0000000 --- a/sploit/symtbl.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -Symtbl data structure - -A Symtbl (symbol table) is an associative data container intended to model -arbitrary memory layouts, such as structure definitions or memory-mapped -objects. Elements may be accessed via subscript or attribute notation. - -A Symtbl is essentially a dictionary, in which each key (symbol name string) -is associated with an offset value. A special key "base" represents the -base or starting address of the overall table in memory. Whenever offset -values are accessed, they are adjusted relative to the table's base value. -This enables the primary function of Symtbl objects: the ability to resolve -mapped, or absolute, addresses of objects in memory. - -Therefore, even though a Symtbl internally tracks symbol offsets, the apparent -value of any symbol will always be its offset plus the table's base address. -The table's base address will also be subtracted from values being stored in -the table, as the provided value is assumed to be mapped in the same manner as -the table itself. - - s = Symtbl() - s.a = 10 - s.b = 20 - print(s.a, s.b) # "10 20" - s.base = 100 - print(s.a, s.b) # "110 120" - s.c = 150 - s.base = 10 - print(s.a, s.b, s.c) # "20 30 60" - -A Symtbl's base value may be changed at any time, and this will affect the -interpretation of offsets as described above. However, one may also create a -remapped version of a Symtbl (without modifying the original) using the '@' -operator. This new object will have the base value given on the right hand -side of the '@' and its collection of symbols is referentially linked to the -source object, meaning changes to symbol entries will be visible in both -objects. - - s1 = Symtbl() - s1.a = 10 - s2 = s1 @ 1000 - print(s1.a, s2.a) # "10 1010" - s2.b = 1234 - print(s1.b, s2.b) # "234 1234" - -Symtbl's are also nestable, to support modeling composite memory layouts. If -a symbol's value is assigned to another Symtbl object, rather than an integer -offset, the child object's base value serves as its offset in the parent -Symtbl. Symbols on the child object may then be accessed recursively from the -parent's scope. If the parent has a non-zero base, it adjusts the offsets -interpreted in the child. - - child = Symtbl() - child.a = 1 - child.b = 2 - parent = Symtbl() - parent.nested = child @ 70 - print(parent.nested.a, parent.nested.b) # "71 72" - -A Symtbl will allow you to uniformly adjust all offsets contained, while leaving -the base value the same, using the '<<' and '>>' operators. A custom -"rebase" operation is also available via the "%" operator. A rebase applies -a uniform shift, such that the right hand side offset operand ends up coinciding -with the Symtbl base address. - - s = Symtbl() - s.a = 1 - s.b = 2 - s.c = 3 - s.d = 4 - s.base = 1000 - s %= s.c # rebase at symbol 'c' - 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. - - 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(base, 0, dict()) - for k, v in symbols.items(): - self[k] = v - return self - -class SymtblImpl(IndexTbl): - """Symtbl implementation class""" - - def __init__(self, base, adjust, entries): - """Construct Symtbl from instance data.""" - super().__init__(base) - object.__setattr__(self, "__adjust__", adjust) - object.__setattr__(self, "__entries__", 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 key, value in self: - key = f"[{key}]" if isinstance(value, IndexEntry) else key - s += FMT.format(hex(value), key) - - return s - - def __rshift__(self, offset): - """Return symbol-adjusted version of object.""" - adjust = self.__adjust__ + int(offset) - return SymtblImpl(self.base, adjust, self.__entries__) - - def __lshift__(self, offset): - """Return symbol-adjusted version of object.""" - return self >> (-offset) - - def __mod__(self, offset): - """Return symbol-rebased version of object.""" - return self >> (self.base - offset) - - # IndexTbl abstract methods - - def __copy__(self): - """Return copy of object with shared symbol entries.""" - return SymtblImpl(self.base, self.__adjust__, self.__entries__) - - 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 __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 __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 __delindex__(self, index): - """Delete symbol.""" - del self.__entries__[index] diff --git a/sploit/types/__init__.py b/sploit/types/__init__.py deleted file mode 100644 index a618162..0000000 --- a/sploit/types/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .indextbl import * -from .index_entry import * -from .lict import * diff --git a/sploit/types/index_entry.py b/sploit/types/index_entry.py deleted file mode 100644 index a03ab92..0000000 --- a/sploit/types/index_entry.py +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 4f57a59..0000000 --- a/sploit/types/indextbl.py +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index ab6cb1f..0000000 --- a/sploit/types/lict.py +++ /dev/null @@ -1,202 +0,0 @@ -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 diff --git a/sploit/until.py b/sploit/until.py deleted file mode 100644 index b4f390f..0000000 --- a/sploit/until.py +++ /dev/null @@ -1,14 +0,0 @@ -from functools import partial as bind -import re - -def lastline(pred, /, *args, **kwargs): - s = args[-1] - args = args[:-1] - p = bind(pred, *args, **kwargs) - return p(s[-1]) - -def contains(regex, s): - return re.search(regex, s) - -def equals(regex, s): - return re.fullmatch(regex, s) diff --git a/sploit/util/__init__.py b/sploit/util/__init__.py deleted file mode 100644 index 32a079b..0000000 --- a/sploit/util/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .cmd import * -from .log import * diff --git a/sploit/util/cmd.py b/sploit/util/cmd.py deleted file mode 100644 index 3a2b842..0000000 --- a/sploit/util/cmd.py +++ /dev/null @@ -1,26 +0,0 @@ -from os import path -from subprocess import run - -def run_cmd(cmd,cwd=None): - return run(cmd,cwd=cwd,capture_output=True,text=True,check=True).stdout.split('\n')[:-1] - -__RUN_CACHE__ = {} -def run_cmd_cached(cmd,cwd=None): - key = ''.join(cmd) - if key in __RUN_CACHE__: - return __RUN_CACHE__[key] - else: - result = run_cmd(cmd,cwd) - __RUN_CACHE__[key] = result - return result - -#try to get the version through git -def git_version(): - try: - cwd = path.dirname(path.realpath(__file__)) - version = run_cmd(["git","describe","--always","--first-parent","--dirty"],cwd=cwd)[0] - #PEP440 compliance - version = version.replace('-','+',1).replace('-','.') - return version - except: - return "0+unknown.version" diff --git a/sploit/util/log.py b/sploit/util/log.py deleted file mode 100644 index 823b252..0000000 --- a/sploit/util/log.py +++ /dev/null @@ -1,32 +0,0 @@ -import codecs -import sys - -# https://docs.python.org/3/library/codecs.html#standard-encodings -ENCODING = None - -ERROR = 31 -WARNING = 33 -STATUS = 32 -NORMAL = 0 -ALT = 90 - -def enc_value(value, enc): - if type(value) is bytes: - if enc is not None: - value = codecs.encode(value, enc) - elif ENCODING is not None: - value = codecs.encode(value, ENCODING) - value = str(value)[2:-1] # strip b'' - return str(value) - -def generic_log(*values, sep, end, file, flush, enc, color): - string = sep.join([ enc_value(x, enc) for x in values ]) - print(f'\033[{color}m{string}\033[0m', end=end, file=file, flush=flush) - -# For library internal use -def ilog(*values, sep=' ', end='\n', file=sys.stderr, flush=True, enc=None, color=STATUS): - generic_log(*values, sep=sep, end=end, file=file, flush=flush, enc=enc, color=color) - -# For external use in user script (via print = elog) -def elog(*values, sep=' ', end='\n', file=sys.stdout, flush=True, enc=None, color=ALT): - generic_log(*values, sep=sep, end=end, file=file, flush=flush, enc=enc, color=color) |