def setup(self, stack_commit=0): # Set the emulator to run in protected mode self.om = objman.ObjectManager(emu=self) arch = self.get_arch() self._setup_gdt(arch) self.setup_user_shared_data() self.set_ptr_size(self.arch) if arch == _arch.ARCH_X86: self.peb_addr = self.fs_addr + 0x30 elif arch == _arch.ARCH_AMD64: self.peb_addr = self.gs_addr + 0x60 self.api = WindowsApi(self) # Init symlinks for sl in self.symlinks: self.om.add_symlink(sl['name'], sl['target']) self.init_sys_modules(self.config_system_modules)
class Win32Emulator(WindowsEmulator): """ User Mode Windows Emulator Class """ def __init__(self, config, argv=[], debug=False, logger=None, exit_event=None): super(Win32Emulator, self).__init__(config, debug=debug, logger=logger, exit_event=exit_event) self.last_error = 0 self.peb_addr = 0 self.heap_allocs = [] self.argv = argv self.sessman = SessionManager(config) self.com = COM(config) def get_argv(self): """ Get command line arguments (if any) that are being passed to the emulated process. (e.g. main(argv)) """ argv0 = '' out = [] if len(self.argv): for m in self.modules: pe = m[0] emu_path = m[2] if pe.is_exe(): argv0 = emu_path out = [argv0] + self.argv elif self.command_line: out = shlex.split(self.command_line, posix=False) return out def set_last_error(self, code): """ Set the last error code for the current thread """ if self.curr_thread: self.curr_thread.set_last_error(code) def get_last_error(self): """ Get the last error code for the current thread """ if self.curr_thread: return self.curr_thread.get_last_error() def get_session_manager(self): """ Get the session manager for the emulator. This will manage things like desktops, windows, and session isolation """ return self.sessman def add_vectored_exception_handler(self, first, handler): """ Add a vectored exception handler that will be executed on an exception """ self.veh_handlers.append(handler) def remove_vectored_exception_handler(self, handler): """ Remove a vectored exception handler """ self.veh_handlers.remove(handler) def get_processes(self): if len(self.processes) <= 1: self.init_processes(self.config_processes) return self.processes def init_processes(self, processes): """ Initialize configured processes set in the emulator config """ for proc in processes: p = objman.Process(self) self.add_object(p) p.name = proc.get('name', '') new_pid = proc.get('pid') if new_pid: p.pid = new_pid base = proc.get('base_addr') if isinstance(base, str): base = int(base, 16) p.base = base p.path = proc.get('path') p.session = proc.get('session', 0) p.image = ntpath.basename(p.path) self.processes.append(p) def load_module(self, path=None, data=None): """ Load a module into the emulator space from the specified path """ pe = self.load_pe(path=path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError('Unsupported architecture: %s', pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) if not data: file_name = os.path.basename(path) + '.exe' mod_name = os.path.splitext(file_name)[0] else: mod_hash = hashlib.sha256() mod_hash.update(data) mod_hash = mod_hash.hexdigest() mod_name = mod_hash file_name = '%s.exe' % (mod_name) self.api = WindowsApi(self) cd = self.get_cd() if not cd.endswith('\\'): cd += '\\' emu_path = cd + file_name if not data: with open(path, 'rb') as f: data = f.read() self.fileman.add_existing_file(emu_path, data) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: self.profiler.strings['ansi'] = self.get_ansi_strings(data) self.profiler.strings['unicode'] = self.get_unicode_strings(data) # Set the emulated path emu_path = '' self.cd = self.get_cd() if self.cd: if not self.cd.endswith('\\'): self.cd += '\\' emu_path = self.cd + os.path.basename(file_name) pe.set_emu_path(emu_path) self.map_pe(pe, mod_name=mod_name, emu_path=emu_path) self.mem_write(pe.base, pe.mapped_image) self.setup() if not self.stack_base: self.stack_base, stack_addr = self.alloc_stack(0x12000) self.set_func_args(self.stack_base, self.return_hook) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write( addr, data_ptr.to_bytes(self.get_ptr_size(), 'little')) return pe def run_module(self, module, all_entrypoints=False): """ Begin emulating a previously loaded module Arguments: module: Module to emulate """ if not module: self.stop() raise Win32EmuError('Module not found') # Check if any TLS callbacks exist, these run before the module's entry point tls = module.get_tls_callbacks() for i, cb_addr in enumerate(tls): base = module.get_base() if base < cb_addr < base + module.get_image_size(): run = Run() run.start_addr = cb_addr run.type = 'tls_callback_%d' % (i) run.args = [base, DLL_PROCESS_ATTACH, 0] self.add_run(run) # Queue up the module's main entry point ep = module.base + module.ep run = Run() run.start_addr = ep main_exe = None if not module.is_exe(): run.args = [module.base, DLL_PROCESS_ATTACH, 0] run.type = 'dll_entry.DLL_PROCESS_ATTACH' container = self.init_container_process() if container: self.processes.append(container) self.curr_process = container else: run.type = 'module_entry' main_exe = module run.args = [ self.mem_map(8, tag='emu.module_arg_%d' % (i)) for i in range(4) ] if main_exe: self.user_modules = [main_exe] + self.user_modules self.add_run(run) if all_entrypoints: # Only emulate a subset of all the exported functions # There are some modules (such as the windows kernel) with # thousands of exports exports = [ k for k in module.get_exports()[:MAX_EXPORTS_TO_EMULATE] ] if exports: args = [ self.mem_map(8, tag='emu.export_arg_%d' % (i), base=0x41420000) for i in range(4) ] # noqa for exp in exports: if exp.name in ('DllMain', 'ServiceMain'): continue run = Run() if exp.name: fn = exp.name else: fn = 'no_name' run.type = 'export.%s' % (fn) run.start_addr = exp.address # Here we set dummy args to pass into the export function run.args = args # Store these runs and only queue them before the unload # routine this is because some exports may not be ready to # be called yet self.add_run(run) # Create an empty process object for the module if none is # supplied if len(self.processes) == 0: p = objman.Process(self, path=module.get_emu_path(), base=module.base, pe=module, cmdline=self.command_line) self.curr_process = p self.om.objects.update({p.address: p}) mm = self.get_address_map(module.base) if mm: mm.process = self.curr_process t = objman.Thread(self, stack_base=self.stack_base, stack_commit=module.stack_commit) self.om.objects.update({t.address: t}) self.curr_process.threads.append(t) self.curr_thread = t peb = self.alloc_peb(self.curr_process) # Set the TEB self.init_teb(t, peb) # Begin emulation self.start() def emulate_module(self, path): """ Load and emulate binary from the given path """ mod = self.load_module(path) self.run_module(mod) def load_shellcode(self, path, arch, data=None): """ Load position independent code (i.e. shellcode) to prepare for emulation """ sc_hash = None if arch == 'x86': arch = _arch.ARCH_X86 elif arch in ('x64', 'amd64'): arch = _arch.ARCH_AMD64 self.arch = arch if data: sc_hash = hashlib.sha256() sc_hash.update(data) sc_hash = sc_hash.hexdigest() sc = data else: with open(path, 'rb') as scpath: sc = scpath.read() sc_hash = hashlib.sha256() sc_hash.update(sc) sc_hash = sc_hash.hexdigest() if self.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif self.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError('Unsupported architecture: %s' % self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, self.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) sc_tag = 'emu.shellcode.%s' % (sc_hash) # Map the shellcode into memory sc_addr = self.mem_map(len(sc), tag=sc_tag) self.mem_write(sc_addr, sc) self.pic_buffers.append((path, sc_addr, len(sc))) sc_arch = 'unknown' if arch == _arch.ARCH_AMD64: sc_arch = 'x64' elif arch == _arch.ARCH_X86: sc_arch = 'x86' if self.profiler: self.input = { 'path': path, 'sha256': sc_hash, 'size': len(sc), 'arch': sc_arch, 'mem_tag': sc_tag, 'emu_version': self.get_emu_version(), 'os_run': self.get_osver_string() } self.profiler.add_input_metadata(self.input) # Strings the initial buffer so that we can detect decoded strings later on if self.do_strings: self.profiler.strings['ansi'] = self.get_ansi_strings(sc) self.profiler.strings['unicode'] = self.get_unicode_strings(sc) self.setup() return sc_addr def run_shellcode(self, sc_addr, offset=0): """ Begin emulating position independent code (i.e. shellcode) to prepare for emulation """ target = None for sc_path, _sc_addr, size in self.pic_buffers: if _sc_addr == sc_addr: target = _sc_addr break if not target: raise Win32EmuError('Invalid shellcode address') stack_commit = 0x4000 self.stack_base, stack_addr = self.alloc_stack(stack_commit) self.set_func_args(self.stack_base, self.return_hook, 0x7000) run = Run() run.type = 'shellcode' run.start_addr = sc_addr + offset run.instr_cnt = 0 args = [ self.mem_map(1024, tag='emu.shellcode_arg_%d' % (i), base=0x41420000 + i) for i in range(4) ] run.args = (args) self.reg_write(_arch.X86_REG_ECX, 1024) self.add_run(run) # Create an empty process object for the shellcode if none is # supplied container = self.init_container_process() if container: self.processes.append(container) self.curr_process = container else: p = objman.Process(self) self.processes.append(p) self.curr_process = p mm = self.get_address_map(sc_addr) if mm: mm.set_process(self.curr_process) t = objman.Thread(self, stack_base=self.stack_base, stack_commit=stack_commit) self.om.objects.update({t.address: t}) self.curr_process.threads.append(t) self.curr_thread = t peb = self.alloc_peb(self.curr_process) # Set the TEB self.init_teb(t, peb) self.start() def alloc_peb(self, proc): """ Allocate memory for the Process Environment Block (PEB) """ if proc.is_peb_active: return size = proc.get_peb_ldr().sizeof() res, size = self.get_valid_ranges(size) self.mem_reserve(size, base=res, tag='emu.struct.PEB_LDR_DATA') proc.set_peb_ldr_address(res) peb = proc.get_peb() proc.is_peb_active = True peb.object.ImageBaseAddress = proc.base peb.write_back() return peb def set_unhandled_exception_handler(self, handler_addr): """ Establish a handler for unhandled exceptions that occur during emulation """ self.unhandled_exception_filter = handler_addr def setup(self, stack_commit=0): # Set the emulator to run in protected mode self.om = objman.ObjectManager(emu=self) arch = self.get_arch() self._setup_gdt(arch) self.setup_user_shared_data() self.set_ptr_size(self.arch) if arch == _arch.ARCH_X86: self.peb_addr = self.fs_addr + 0x30 elif arch == _arch.ARCH_AMD64: self.peb_addr = self.gs_addr + 0x60 self.api = WindowsApi(self) # Init symlinks for sl in self.symlinks: self.om.add_symlink(sl['name'], sl['target']) self.init_sys_modules(self.config_system_modules) def init_sys_modules(self, modules_config): """ Get the system modules (e.g. drivers) that are loaded in the emulator """ sys_mods = [] for modconf in modules_config: mod = w32common.DecoyModule() mod.name = modconf['name'] base = modconf.get('base_addr') if isinstance(base, str): base = int(base, 16) mod.decoy_base = base mod.decoy_path = modconf['path'] drv = modconf.get('driver') if drv: devs = drv.get('devices') for dev in devs: name = dev.get('name', '') do = self.new_object(objman.Device) do.name = name sys_mods.append(mod) return sys_mods def init_container_process(self): """ Create a process to be used to host shellcode or DLLs """ for p in self.config_processes: if p.get('is_main_exe'): name = p.get('name', '') emu_path = p.get('path', '') base = p.get('base_addr', 0) if isinstance(base, str): base = int(base, 0) cmd_line = p.get('command_line', '') proc = objman.Process(self, name=name, path=emu_path, base=base, cmdline=cmd_line) return proc return None def get_user_modules(self): """ Get the user modules (e.g. dlls) that are loaded in the emulator """ # Generate the decoy user module list if len(self.user_modules) < 2: # Check if we have a host process configured proc_mod = None for p in self.config_processes: if not self.user_modules and p.get('is_main_exe'): proc_mod = p break if proc_mod: all_user_mods = [proc_mod] + self.config_user_modules user_modules = self.init_user_modules(all_user_mods) else: user_modules = self.init_user_modules(self.config_user_modules) self.user_modules += user_modules # add sample to user modules list if it is a dll if self.modules and not self.modules[0][0].is_exe(): self.user_modules.append(self.modules[0][0]) return self.user_modules def exit_process(self): """ An emulated binary is attempted to terminate its current process. Signal that the run has finished. """ self.enable_code_hook() self.run_complete = True def _hook_mem_unmapped(self, emu, access, address, size, value, user_data): _access = self.emu_eng.mem_access.get(access) if _access == common.INVALID_MEM_READ: p = self.get_current_process() pld = p.get_peb_ldr() if address > pld.address and address < (pld.address + pld.sizeof()): self.mem_map_reserve(pld.address) user_mods = self.get_user_modules() self.init_peb(user_mods) return True return super(Win32Emulator, self)._hook_mem_unmapped(emu, access, address, size, value, user_data) def set_hooks(self): """Set the emulator callbacks""" super(Win32Emulator, self).set_hooks() if not self.builtin_hooks_set: self.add_mem_invalid_hook(cb=self._hook_mem_unmapped) self.add_interrupt_hook(cb=self._hook_interrupt) self.builtin_hooks_set = True self.set_mem_tracing_hooks() def stop(self): self.run_complete = True super(Win32Emulator, self).stop() def on_emu_complete(self): """ Called when all runs have completed emulation """ if not self.emu_complete: self.emu_complete = True if self.do_strings and self.profiler: dec_ansi, dec_unicode = self.get_mem_strings() dec_ansi = [ a for a in dec_ansi if a not in self.profiler.strings['ansi'] ] dec_unicode = [ u for u in dec_unicode if u not in self.profiler.strings['unicode'] ] self.profiler.decoded_strings['ansi'] = dec_ansi self.profiler.decoded_strings['unicode'] = dec_unicode self.stop() def on_run_complete(self): """ Clean up after a run completes. This function will pop the next run from the run queue and emulate it. """ self.run_complete = True self.curr_run.ret_val = self.get_return_val() if self.profiler: self.profiler.log_dropped_files(self.curr_run, self.get_dropped_files()) return self._exec_next_run() def heap_alloc(self, size, heap='None'): """ Allocate a memory chunk and add it to the "heap" """ addr = self.mem_map(size, base=None, tag='api.heap.%s' % (heap)) self.heap_allocs.append((addr, size, heap)) return addr
def load_module(self, path=None, data=None): """ Load a module into the emulator space from the specified path """ pe = self.load_pe(path=path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError('Unsupported architecture: %s', pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) if not data: file_name = os.path.basename(path) + '.exe' mod_name = os.path.splitext(file_name)[0] else: mod_hash = hashlib.sha256() mod_hash.update(data) mod_hash = mod_hash.hexdigest() mod_name = mod_hash file_name = '%s.exe' % (mod_name) self.api = WindowsApi(self) cd = self.get_cd() if not cd.endswith('\\'): cd += '\\' emu_path = cd + file_name if not data: with open(path, 'rb') as f: data = f.read() self.fileman.add_existing_file(emu_path, data) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: self.profiler.strings['ansi'] = self.get_ansi_strings(data) self.profiler.strings['unicode'] = self.get_unicode_strings(data) # Set the emulated path emu_path = '' self.cd = self.get_cd() if self.cd: if not self.cd.endswith('\\'): self.cd += '\\' emu_path = self.cd + os.path.basename(file_name) pe.set_emu_path(emu_path) self.map_pe(pe, mod_name=mod_name, emu_path=emu_path) self.mem_write(pe.base, pe.mapped_image) self.setup() if not self.stack_base: self.stack_base, stack_addr = self.alloc_stack(0x12000) self.set_func_args(self.stack_base, self.return_hook) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write( addr, data_ptr.to_bytes(self.get_ptr_size(), 'little')) return pe
class Win32Emulator(WindowsEmulator): """ User Mode Windows Emulator Class """ def __init__(self, config, argv=[], debug=False, logger=None, exit_event=None): super(Win32Emulator, self).__init__( config, debug=debug, logger=logger, exit_event=exit_event ) self.last_error = 0 self.peb_addr = 0 self.heap_allocs = [] self.argv = argv self.sessman = SessionManager(config) self.com = COM(config) def get_argv(self): """ Get command line arguments (if any) that are being passed to the emulated process. (e.g. main(argv)) """ argv0 = "" out = [] if len(self.argv): for m in self.modules: pe = m[0] emu_path = m[2] if pe.is_exe(): argv0 = emu_path out = [argv0] + self.argv elif self.command_line: out = shlex.split(self.command_line, posix=False) return out def set_last_error(self, code): """ Set the last error code for the current thread """ if self.curr_thread: self.curr_thread.set_last_error(code) def get_last_error(self): """ Get the last error code for the current thread """ if self.curr_thread: return self.curr_thread.get_last_error() def get_session_manager(self): """ Get the session manager for the emulator. This will manage things like desktops, windows, and session isolation """ return self.sessman def add_vectored_exception_handler(self, first, handler): """ Add a vectored exception handler that will be executed on an exception """ self.veh_handlers.append(handler) def remove_vectored_exception_handler(self, handler): """ Remove a vectored exception handler """ self.veh_handlers.remove(handler) def get_processes(self): if len(self.processes) <= 1: self.init_processes(self.config_processes) return self.processes def init_processes(self, processes): """ Initialize configured processes set in the emulator config """ for proc in processes: p = objman.Process(self) self.add_object(p) p.name = proc.get("name", "") new_pid = proc.get("pid") if new_pid: p.pid = new_pid base = proc.get("base_addr") if isinstance(base, str): base = int(base, 16) p.base = base p.path = proc.get("path") p.session = proc.get("session", 0) p.image = ntpath.basename(p.path) self.processes.append(p) def load_module(self, path=None, data=None, first_time_setup=True): """ Load a module into the emulator space from the specified path """ self._init_name(path, data) pe = self.load_pe(path=path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError("Unsupported architecture: %s", pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) # No need to initialize the engine and Capstone again if first_time_setup: self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) self.api = WindowsApi(self) cd = self.get_cd() if not cd.endswith("\\"): cd += "\\" emu_path = cd + self.file_name if not data: with open(path, "rb") as f: data = f.read() self.fileman.add_existing_file(emu_path, data) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: self.profiler.strings["ansi"] = [a[1] for a in self.get_ansi_strings(data)] self.profiler.strings["unicode"] = [ u[1] for u in self.get_unicode_strings(data) ] # Set the emulated path emu_path = "" self.cd = self.get_cd() if self.cd: if not self.cd.endswith('\\'): self.cd += '\\' emu_path = self.cd + os.path.basename(self.file_name) pe.set_emu_path(emu_path) # There's a bit of a problem here, if we cannot reserve memory # at the PE's desired base address, and the relocation table # is not present, we can't rebase it. So this is gonna have to # be a bit of a hack for binaries without a relocation table. # This logic is really only for child processes, since we're pretty # much guarenteed memory at the base address of the main module. # 1. If the memory at the child's desired load address is already # being used, remap it somewhere else. I'm pretty sure that # the already-used memory will always be for a module, # since desired load addresses don't really vary across PEs # 2. Fix up any modules that speakeasy has open for the parent # to reflect where it was remapped # 3. Try and grab memory at the child's desired base address, # if that isn't still isn't possible, we're out of luck # # But if the relocation table is present, we can rebase it, # so we do that instead of the above hack. imgbase = pe.OPTIONAL_HEADER.ImageBase ranges = self.get_valid_ranges(pe.image_size, addr=imgbase) base, size = ranges if base != imgbase: if pe.has_reloc_table(): pe.rebase(base) else: parent_map = self.get_address_map(imgbase) # Already being used by the parent, so let's remap the parent # Do get_valid_ranges on the parent map size so we get a # suitable region for it new_parent_mem, unused = self.get_valid_ranges(parent_map.size) new_parent_mem = self.mem_remap(imgbase, new_parent_mem) # Failed if new_parent_mem == -1: # XXX what to do here pass # Update parent module pointer for pe_, ranges_, emu_path_ in self.modules: base_, size_ = ranges_ if base_ == imgbase: self.modules.remove((pe_, ranges_, emu_path_)) self.modules.append((pe_, (new_parent_mem, size_), emu_path_)) break # Alright, let's try to grab that memory for the child again ranges = self.get_valid_ranges(pe.image_size, addr=imgbase) base, size = ranges if base != imgbase: # Out of luck # XXX what to do here pass self.mem_map(pe.image_size, base=base, tag='emu.module.%s' % (self.mod_name)) self.modules.append((pe, ranges, emu_path)) self.mem_write(pe.base, pe.mapped_image) self.setup(first_time_setup=first_time_setup) if not self.stack_base: self.stack_base, stack_addr = self.alloc_stack(0x12000) self.set_func_args(self.stack_base, self.return_hook) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write(addr, data_ptr.to_bytes(self.get_ptr_size(), "little")) return pe def prepare_module_for_emulation(self, module, all_entrypoints, entrypoints): if not module: self.stop() raise Win32EmuError("Module not found") # Check if any TLS callbacks exist, these run before the module's entry point tls = module.get_tls_callbacks() for i, cb_addr in enumerate(tls): base = module.get_base() if base < cb_addr < base + module.get_image_size(): run = Run() run.start_addr = cb_addr run.type = "tls_callback_%d" % (i) run.args = [base, DLL_PROCESS_ATTACH, 0] self.add_run(run) ep = module.base + module.ep run = Run() run.start_addr = ep main_exe = None if not module.is_exe(): run.args = [module.base, DLL_PROCESS_ATTACH, 0] run.type = "dll_entry.DLL_PROCESS_ATTACH" container = self.init_container_process() if container: self.processes.append(container) self.curr_process = container else: run.type = "module_entry" main_exe = module run.args = [ self.mem_map(8, tag="emu.module_arg_%d" % (i)) for i in range(4) ] if main_exe: self.user_modules = [main_exe] + self.user_modules # we consider this run only if all entry_points is selected or DLL_PROCESS_ATTACH is in the entrypoints if all_entrypoints or "DLL_PROCESS_ATTACH" in entrypoints: self.add_run(run) if all_entrypoints or entrypoints: # Only emulate a subset of all the exported functions # There are some modules (such as the windows kernel) with # thousands of exports exports = [k for k in module.get_exports()[:MAX_EXPORTS_TO_EMULATE]] if exports: args = [ self.mem_map(8, tag="emu.export_arg_%d" % (i), base=0x41420000) for i in range(4) ] # noqa for exp in exports: if exp.name in ("DllMain",): continue if all_entrypoints or exp.name in entrypoints: run = Run() if exp.name: fn = exp.name else: fn = "no_name" run.type = "export.%s" % (fn) run.start_addr = exp.address if exp.name == "ServiceMain": # ServiceMain accepts a (argc, argv) pair like main(). # # now, we're not exactly sure if we're in A or W mode. # maybe there are some hints we could take to guess this. # instead, we'll assume W mode and use default service name "IPRIP". # # hack: if we're actually in A mode, then string routines # will think the service name is "I" which isn't perfect, # but might still be good enough. # # layout: # argc: 1 # argv: # 0x00: (argv[0]) pointer to +0x10 -+ # 0x04/08: (argv[1]) 0x0 | # 0x10: "IPRIP" <------------------+ svc_name = "IPRIP\x00".encode("utf-16le") argc = 1 argv = self.mem_map( len(svc_name) + 0x10, tag="emu.export_ServiceMain_argv", base=0x41420000, ) self.write_ptr(argv, argv + 0x10) self.mem_write(argv + 0x10, svc_name) run.args = [argc, argv] else: # Here we set dummy args to pass into the export function run.args = args # Store these runs and only queue them before the unload # routine this is because some exports may not be ready to # be called yet self.add_run(run) return def run_module(self, module, all_entrypoints=False, emulate_children=False, entrypoints=None): """ Begin emulating a previously loaded module Arguments: module: Module to emulate """ if entrypoints is None: entrypoints = [] self.prepare_module_for_emulation(module, all_entrypoints, entrypoints) # Create an empty process object for the module if none is # supplied, only do this for the main module if len(self.processes) == 0: p = objman.Process( self, path=module.get_emu_path(), base=module.base, pe=module, cmdline=self.command_line, ) self.curr_process = p self.om.objects.update({p.address: p}) mm = self.get_address_map(module.base) if mm: mm.process = self.curr_process t = objman.Thread( self, stack_base=self.stack_base, stack_commit=module.stack_commit ) self.om.objects.update({t.address: t}) self.curr_process.threads.append(t) self.curr_thread = t peb = self.alloc_peb(self.curr_process) # Set the TEB self.init_teb(t, peb) # Begin emulation of main module self.start() if not emulate_children or len(self.child_processes) == 0: return # Emulate any child processes while len(self.child_processes) > 0: child = self.child_processes.pop(0) child.pe = self.load_module(data=child.pe_data, first_time_setup=False) self.prepare_module_for_emulation(child.pe, all_entrypoints, entrypoints) self.command_line = child.cmdline self.curr_process = child self.curr_process.base = child.pe.base self.curr_thread = child.threads[0] self.om.objects.update({self.curr_thread.address: self.curr_thread}) # PEB and TEB will be initialized when the next run happens self.start() return def _init_name(self, path, data=None): if not data: self.file_name = os.path.basename(path) self.mod_name = os.path.splitext(self.file_name)[0] else: mod_hash = hashlib.sha256() mod_hash.update(data) mod_hash = mod_hash.hexdigest() self.mod_name = mod_hash self.file_name = f"{self.mod_name}.exe" self.bin_base_name = os.path.basename(self.file_name) def emulate_module(self, path): """ Load and emulate binary from the given path """ mod = self.load_module(path) self.run_module(mod) def load_shellcode(self, path, arch, data=None): """ Load position independent code (i.e. shellcode) to prepare for emulation """ sc_hash = None self._init_name(path, data) if arch == "x86": arch = _arch.ARCH_X86 elif arch in ("x64", "amd64"): arch = _arch.ARCH_AMD64 self.arch = arch if data: sc_hash = hashlib.sha256() sc_hash.update(data) sc_hash = sc_hash.hexdigest() sc = data else: with open(path, "rb") as scpath: sc = scpath.read() sc_hash = hashlib.sha256() sc_hash.update(sc) sc_hash = sc_hash.hexdigest() if self.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif self.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError("Unsupported architecture: %s" % self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, self.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) sc_tag = "emu.shellcode.%s" % (sc_hash) # Map the shellcode into memory sc_addr = self.mem_map(len(sc), tag=sc_tag) self.mem_write(sc_addr, sc) self.pic_buffers.append((path, sc_addr, len(sc))) sc_arch = "unknown" if arch == _arch.ARCH_AMD64: sc_arch = "x64" elif arch == _arch.ARCH_X86: sc_arch = "x86" if self.profiler: self.input = { "path": path, "sha256": sc_hash, "size": len(sc), "arch": sc_arch, "mem_tag": sc_tag, "emu_version": self.get_emu_version(), "os_run": self.get_osver_string(), } self.profiler.add_input_metadata(self.input) # Strings the initial buffer so that we can detect decoded strings later on if self.do_strings: self.profiler.strings["ansi"] = [ a[1] for a in self.get_ansi_strings(sc) ] self.profiler.strings["unicode"] = [ u[1] for u in self.get_unicode_strings(sc) ] self.setup() return sc_addr def run_shellcode(self, sc_addr, offset=0): """ Begin emulating position independent code (i.e. shellcode) to prepare for emulation """ target = None for sc_path, _sc_addr, size in self.pic_buffers: if _sc_addr == sc_addr: target = _sc_addr break if not target: raise Win32EmuError("Invalid shellcode address") stack_commit = 0x4000 self.stack_base, stack_addr = self.alloc_stack(stack_commit) self.set_func_args(self.stack_base, self.return_hook, 0x7000) run = Run() run.type = "shellcode" run.start_addr = sc_addr + offset run.instr_cnt = 0 args = [ self.mem_map(1024, tag="emu.shellcode_arg_%d" % (i), base=0x41420000 + i) for i in range(4) ] run.args = args self.reg_write(_arch.X86_REG_ECX, 1024) self.add_run(run) # Create an empty process object for the shellcode if none is # supplied container = self.init_container_process() if container: self.processes.append(container) self.curr_process = container else: p = objman.Process(self) self.processes.append(p) self.curr_process = p mm = self.get_address_map(sc_addr) if mm: mm.set_process(self.curr_process) t = objman.Thread(self, stack_base=self.stack_base, stack_commit=stack_commit) self.om.objects.update({t.address: t}) self.curr_process.threads.append(t) self.curr_thread = t peb = self.alloc_peb(self.curr_process) # Set the TEB self.init_teb(t, peb) self.start() def alloc_peb(self, proc): """ Allocate memory for the Process Environment Block (PEB) """ if proc.is_peb_active: return size = proc.get_peb_ldr().sizeof() res, size = self.get_valid_ranges(size) self.mem_reserve(size, base=res, tag="emu.struct.PEB_LDR_DATA") proc.set_peb_ldr_address(res) peb = proc.get_peb() proc.is_peb_active = True peb.object.ImageBaseAddress = proc.base peb.object.OSMajorVersion = self.osversion["major"] peb.object.OSMinorVersion = self.osversion["minor"] peb.object.OSBuildNumber = self.osversion["build"] peb.write_back() return peb def set_unhandled_exception_handler(self, handler_addr): """ Establish a handler for unhandled exceptions that occur during emulation """ self.unhandled_exception_filter = handler_addr def setup(self, stack_commit=0, first_time_setup=True): if first_time_setup: # Set the emulator to run in protected mode self.om = objman.ObjectManager(emu=self) arch = self.get_arch() self._setup_gdt(arch) self.setup_user_shared_data() self.set_ptr_size(self.arch) if arch == _arch.ARCH_X86: self.peb_addr = self.fs_addr + 0x30 elif arch == _arch.ARCH_AMD64: self.peb_addr = self.gs_addr + 0x60 self.api = WindowsApi(self) # Init symlinks for sl in self.symlinks: self.om.add_symlink(sl["name"], sl["target"]) self.init_sys_modules(self.config_system_modules) def init_sys_modules(self, modules_config): """ Get the system modules (e.g. drivers) that are loaded in the emulator """ sys_mods = [] for modconf in modules_config: mod = w32common.DecoyModule() mod.name = modconf["name"] base = modconf.get("base_addr") if isinstance(base, str): base = int(base, 16) mod.decoy_base = base mod.decoy_path = modconf["path"] drv = modconf.get("driver") if drv: devs = drv.get("devices") for dev in devs: name = dev.get("name", "") do = self.new_object(objman.Device) do.name = name sys_mods.append(mod) return sys_mods def init_container_process(self): """ Create a process to be used to host shellcode or DLLs """ for p in self.config_processes: if p.get("is_main_exe"): name = p.get("name", "") emu_path = p.get("path", "") base = p.get("base_addr", 0) if isinstance(base, str): base = int(base, 0) cmd_line = p.get("command_line", "") proc = objman.Process( self, name=name, path=emu_path, base=base, cmdline=cmd_line ) return proc return None def get_user_modules(self): """ Get the user modules (e.g. dlls) that are loaded in the emulator """ # Generate the decoy user module list if len(self.user_modules) < 2: # Check if we have a host process configured proc_mod = None for p in self.config_processes: if not self.user_modules and p.get("is_main_exe"): proc_mod = p break if proc_mod: all_user_mods = [proc_mod] + self.config_user_modules user_modules = self.init_user_modules(all_user_mods) else: user_modules = self.init_user_modules(self.config_user_modules) self.user_modules += user_modules # add sample to user modules list if it is a dll if self.modules and not self.modules[0][0].is_exe(): self.user_modules.append(self.modules[0][0]) return self.user_modules def exit_process(self): """ An emulated binary is attempted to terminate its current process. Signal that the run has finished. """ self.enable_code_hook() self.run_complete = True def _hook_mem_unmapped(self, emu, access, address, size, value, user_data): _access = self.emu_eng.mem_access.get(access) if _access == common.INVALID_MEM_READ: p = self.get_current_process() pld = p.get_peb_ldr() if address > pld.address and address < (pld.address + pld.sizeof()): self.mem_map_reserve(pld.address) user_mods = self.get_user_modules() self.init_peb(user_mods) return True return super(Win32Emulator, self)._hook_mem_unmapped( emu, access, address, size, value, user_data ) def set_hooks(self): """Set the emulator callbacks""" super(Win32Emulator, self).set_hooks() if not self.builtin_hooks_set: self.add_mem_invalid_hook(cb=self._hook_mem_unmapped) self.add_interrupt_hook(cb=self._hook_interrupt) self.builtin_hooks_set = True self.set_mem_tracing_hooks() def stop(self): self.run_complete = True # self._unset_emu_hooks() # self.unset_hooks() super(Win32Emulator, self).stop() def on_emu_complete(self): """ Called when all runs have completed emulation """ if not self.emu_complete: self.emu_complete = True if self.do_strings and self.profiler: dec_ansi, dec_unicode = self.get_mem_strings() dec_ansi = [ a[1] for a in dec_ansi if a not in self.profiler.strings["ansi"] ] dec_unicode = [ u[1] for u in dec_unicode if u not in self.profiler.strings["unicode"] ] self.profiler.decoded_strings["ansi"] = dec_ansi self.profiler.decoded_strings["unicode"] = dec_unicode self.stop() def on_run_complete(self): """ Clean up after a run completes. This function will pop the next run from the run queue and emulate it. """ self.run_complete = True self.curr_run.ret_val = self.get_return_val() if self.profiler: self.profiler.log_dropped_files(self.curr_run, self.get_dropped_files()) return self._exec_next_run() def heap_alloc(self, size, heap="None"): """ Allocate a memory chunk and add it to the "heap" """ addr = self.mem_map(size, base=None, tag="api.heap.%s" % (heap)) self.heap_allocs.append((addr, size, heap)) return addr
def load_module(self, path=None, data=None, first_time_setup=True): """ Load a module into the emulator space from the specified path """ self._init_name(path, data) pe = self.load_pe(path=path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise Win32EmuError("Unsupported architecture: %s", pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) # No need to initialize the engine and Capstone again if first_time_setup: self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) self.api = WindowsApi(self) cd = self.get_cd() if not cd.endswith("\\"): cd += "\\" emu_path = cd + self.file_name if not data: with open(path, "rb") as f: data = f.read() self.fileman.add_existing_file(emu_path, data) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: self.profiler.strings["ansi"] = [a[1] for a in self.get_ansi_strings(data)] self.profiler.strings["unicode"] = [ u[1] for u in self.get_unicode_strings(data) ] # Set the emulated path emu_path = "" self.cd = self.get_cd() if self.cd: if not self.cd.endswith('\\'): self.cd += '\\' emu_path = self.cd + os.path.basename(self.file_name) pe.set_emu_path(emu_path) # There's a bit of a problem here, if we cannot reserve memory # at the PE's desired base address, and the relocation table # is not present, we can't rebase it. So this is gonna have to # be a bit of a hack for binaries without a relocation table. # This logic is really only for child processes, since we're pretty # much guarenteed memory at the base address of the main module. # 1. If the memory at the child's desired load address is already # being used, remap it somewhere else. I'm pretty sure that # the already-used memory will always be for a module, # since desired load addresses don't really vary across PEs # 2. Fix up any modules that speakeasy has open for the parent # to reflect where it was remapped # 3. Try and grab memory at the child's desired base address, # if that isn't still isn't possible, we're out of luck # # But if the relocation table is present, we can rebase it, # so we do that instead of the above hack. imgbase = pe.OPTIONAL_HEADER.ImageBase ranges = self.get_valid_ranges(pe.image_size, addr=imgbase) base, size = ranges if base != imgbase: if pe.has_reloc_table(): pe.rebase(base) else: parent_map = self.get_address_map(imgbase) # Already being used by the parent, so let's remap the parent # Do get_valid_ranges on the parent map size so we get a # suitable region for it new_parent_mem, unused = self.get_valid_ranges(parent_map.size) new_parent_mem = self.mem_remap(imgbase, new_parent_mem) # Failed if new_parent_mem == -1: # XXX what to do here pass # Update parent module pointer for pe_, ranges_, emu_path_ in self.modules: base_, size_ = ranges_ if base_ == imgbase: self.modules.remove((pe_, ranges_, emu_path_)) self.modules.append((pe_, (new_parent_mem, size_), emu_path_)) break # Alright, let's try to grab that memory for the child again ranges = self.get_valid_ranges(pe.image_size, addr=imgbase) base, size = ranges if base != imgbase: # Out of luck # XXX what to do here pass self.mem_map(pe.image_size, base=base, tag='emu.module.%s' % (self.mod_name)) self.modules.append((pe, ranges, emu_path)) self.mem_write(pe.base, pe.mapped_image) self.setup(first_time_setup=first_time_setup) if not self.stack_base: self.stack_base, stack_addr = self.alloc_stack(0x12000) self.set_func_args(self.stack_base, self.return_hook) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write(addr, data_ptr.to_bytes(self.get_ptr_size(), "little")) return pe
class WinKernelEmulator(WindowsEmulator, IoManager): """ Class used to emulate Windows drivers """ def __init__(self, config, debug=False, logger=None, exit_event=None): super(WinKernelEmulator, self).__init__(config, debug=debug, logger=logger, exit_event=exit_event) self.disasm_eng = None self.curr_mod = None self.debug = debug self.drivers = [] self.pool_allocs = [] self.all_entrypoints = False self.kernel_mode = True self.irql = ddk.PASSIVE_LEVEL self.delayed_runs = [] self.system_time = SYSTEM_TIME_START self.ktypes = ntos def get_system_time(self): return self.system_time def get_system_process(self): """ Get the process object for the system process (PID 4) """ for proc in self.processes: if proc.get_pid() == 4: return proc def get_current_irql(self): """ Get the current interrupt request level """ return self.irql def set_current_irql(self, irql): """ Set the current interrupt request level """ self.irql = irql def create_driver_object(self, name=None, pe=None): """ Create a driver object for the driver that is going to be emulated """ drv = objman.Driver(emu=self) # If no PE was supplied, assign a dummy driver if not pe: # Get the path for the dummy driver default_path = self.get_native_module_path('default_sys') pe = w32common.DecoyModule(path=default_path) if name: bn = ntpath.basename(name) else: bn = 'none' pe.decoy_path = ('%sdrivers\\%s.sys' % (self.get_system_root(), os.path.basename(bn))) pe.decoy_base = pe.get_base() else: if not name: bn = pe.path path = '%sdrivers\\%s' % (self.get_system_root(), os.path.basename(bn)) pe.decoy_path = path pe.decoy_base = pe.base drv.init_driver_object(name, pe, is_decoy=False) self.add_object(drv) self.drivers.append(drv) return drv def load_module(self, path=None, data=None): """ Load the kernel module to be emulated """ pe = self.load_pe(path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise KernelEmuError('Unsupported architecture: %s', pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) self.api = WindowsApi(self) self.om = objman.ObjectManager(emu=self) if not data: file_name = os.path.basename(path) mod_name = os.path.splitext(file_name)[0] else: drv_hash = hashlib.sha256() drv_hash.update(data) drv_hash = drv_hash.hexdigest() mod_name = drv_hash file_name = '%s.sys' % (mod_name) emu_path = '%sdrivers\\%s' % (self.get_system_root(), file_name) pe.emu_path = emu_path self.map_pe(pe, mod_name=mod_name, emu_path=emu_path) self.mem_write(pe.base, pe.mapped_image) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: astrs = self.get_ansi_strings(pe.mapped_image) wstrs = self.get_unicode_strings(pe.mapped_image) for s in astrs: if s not in self.profiler.strings['ansi']: self.profiler.strings['ansi'].append(s) for s in wstrs: if s not in self.profiler.strings['unicode']: self.profiler.strings['unicode'].append(s) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write( addr, data_ptr.to_bytes(self.get_ptr_size(), 'little')) # Set the emulator to run in protected mode self._setup_gdt(self.get_arch()) self.setup_kernel_mode() self.setup_user_shared_data() if not self.stack_base: self.stack_base, stack_ptr = self.alloc_stack(pe.stack_commit) return pe def pool_alloc(self, pooltype, size, tag='None'): """ Allocate memory in the emulated "pool" """ if pooltype == ddk.POOL_TYPE.NonPagedPool: pt = 'NonPagedPool' elif pooltype == ddk.POOL_TYPE.PagedPool: pt = 'PagedPool' elif pooltype == ddk.POOL_TYPE.NonPagedPoolNx: pt = 'NonPagedPoolNx' else: pt = 'unk' system_proc = self.get_system_process() addr = self.mem_map(size, base=None, tag='api.pool.%s.%s' % (pt, tag), process=system_proc) self.pool_allocs.append((addr, pooltype, size, tag)) return addr def init_sys_modules(self, modules_config): """ Initialize kernel mode modules that may be accessed by emulated modules """ sysmods = super(WinKernelEmulator, self).init_sys_modules(modules_config) # Initalize any DRIVER_OBJECTs needed by the module for mc in modules_config: drv = mc.get('driver') if drv: mod = [m for m in sysmods if m.name == mc.get('name')] if not mod: continue mod = mod[0] driver = self.create_driver_object(name=drv.get('name'), pe=mod) devs = drv.get('devices') for dev in devs: name = dev.get('name', '') ext_size = dev.get('ext_size', 0) devtype = dev.get('devtype', 0) chars = dev.get('chars', 0) self.create_device_object(name, driver, ext_size, devtype, chars) for m in self.modules: mod = m[0] sysmods.append(mod) return sysmods def get_processes(self): """ Get processes that exist in the emulation space """ if not self.processes: self.init_processes(self.config_processes) return self.processes def init_processes(self, processes): for proc in processes: p = objman.Process(self) self.add_object(p) p.name = proc.get('name', '') p.pid = proc.get('pid') if p.name.lower() == 'system': p.pid = 4 p.path = 'System' if not p.pid: p.pid = self.om.new_id() base = proc.get('base_addr') if isinstance(base, str): base = int(base, 16) p.base = base if not p.path: p.path = proc.get('path') p.image = ntpath.basename(p.path) # Create an initial thread for each process t = objman.Thread(self) self.add_object(t) t.process = p p.threads.append(t) self.processes.append(p) # The SYSTEM process should be the starting context sp = [p for p in self.processes if p.name.lower() == 'system'] if sp: sp = sp[0] self.set_current_process(sp) def alloc_peb(self, proc): """ Allocate PEB and related substructures for a given process """ ldr = proc.get_peb_ldr() if not ldr.address: size = ldr.sizeof() res, size = self.get_valid_ranges(size) base = self.mem_map(size, base=res, tag='emu.struct.PEB_LDR_DATA') proc.set_peb_ldr_address(base) return proc.get_peb() def get_process_peb(self, process): """ Retrieve the PEB for the supplied process """ self.alloc_peb(process) user_mods = self.get_user_modules() process.init_peb(user_mods) return process.peb def set_current_process(self, process): """ Set the current process context """ self.curr_process = process def get_current_process(self): """ Get the current process context """ if not self.processes: self.processes = self.get_processes() return self.curr_process def run_module(self, module, all_entrypoints=False): """ Begin emulation fo a previously loaded kernel module """ self.all_entrypoints = all_entrypoints # Create the service key for the driver drv = self.create_driver_object(pe=module) svc_key = self.regman.create_key(drv.get_reg_path()) # Create the values for the service key svc_key.create_value('ImagePath', regdefs.REG_EXPAND_SZ, module.get_emu_path()) svc_key.create_value('Type', regdefs.REG_DWORD, 0x1) # SERVICE_KERNEL_DRIVER svc_key.create_value('Start', regdefs.REG_DWORD, 0x3) # SERVICE_DEMAND_START svc_key.create_value('ErrorControl', regdefs.REG_DWORD, 0x1) # SERVICE_ERROR_NORMAL # Create the parameters subkey self.regman.create_key(drv.get_reg_path() + '\\Parameters') if module.ep > 0: ep = module.base + module.ep run = Run() run.type = EP_DRIVER_ENTRY run.start_addr = ep run.instr_cnt = 0 run.args = [drv.address, drv.reg_path_ptr] self.add_run(run) if self.all_entrypoints: # Only emulate a subset of all the exported functions # There are some modules (such as the windows kernel) with thousands of exports exports = [ k for k in module.get_exports()[:MAX_EXPORTS_TO_EMULATE] ] if exports: args = [ self.mem_map(8, tag='emu.export_arg_%d' % (i)) for i in range(4) ] for exp in exports: run = Run() if exp.name: fn = exp.name else: fn = 'no_name' run.type = 'export.%s' % (fn) run.start_addr = exp.address # Here we set dummy args to pass into the export function run.args = args # Store these runs and only queue them before the unload routine # this is because some exports may not be ready to be called yet self.delayed_runs.append(run) self.start() def create_device_object(self, name='', drv=0, ext_size=0, devtype=0, chars=0, tag=''): """ Create a device object to use for kernel emulation """ dev = objman.Device(self) alloc_size = ext_size + dev.sizeof() if not name: devname = r'\Device\%x' % (dev.get_id()) if not tag: tag = 'emu.device.autogen' name = '%s.%s' % (tag, devname) else: devname = name if not tag: tag = 'emu.object' name = '%s.%s' % (tag, devname) dev.address = self.mem_map(alloc_size, tag=name) dev.name = devname # Create a FILE_OBJECT for the device fobj = objman.FileObject(self) dev.object.DeviceObject = dev.address dev.file_object = fobj self.add_object(dev) if drv: drv.read_back() dev.object.DriverObject = drv.address dev.driver = drv if not drv.object.DeviceObject: drv.object.DeviceObject = dev.address drv.write_back() else: next_dev = self.get_object_from_addr(drv.object.DeviceObject) while next_dev: if next_dev.object.NextDevice: next_dev = \ self.get_object_from_addr(drv.object.NextDevice) else: # This is the last in the list, add our new device next_dev.object.NextDevice = dev.address next_dev.write_back() break drv.devices.append(dev) dev.object.Characteristics = chars dev.object.DeviceType = devtype if ext_size > 0: dev.object.DeviceExtension = dev.address + dev.sizeof() dev.write_back() return dev def add_symlink(self, symlink, devname): """ Add a symlink for a device """ self.om.add_symlink(symlink, devname) def _call_driver_dispatch(self, func, dev_addr, irp_addr): """ Call a WDM driver dispatch function with a supplied IRP """ stk_ptr = self.get_stack_ptr() self.set_func_args(stk_ptr, self.return_hook, dev_addr, irp_addr) self.set_pc(func) def new_irp(self): """ Create a new IRP """ return objman.Irp(emu=self) def irp_mj_create(self, func, dev): # Generate an IRP for the create request irp = self.new_irp() self._call_driver_dispatch(func, dev.address, irp.address) return irp def irp_mj_close(self, func, dev): irp = self.new_irp() self._call_driver_dispatch(func, dev.address, irp.address) return irp def irp_mj_dev_io(self, func, dev): irp = self.new_irp() ios = irp.get_curr_stack_loc() ios.object.MajorFunction = ddk.IRP_MJ_DEVICE_CONTROL ios.object.Parameters.DeviceIoControl.IoControlCode = 0x5d5d5d5d ios.write_back() irp.write_back() self._call_driver_dispatch(func, dev.address, irp.address) return irp def irp_mj_read(self, func, dev): irp = self.new_irp() self._call_driver_dispatch(func, dev.address, irp.address) return irp def irp_mj_write(self, func, dev): irp = self.new_irp() self._call_driver_dispatch(func, dev.address, irp.address) return irp def irp_mj_cleanup(self, func, dev): irp = self.new_irp() self._call_driver_dispatch(func, dev.address, irp.address) return irp def driver_unload(self, drv): """ Call the unload routine for a driver """ if not drv.on_unload or drv.unload_called: self.on_emu_complete() return stk_ptr = self.get_stack_ptr() self.set_func_args(stk_ptr, self.exit_hook, drv.address) self.set_pc(drv.on_unload) run = Run() run.type = EP_DRIVER_UNLOAD run.start_addr = drv.on_unload run.instr_cnt = 0 run.args = (drv.address, ) run.ret_val = None self.add_run(run) drv.unload_called = True def next_driver_func(self, drv): func_addr = None func_handler = None if self.curr_run.type is not None: self.curr_run.ret_val = self.get_return_val() # Check if theres anything in the run queue if len(self.run_queue): return if not self.all_entrypoints: return # TODO right now just use the first device object that was created dev = None if len(drv.devices): dev = drv.devices[0] # Run any remaining IRP handlers for hdlr, i in ((self.irp_mj_create, ddk.IRP_MJ_CREATE), (self.irp_mj_dev_io, ddk.IRP_MJ_DEVICE_CONTROL), (self.irp_mj_read, ddk.IRP_MJ_READ), (self.irp_mj_write, ddk.IRP_MJ_WRITE), (self.irp_mj_close, ddk.IRP_MJ_CLOSE), (self.irp_mj_cleanup, ddk.IRP_MJ_CLEANUP)): # Did we run this mj func yet? if i not in [r.type for r in self.runs]: func_handler = hdlr func_addr = int(drv.mj_funcs[i]) if not func_addr: continue break if len(self.delayed_runs): [self.add_run(r) for r in self.delayed_runs] self.delayed_runs = [] if not func_addr or not dev: # We are done here, call the unload routine self.driver_unload(drv) return irp = func_handler(func_addr, dev) run = Run() run.type = i run.start_addr = func_addr run.instr_cnt = 0 run.args = (dev.address, irp.address) self.add_run(run) def on_run_complete(self): self.curr_run.ret_val = self.get_return_val() for drv in self.drivers: drv.read_back() if drv.pe == self.curr_mod: self.next_driver_func(drv) # Dispatch the next run return self._exec_next_run() def on_emu_complete(self): if not self.emu_complete: self.emu_complete = True if self.do_strings and self.profiler: dec_ansi, dec_unicode = self.get_mem_strings() dec_ansi = [ a for a in dec_ansi if a not in self.profiler.strings['ansi'] ] dec_unicode = [ u for u in dec_unicode if u not in self.profiler.strings['unicode'] ] self.profiler.decoded_strings['ansi'] = dec_ansi self.profiler.decoded_strings['unicode'] = dec_unicode self.stop() def set_hooks(self): """Set the emulator callbacks""" super(WinKernelEmulator, self).set_hooks() if not self.builtin_hooks_set: self.add_mem_invalid_hook(cb=self._hook_mem_unmapped) self.add_interrupt_hook(cb=self._hook_interrupt) self.builtin_hooks_set = True self.set_mem_tracing_hooks() def get_kernel_base(self): """ Get the base address of the kernel image (ntoskrnl.exe) """ # Get kernel base address kern = self.get_kernel_mod() return kern.get_base() def get_kernel_mod(self): """ Get the kernel image module """ sys_mods = self.get_sys_modules() for mod in sys_mods: if mod.name.lower() == 'ntoskrnl': return mod raise KernelEmuError('Failed to get kernel base') def _set_entry_point_names(self): run_types = { ddk.IRP_MJ_CREATE: 'irp_mj_create', ddk.IRP_MJ_DEVICE_CONTROL: 'irp_mj_device_control', ddk.IRP_MJ_READ: 'irp_mj_read', ddk.IRP_MJ_WRITE: 'irp_mj_write', ddk.IRP_MJ_CLOSE: 'irp_mj_close', ddk.IRP_MJ_CLEANUP: 'irp_mj_cleanup', EP_DRIVER_ENTRY: 'entry_point', EP_DRIVER_UNLOAD: 'driver_unload' } for r in self.runs: if not r.type or run_types.get(r.type): r.type = run_types.get(r.type, 'unk') def get_report(self): """ Retrieve the execution profile for the emulator """ self._set_entry_point_names() return super(WinKernelEmulator, self).get_report() def get_json_report(self): self._set_entry_point_names() return super(WinKernelEmulator, self).get_json_report() def get_ssdt_ptr(self): return self.ssdt_ptr def setup_kernel_mode(self): idt = objman.IDT(self) idt.init_descriptors() # selector, base, limit, flags self.reg_write(_arch.X86_REG_IDTR, (0, idt.object.Descriptors, idt.object.Limit, 0)) # Setup the SSDT ssdt = self.ktypes.SSDT(self.get_ptr_size()) size = self.get_ptr_size() * 256 self.ssdt_ptr = self.mem_map(size, base=None, tag='api.struct.SSDT') ssdt.NumberOfServices = 256 ssdt.pServiceTable = self.ssdt_ptr + self.sizeof(ssdt) self.mem_write(self.ssdt_ptr, self.get_bytes(ssdt)) self.get_sys_modules() self.setup_msrs() for sl in self.symlinks: self.om.add_symlink(sl['name'], sl['target']) def setup_msrs(self): """ Setup machine specific registers for kernel emulation """ # Initalize the LSTAR on amd64 with the address of KiSystemCall64 km = self.get_kernel_mod() if self.get_arch() == _arch.ARCH_AMD64: ksc64_off = km.find_bytes(b'\x00' * 100, 0) if ksc64_off != -1: self.map_decoy(km) sdt = km.get_export_by_name('KeServiceDescriptorTable') if sdt: kbase = km.get_base() sdt_addr = kbase + sdt # Set the symbols up for i in range(0x20): self.symbols.update({ sdt_addr + i: (km.get_base_name(), 'KeServiceDescriptorTable') }) self.symbols.update({ sdt_addr: (km.get_base_name(), 'KeServiceDescriptorTable.pServiceTable') }) self.symbols.update({ sdt_addr + 0x10: (km.get_base_name(), 'KeServiceDescriptorTable.NumberOfServices') }) ksc64_off += 5 ksc64_addr = kbase + ksc64_off self.symbols.update( {ksc64_addr: (km.get_base_name(), 'KiSystemCall64')}) # Write the address of our fake KiSystemCall64 to the LSTAR register self.reg_write(_arch.X86_REG_MSR, (_arch.LSTAR, ksc64_addr)) # ssdt load: # KeServiceDescriptorTable sdt_offset = (sdt_addr - ksc64_addr) - 7 data = b'\x90\x90\xc3' + sdt_offset.to_bytes(4, 'little') self.mem_write(kbase + ksc64_off, data) ksc64_off += 7 # shadow_ssdt_load: # KeServiceDescriptorTableShadow sdt_offset = sdt_addr - (kbase + ksc64_off) data = b'\x90\x90\xc3' + sdt_offset.to_bytes(4, 'little') km.set_bytes(ksc64_off, data) ksc64_off += 7 data = b'\x90\x90\x90\x90\x90\x90\xc3' km.set_bytes(ksc64_off, data)
def load_module(self, path=None, data=None): """ Load the kernel module to be emulated """ pe = self.load_pe(path, data=data, imp_id=w32common.IMPORT_HOOK_ADDR) if pe.arch == _arch.ARCH_X86: disasm_mode = cs.CS_MODE_32 elif pe.arch == _arch.ARCH_AMD64: disasm_mode = cs.CS_MODE_64 else: raise KernelEmuError('Unsupported architecture: %s', pe.arch) if not self.arch: self.arch = pe.arch self.set_ptr_size(self.arch) self.emu_eng.init_engine(_arch.ARCH_X86, pe.arch) if not self.disasm_eng: self.disasm_eng = cs.Cs(cs.CS_ARCH_X86, disasm_mode) self.api = WindowsApi(self) self.om = objman.ObjectManager(emu=self) if not data: file_name = os.path.basename(path) mod_name = os.path.splitext(file_name)[0] else: drv_hash = hashlib.sha256() drv_hash.update(data) drv_hash = drv_hash.hexdigest() mod_name = drv_hash file_name = '%s.sys' % (mod_name) emu_path = '%sdrivers\\%s' % (self.get_system_root(), file_name) pe.emu_path = emu_path self.map_pe(pe, mod_name=mod_name, emu_path=emu_path) self.mem_write(pe.base, pe.mapped_image) # Strings the initial buffer so that we can detect decoded strings later on if self.profiler and self.do_strings: astrs = self.get_ansi_strings(pe.mapped_image) wstrs = self.get_unicode_strings(pe.mapped_image) for s in astrs: if s not in self.profiler.strings['ansi']: self.profiler.strings['ansi'].append(s) for s in wstrs: if s not in self.profiler.strings['unicode']: self.profiler.strings['unicode'].append(s) # Init imported data for addr, imp in pe.imports.items(): mn, fn = imp mod, eh = self.api.get_data_export_handler(mn, fn) if eh: data_ptr = self.handle_import_data(mn, fn) sym = "%s.%s" % (mn, fn) self.global_data.update({addr: [sym, data_ptr]}) self.mem_write( addr, data_ptr.to_bytes(self.get_ptr_size(), 'little')) # Set the emulator to run in protected mode self._setup_gdt(self.get_arch()) self.setup_kernel_mode() self.setup_user_shared_data() if not self.stack_base: self.stack_base, stack_ptr = self.alloc_stack(pe.stack_commit) return pe