#!/usr/bin/env python3 # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # import io import os from enum import IntEnum from typing import Optional, Sequence, Mapping, Tuple from elftools.common.utils import preserve_stream_pos from elftools.elf.constants import P_FLAGS, SH_FLAGS from elftools.elf.elffile import ELFFile from elftools.elf.relocation import RelocationHandler from elftools.elf.sections import Symbol, SymbolTableSection from elftools.elf.descriptions import describe_reloc_type from unicorn.unicorn_const import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC from qiling import Qiling from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS from qiling.exception import QlErrorELFFormat, QlMemoryMappedError from qiling.loader.loader import QlLoader, Image from qiling.os.linux.function_hook import FunctionHook from qiling.os.linux.syscall_nums import SYSCALL_NR from qiling.os.linux.kernel_api.hook import * from qiling.os.linux.kernel_api.kernel_api import hook_sys_open, hook_sys_read, hook_sys_write # auxiliary vector types # see: https://man7.org/linux/man-pages/man3/getauxval.3.html class AUXV(IntEnum): AT_NULL = 0 AT_IGNORE = 1 AT_EXECFD = 2 AT_PHDR = 3 AT_PHENT = 4 AT_PHNUM = 5 AT_PAGESZ = 6 AT_BASE = 7 AT_FLAGS = 8 AT_ENTRY = 9 AT_NOTELF = 10 AT_UID = 11 AT_EUID = 12 AT_GID = 13 AT_EGID = 14 AT_PLATFORM = 15 AT_HWCAP = 16 AT_CLKTCK = 17 AT_SECURE = 23 AT_BASE_PLATFORM = 24 AT_RANDOM = 25 AT_HWCAP2 = 26 AT_EXECFN = 31 # start area memory for API hooking # we will reserve 0x1000 bytes for this (which contains multiple slots of 4/8 bytes, each for one api) API_HOOK_MEM = 0x1000000 # memory for syscall table SYSCALL_MEM = API_HOOK_MEM + 0x1000 class QlLoaderELF(QlLoader): def __init__(self, ql: Qiling): super().__init__(ql) def run(self): if self.ql.code: self.ql.mem.map(self.ql.os.entry_point, self.ql.os.code_ram_size, info="[shellcode_stack]") shellcode_base = self.ql.os.entry_point + 0x200000 - 0x1000 self.ql.mem.write(shellcode_base, self.ql.code) self.ql.arch.regs.arch_sp = shellcode_base self.ql.os.entry_point = shellcode_base self.load_address = shellcode_base return self.profile = self.ql.os.profile[f'OS{self.ql.arch.bits}'] # setup program stack stack_address = self.profile.getint('stack_address') stack_size = self.profile.getint('stack_size') self.ql.mem.map(stack_address, stack_size, info='[stack]') self.path = self.ql.path with open(self.path, 'rb') as infile: fstream = io.BytesIO(infile.read()) elffile = ELFFile(fstream) elftype = elffile['e_type'] # is it a driver? if elftype == 'ET_REL': self.load_driver(elffile, stack_address + stack_size, loadbase=0x8000000) self.ql.hook_code(hook_kernel_api) # is it an executable? elif elftype == 'ET_EXEC': load_address = 0 self.load_with_ld(elffile, stack_address + stack_size, load_address, self.argv, self.env) # is it a shared object? elif elftype == 'ET_DYN': load_address = self.profile.getint('load_address') self.load_with_ld(elffile, stack_address + stack_size, load_address, self.argv, self.env) else: raise QlErrorELFFormat(f'unexpected elf type value (e_type = {elftype})') self.is_driver = (elftype == 'ET_REL') self.ql.arch.regs.arch_sp = self.stack_address # No idea why. if self.ql.os.type == QL_OS.FREEBSD: # self.ql.arch.regs.rbp = self.stack_address + 0x40 self.ql.arch.regs.rdi = self.stack_address self.ql.arch.regs.r14 = self.stack_address @staticmethod def seg_perm_to_uc_prot(perm: int) -> int: """Translate ELF segment perms to Unicorn protection constants. """ prot = UC_PROT_NONE if perm & P_FLAGS.PF_X: prot |= UC_PROT_EXEC if perm & P_FLAGS.PF_W: prot |= UC_PROT_WRITE if perm & P_FLAGS.PF_R: prot |= UC_PROT_READ return prot def load_with_ld(self, elffile: ELFFile, stack_addr: int, load_address: int, argv: Sequence[str] = [], env: Mapping[str, str] = {}): def load_elf_segments(elffile: ELFFile, load_address: int, info: str): # get list of loadable segments; these segments will be loaded to memory load_segments = sorted(elffile.iter_segments(type='PT_LOAD'), key=lambda s: s['p_vaddr']) # determine the memory regions that need to be mapped in order to load the segments. # note that region boundaries are aligned to page, which means they may be larger than # the segment they contain. to reduce mapping clutter, adjacent regions with the same # perms are consolidated into one contigous memory region load_regions: Sequence[Tuple[int, int, int]] = [] # iterate over loadable segments for seg in load_segments: lbound = self.ql.mem.align(load_address + seg['p_vaddr']) ubound = self.ql.mem.align_up(load_address + seg['p_vaddr'] + seg['p_memsz']) perms = QlLoaderELF.seg_perm_to_uc_prot(seg['p_flags']) if load_regions: prev_lbound, prev_ubound, prev_perms = load_regions[-1] # new region starts where the previous one ended if lbound == prev_ubound: # same perms? extend previous memory region if perms == prev_perms: load_regions[-1] = (prev_lbound, ubound, prev_perms) # different perms? start a new one else: load_regions.append((lbound, ubound, perms)) # start a new memory region elif lbound > prev_ubound: load_regions.append((lbound, ubound, perms)) # overlapping segments? something probably went wrong elif lbound < prev_ubound: # EDL ELF files use 0x400 bytes pages, which might make some segments look as if they # start at the same segment as their predecessor. though that is fixable, unicorn # supports only 0x1000 bytes pages; this becomes problematic when using mem.protect # # this workaround unifies such "overlapping" segments, which may apply more permissive # protection flags to that memory region. if self.ql.arch.type == QL_ARCH.ARM64: load_regions[-1] = (prev_lbound, ubound, prev_perms | perms) continue raise RuntimeError else: load_regions.append((lbound, ubound, perms)) # map the memory regions for lbound, ubound, perms in load_regions: size = ubound - lbound # there might be a region with zero size. in this case, do not mmap it if size: try: self.ql.mem.map(lbound, size, perms, os.path.basename(info)) except QlMemoryMappedError: self.ql.log.exception(f'Failed to map {lbound:#x}-{ubound:#x}') else: self.ql.log.debug(f'Mapped {lbound:#x}-{ubound:#x}') # load loadable segments contents to memory for seg in load_segments: self.ql.mem.write(load_address + seg['p_vaddr'], seg.data()) return load_regions[0][0], load_regions[-1][1] mem_start, mem_end = load_elf_segments(elffile, load_address, self.path) self.elf_entry = entry_point = load_address + elffile['e_entry'] self.ql.log.debug(f'mem_start : {mem_start:#x}') self.ql.log.debug(f'mem_end : {mem_end:#x}') # by convention the loaded binary is first on the list self.images.append(Image(mem_start, mem_end, os.path.abspath(self.path))) # note: 0x2000 is the size of [hook_mem] self.brk_address = mem_end + 0x2000 # determine interpreter path interp_seg = next(elffile.iter_segments(type='PT_INTERP'), None) interp_path = str(interp_seg.get_interp_name()) if interp_seg else '' interp_address = 0 # load the interpreter, if there is one if interp_path: interp_vpath = self.ql.os.path.virtual_abspath(interp_path) interp_hpath = self.ql.os.path.virtual_to_host_path(interp_path) self.ql.log.debug(f'Interpreter path: {interp_vpath}') if not self.ql.os.path.is_safe_host_path(interp_hpath): raise PermissionError(f'unsafe path: {interp_hpath}') with open(interp_hpath, 'rb') as infile: interp = ELFFile(infile) min_vaddr = min(seg['p_vaddr'] for seg in interp.iter_segments(type='PT_LOAD')) # determine interpreter base address # some old interpreters may not be PIE: p_vaddr of the first LOAD segment is not zero # we should load interpreter at the address p_vaddr specified in such situation interp_address = self.profile.getint('interp_address') if min_vaddr == 0 else 0 self.ql.log.debug(f'Interpreter addr: {interp_address:#x}') # load interpreter segments data to memory interp_start, interp_end = load_elf_segments(interp, interp_address, interp_vpath) # add interpreter to the loaded images list self.images.append(Image(interp_start, interp_end, interp_hpath)) # determine entry point entry_point = interp_address + interp['e_entry'] # set mmap addr mmap_address = self.profile.getint('mmap_address') self.ql.log.debug(f'mmap_address is : {mmap_address:#x}') # set info to be used by gdb self.mmap_address = mmap_address # set elf table elf_table = bytearray() new_stack = stack_addr def __push_str(top: int, s: str) -> int: """Write a string to stack memory and adjust the top of stack accordingly. Top of stack remains aligned to pointer size """ data = s.encode('latin') + b'\x00' top = self.ql.mem.align(top - len(data), self.ql.arch.pointersize) self.ql.mem.write(top, data) return top # write argc elf_table.extend(self.ql.pack(len(argv))) # write argv for s in argv: new_stack = __push_str(new_stack, s) elf_table.extend(self.ql.pack(new_stack)) # add a nullptr sentinel elf_table.extend(self.ql.pack(0)) # write env for k, v in env.items(): new_stack = __push_str(new_stack, f'{k}={v}') elf_table.extend(self.ql.pack(new_stack)) # add a nullptr sentinel elf_table.extend(self.ql.pack(0)) new_stack = randstraddr = __push_str(new_stack, 'a' * 16) new_stack = cpustraddr = __push_str(new_stack, 'i686') new_stack = execfn = __push_str(new_stack, argv[0]) # store aux vector data for gdb use elf_phdr = elffile['e_phoff'] + mem_start elf_phent = elffile['e_phentsize'] elf_phnum = elffile['e_phnum'] if self.ql.arch.bits == 64: elf_hwcap = 0x078bfbfd elif self.ql.arch.bits == 32: elf_hwcap = 0x1fb8d7 if self.ql.arch.endian == QL_ENDIAN.EB: # FIXME: considering this is a 32 bits value, it is not a big-endian version of the # value above like it is meant to be, since the one above has an implied leading zero # byte (i.e. 0x001fb8d7) which the EB value didn't take into account elf_hwcap = 0xd7b81f # setup aux vector auxv_entries = ( (AUXV.AT_HWCAP, elf_hwcap), (AUXV.AT_PAGESZ, self.ql.mem.pagesize), (AUXV.AT_CLKTCK, 100), (AUXV.AT_PHDR, elf_phdr), (AUXV.AT_PHENT, elf_phent), (AUXV.AT_PHNUM, elf_phnum), (AUXV.AT_BASE, interp_address), (AUXV.AT_FLAGS, 0), (AUXV.AT_ENTRY, self.elf_entry), (AUXV.AT_UID, self.ql.os.uid), (AUXV.AT_EUID, self.ql.os.euid), (AUXV.AT_GID, self.ql.os.gid), (AUXV.AT_EGID, self.ql.os.egid), (AUXV.AT_SECURE, 0), (AUXV.AT_RANDOM, randstraddr), (AUXV.AT_HWCAP2, 0), (AUXV.AT_EXECFN, execfn), (AUXV.AT_PLATFORM, cpustraddr), (AUXV.AT_NULL, 0) ) bytes_before_auxv = len(elf_table) # add all auxv entries for key, val in auxv_entries: elf_table.extend(self.ql.pack(key)) elf_table.extend(self.ql.pack(val)) new_stack = self.ql.mem.align(new_stack - len(elf_table), 0x10) self.ql.mem.write(new_stack, bytes(elf_table)) self.auxv = new_stack + bytes_before_auxv self.stack_address = new_stack self.load_address = load_address self.init_sp = self.ql.arch.regs.arch_sp self.ql.os.entry_point = self.entry_point = entry_point self.ql.os.elf_entry = self.elf_entry self.ql.os.function_hook = FunctionHook(self.ql, elf_phdr, elf_phnum, elf_phent, load_address, mem_end) # If there is a loader, we ignore exit self.skip_exit_check = (self.elf_entry != self.entry_point) # map vsyscall section for some specific needs if self.ql.arch.type == QL_ARCH.X8664 and self.ql.os.type == QL_OS.LINUX: vsyscall_addr = self.profile.getint('vsyscall_address') vsyscall_ids = ( SYSCALL_NR.gettimeofday, SYSCALL_NR.time, SYSCALL_NR.getcpu ) # each syscall should be 1KiB away entry_size = 1024 vsyscall_size = self.ql.mem.align_up(len(vsyscall_ids) * entry_size) if self.ql.mem.is_available(vsyscall_addr, vsyscall_size): # initialize with int3 instructions then insert syscall entry self.ql.mem.map(vsyscall_addr, vsyscall_size, info="[vsyscall]") assembler = self.ql.arch.assembler def __assemble(asm: str) -> bytes: bs, _ = assembler.asm(asm) return bytes(bs) for i, scid in enumerate(vsyscall_ids): entry = __assemble(f'mov rax, {scid:#x}; syscall; ret') self.ql.mem.write(vsyscall_addr + i * entry_size, entry.ljust(entry_size, b'\xcc')) def lkm_get_init(self, elffile: ELFFile) -> int: """Get file offset of the init_module function. """ symbol_tables = (sec for sec in elffile.iter_sections() if type(sec) is SymbolTableSection) for sec in symbol_tables: syms = sec.get_symbol_by_name('init_module') if syms: sym = syms[0] addr = sym['st_value'] + elffile.get_section(sym['st_shndx'])['sh_offset'] return addr raise QlErrorELFFormat('invalid module: symbol init_module not found') def lkm_dynlinker(self, elffile: ELFFile, mem_start: int) -> Mapping[str, int]: def __get_symbol(name: str) -> Optional[Symbol]: _symtab = elffile.get_section_by_name('.symtab') _sym = _symtab.get_symbol_by_name(name) return _sym[0] if _sym else None ql = self.ql all_symbols = [] self.ql.os.hook_addr = API_HOOK_MEM # map address to symbol name self.import_symbols = {} # reverse dictionary to map symbol name -> address rev_reloc_symbols = {} rh = RelocationHandler(elffile) sections = [sec for sec in elffile.iter_sections() if sec['sh_flags'] & SH_FLAGS.SHF_ALLOC] for sec in sections: reloc_sec = rh.find_relocations_for_section(sec) if reloc_sec and reloc_sec.name != '.rela.gnu.linkonce.this_module': # get the symbol table section pointed in sh_link symtab = elffile.get_section(reloc_sec['sh_link']) assert isinstance(symtab, SymbolTableSection) for rel in reloc_sec.iter_relocations(): # if reloc['r_info_sym'] == 0: # continue symbol = symtab.get_symbol(rel['r_info_sym']) assert symbol # Some symbols have zero 'st_name', so instead what's used is # the name of the section they point at. if symbol['st_name'] == 0: symsec = elffile.get_section(symbol['st_shndx']) symbol_name = symsec.name sym_offset = symsec['sh_offset'] rev_reloc_symbols[symbol_name] = sym_offset + mem_start else: symbol_name = symbol.name # get info about related section to be patched info_section = elffile.get_section(reloc_sec['sh_info']) sym_offset = info_section['sh_offset'] if symbol_name in all_symbols: sym_offset = rev_reloc_symbols[symbol_name] - mem_start else: all_symbols.append(symbol_name) _symbol = __get_symbol(symbol_name) if _symbol['st_shndx'] == 'SHN_UNDEF': # external symbol # only save symbols of APIs # we need to lookup from address to symbol, so we can find the right callback # for sys_xxx handler for syscall, the address must be aligned to pointer size if symbol_name.startswith('sys_'): self.ql.os.hook_addr = self.ql.mem.align_up(self.ql.os.hook_addr, self.ql.arch.pointersize) self.import_symbols[self.ql.os.hook_addr] = symbol_name # FIXME: this is for rootkit to scan for syscall table from page_offset_base # write address of syscall table to this slot, so syscall scanner can quickly find it if symbol_name == "page_offset_base": ql.mem.write_ptr(self.ql.os.hook_addr, SYSCALL_MEM) # we also need to do reverse lookup from symbol to address rev_reloc_symbols[symbol_name] = self.ql.os.hook_addr sym_offset = self.ql.os.hook_addr - mem_start self.ql.os.hook_addr += self.ql.arch.pointersize elif _symbol['st_shndx'] == 'SHN_ABS': rev_reloc_symbols[symbol_name] = _symbol['st_value'] else: # local symbol _section = elffile.get_section(_symbol['st_shndx']) rev_reloc_symbols[symbol_name] = _section['sh_offset'] + _symbol['st_value'] + mem_start # ql.log.info(f'relocating: {symbol_name} -> {rev_reloc_symbols[symbol_name]:#010x}') # FIXME: using the rh.apply_section_relocations method for the following relocation work # seems to be cleaner. loc = elffile.get_section(reloc_sec['sh_info'])['sh_offset'] + rel['r_offset'] loc += mem_start desc = describe_reloc_type(rel['r_info_type'], elffile) if desc in ('R_X86_64_32S', 'R_X86_64_32'): # patch this reloc if rel['r_addend']: val = sym_offset + rel['r_addend'] val += mem_start else: val = rev_reloc_symbols[symbol_name] ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4) elif desc == 'R_X86_64_64': val = sym_offset + rel['r_addend'] val += 0x2000000 # init_module position: FIXME ql.mem.write_ptr(loc, val, 8) elif desc == 'R_X86_64_PC64': val = rel['r_addend'] - loc val += rev_reloc_symbols[symbol_name] ql.mem.write_ptr(loc, val, 8) elif desc in ('R_X86_64_PC32', 'R_X86_64_PLT32'): val = rel['r_addend'] - loc val += rev_reloc_symbols[symbol_name] ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4) elif desc in ('R_386_PC32', 'R_386_PLT32'): val = ql.mem.read_ptr(loc, 4) val += rev_reloc_symbols[symbol_name] - loc ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4) elif desc in ('R_386_32', 'R_MIPS_32'): val = ql.mem.read_ptr(loc, 4) val += rev_reloc_symbols[symbol_name] ql.mem.write_ptr(loc, (val & 0xFFFFFFFF), 4) elif desc == 'R_MIPS_HI16': # actual relocation is done in R_MIPS_LO16 prev_mips_hi16_loc = loc elif desc == 'R_MIPS_LO16': val = ql.mem.read_ptr(prev_mips_hi16_loc + 2, 2) << 16 | ql.mem.read_ptr(loc + 2, 2) val = rev_reloc_symbols[symbol_name] + val # *(word)(mips_lo16_loc + 2) is treated as signed if (val & 0xFFFF) >= 0x8000: val += (1 << 16) ql.mem.write_ptr(prev_mips_hi16_loc + 2, (val >> 16), 2) ql.mem.write_ptr(loc + 2, (val & 0xFFFF), 2) else: raise NotImplementedError(f'Relocation type {desc} not implemented') return rev_reloc_symbols def load_driver(self, elffile: ELFFile, stack_addr: int, loadbase: int = 0) -> None: elfdata_mapping = self.get_elfdata_mapping(elffile) # FIXME: determine true memory boundaries, taking relocation into account (if requested) mem_start = 0 mem_end = mem_start + self.ql.mem.align_up(len(elfdata_mapping), 0x1000) # map some memory to intercept external functions of Linux kernel self.ql.mem.map(API_HOOK_MEM, 0x1000, info="[api_mem]") self.ql.log.debug(f'loadbase : {loadbase:#x}') self.ql.log.debug(f'mem_start : {mem_start:#x}') self.ql.log.debug(f'mem_end : {mem_end:#x}') self.ql.mem.map(loadbase + mem_start, mem_end - mem_start, info=self.ql.path) self.ql.mem.write(loadbase + mem_start, elfdata_mapping) init_module = self.lkm_get_init(elffile) + loadbase + mem_start self.ql.log.debug(f'init_module : {init_module:#x}') self.brk_address = mem_end + loadbase # Set MMAP addr mmap_address = self.profile.getint('mmap_address') self.ql.log.debug(f'mmap_address is : {mmap_address:#x}') # self.ql.os.elf_entry = self.elf_entry = loadbase + elfhead['e_entry'] self.ql.os.entry_point = self.entry_point = init_module self.elf_entry = self.ql.os.elf_entry = self.ql.os.entry_point self.stack_address = self.ql.mem.align(stack_addr, self.ql.arch.pointersize) self.load_address = loadbase # remember address of syscall table, so external tools can access to it # self.ql.os.syscall_addr = SYSCALL_MEM # setup syscall table self.ql.mem.map(SYSCALL_MEM, 0x1000, info="[syscall_mem]") self.ql.mem.write(SYSCALL_MEM, b'\x00' * 0x1000) rev_reloc_symbols = self.lkm_dynlinker(elffile, mem_start + loadbase) # iterate over relocatable symbols, but pick only those who start with 'sys_' for sc, addr in rev_reloc_symbols.items(): if sc.startswith('sys_') and sc != 'sys_call_table': tmp_sc = sc[4:] if hasattr(SYSCALL_NR, tmp_sc): syscall_id = getattr(SYSCALL_NR, tmp_sc).value dest = SYSCALL_MEM + syscall_id * self.ql.arch.pointersize self.ql.log.debug(f'Writing syscall {tmp_sc} to {dest:#x}') self.ql.mem.write_ptr(dest, addr) # write syscall addresses into syscall table self.ql.mem.write_ptr(SYSCALL_MEM + 0 * self.ql.arch.pointersize, self.ql.os.hook_addr + 0 * self.ql.arch.pointersize) self.ql.mem.write_ptr(SYSCALL_MEM + 1 * self.ql.arch.pointersize, self.ql.os.hook_addr + 1 * self.ql.arch.pointersize) self.ql.mem.write_ptr(SYSCALL_MEM + 2 * self.ql.arch.pointersize, self.ql.os.hook_addr + 2 * self.ql.arch.pointersize) # setup hooks for read/write/open syscalls self.import_symbols[self.ql.os.hook_addr + 0 * self.ql.arch.pointersize] = hook_sys_read self.import_symbols[self.ql.os.hook_addr + 1 * self.ql.arch.pointersize] = hook_sys_write self.import_symbols[self.ql.os.hook_addr + 2 * self.ql.arch.pointersize] = hook_sys_open def get_elfdata_mapping(self, elffile: ELFFile) -> bytes: # from io import BytesIO # # rh = RelocationHandler(elffile) # # for sec in elffile.iter_sections(): # rs = rh.find_relocations_for_section(sec) # # if rs is not None: # ss = BytesIO(sec.data()) # rh.apply_section_relocations(ss, rs) # # # apply changes to stream # elffile.stream.seek(sec['sh_offset']) # elffile.stream.write(ss.getbuffer()) # # TODO: need to patch hooked symbols with their hook targets # (e.g. replace calls to 'printk' with the hooked address that # was allocate for it) elfdata_mapping = bytearray() # pick up elf header with preserve_stream_pos(elffile.stream): elffile.stream.seek(0) elf_header = elffile.stream.read(elffile['e_ehsize']) elfdata_mapping.extend(elf_header) # FIXME: normally the address of a section would be determined by its 'sh_addr' value. # in case of a relocatable object all its sections' sh_addr will be set to zero, so # the value in 'sh_offset' should be used to determine the final address. # see: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html # # here we presume this a relocatable object and don't do any relocation (that is, it # is relocated to 0) # pick up loadable sections for sec in elffile.iter_sections(): if sec['sh_flags'] & SH_FLAGS.SHF_ALLOC: # pad aggregated elf data to the offset of the current section elfdata_mapping.extend(b'\x00' * (sec['sh_offset'] - len(elfdata_mapping))) # aggregate section data elfdata_mapping.extend(sec.data()) return bytes(elfdata_mapping)