class NitroThread(Thread): def __init__(self, domain, analyze=False, enter_hooks=None, exit_hooks=None, ready_event=None): super().__init__() self.domain = domain self.analyze_enabled = analyze self.enter_hooks = enter_hooks or {} self.exit_hooks = exit_hooks or {} self.stop_request = Event() self.total_time = None self.events = [] self.ready_event = ready_event self.nitro = Nitro(self.domain, self.analyze_enabled) def run(self): # start timer start_time = datetime.datetime.now() if self.analyze_enabled: for name, callback in self.enter_hooks.items(): self.nitro.backend.define_hook( name, callback, direction=SyscallDirection.enter) for name, callback in self.exit_hooks.items(): self.nitro.backend.define_hook(name, callback, direction=SyscallDirection.exit) self.nitro.listener.set_traps(True) if self.ready_event is not None: self.ready_event.set() # is this really necessary for event in self.nitro.listen(): if self.analyze_enabled: try: syscall = self.nitro.backend.process_event(event) except LibvmiError: ev_info = event.as_dict() else: ev_info = syscall.as_dict() else: ev_info = event.as_dict() self.events.append(ev_info) # stop timer self.ready_event.clear() stop_time = datetime.datetime.now() self.total_time = str(stop_time - start_time) def stop(self): self.nitro.stop() self.join()
class NitroRunner: def __init__(self, vm_name, analyze_enabled, output=None): self.vm_name = vm_name self.analyze_enabled = analyze_enabled self.output = output # get domain from libvirt con = libvirt.open('qemu:///system') self.domain = con.lookupByName(vm_name) self.events = [] self.nitro = None # define new SIGINT handler, to stop nitro signal.signal(signal.SIGINT, self.sigint_handler) def run(self): self.nitro = Nitro(self.domain, self.analyze_enabled) self.nitro.listener.set_traps(True) for event in self.nitro.listen(): event_info = event.as_dict() if self.analyze_enabled: try: syscall = self.nitro.backend.process_event(event) except LibvmiError: logging.error("Backend event processing failure") else: event_info = syscall.as_dict() if self.output is None: pprint(event_info, width=1) else: self.events.append(event_info) if self.analyze_enabled: # we can safely stop the backend self.nitro.backend.stop() if self.output is not None: logging.info('Writing events') with open(self.output, 'w') as f: json.dump(self.events, f, indent=4) def sigint_handler(self, *args, **kwargs): logging.info('CTRL+C received, stopping Nitro') self.nitro.stop()
class NitroRunner: def __init__(self, vm_name, analyze_enabled, output=None): self.start = timer() self.end = 0 self.is_busy = False self.vm_name = vm_name self.analyze_enabled = analyze_enabled self.output = output # get domain from libvirt con = libvirt.open('qemu:///system') self.domain = con.lookupByName(vm_name) self.events = [] self.nitro = None self.ox = None # define new SIGINT handler, to stop nitro signal.signal(signal.SIGINT, self.sigint_handler) def run(self): # ffff880074c13c74 print("Wait for setup rekall and nitro ...") self.nitro = Nitro(self.domain, self.analyze_enabled) self.ox = OX(self.nitro) while True: print("Which program you want to execute:") print("---- help:") print("---- kill [pid]") print("---- write [vaddr] [optional value]") print("---- nop") print("---- exit") print("---- statistics") callback = None is_statistics = False cmd = input("> ") command = cmd.split() print(command) if command[0].lower() not in [ "kill", "write", "exit", "nop", "statistics" ]: print("Program not found") continue if command[0].lower() == "exit": print("Bye!") break try: if command[0].lower() == "statistics": is_statistics = True if command[0].lower() == "nop": callback = NOPCallback(self.ox).do_callback if command[0].lower() == "kill": if len(command) != 2: print("PID is needed for kill program") continue else: callback = KillCallback(self.ox, int(command[1])).do_callback if command[0].lower() == "write": if len(command) == 2: callback = WriteCallback(self.ox, int(command[1], 0)).do_callback elif len(command) == 3: callback = WriteCallback(self.ox, int(command[1], 0), int(command[2])).do_callback else: print("vaddr is needed for write program") continue loop_nr = int( input( "> How many times do you want to execute this program[defualt: 1]: " ) or "1") sleep_time = float( input( "> How much do you want to sleep between each run[defualt: 0.1]: " ) or "0.1") delay_file_output = input("> Save results in which file: ") except ValueError as e: print(e) print("Value error. Please enter a valid value") continue self.is_busy = 0 busy_times = 0.0 total_times = 0.0 # callback() for i in range(0, loop_nr): print("#{}".format(i)) # self.start = timer() try: # loop = asyncio.get_event_loop() # self.end = loop.run_until_complete(asyncio.wait_for( # self.ox.begin_transaction(callback, loop), 3) # ) # for signame in ('SIGINT', 'SIGTERM'): # loop.add_signal_handler(getattr(signal, signame), # functools.partial(self.ox.run_rollback)) # # loop.close() if is_statistics: self.is_busy = self.ox.get_statistic() self.end = 1 if self.is_busy: busy_times += 1 total_times += 1 else: self.end, self.start = self.ox.sync_begin_transaction( callback) except Exception as e: traceback.print_exc() print("Oxpecker got exception.") self.end = 0 self.is_busy = 0 finally: if self.nitro.listener.current_cont_event: self.nitro.listener.stop_listen() # self.is_busy = self.ox.get_statistic() # self.nitro.listener.stop() is_write_to_output = self.end != 0 if delay_file_output and is_write_to_output: row = [self.is_busy] if is_statistics else [ self.start, self.end, self.end - self.start ] with open(delay_file_output, "a") as f: writer = csv.writer(f) writer.writerow(row) sleep(sleep_time) if is_statistics: print('busy times: {}, total_times:{}'.format( busy_times, total_times)) print( 'The percentage of idle time: {}'.format(1.0 - busy_times / total_times)) self.nitro.listener.close_fd() def sigint_handler(self, *args, **kwargs): logging.info('CTRL+C received, stopping Nitro') self.nitro.stop()
class Backend: __slots__ = ( 'domain', 'nb_vcpu', 'syscall_stack', 'sdt', 'libvmi', 'processes', 'hooks', 'stats', 'nitro', 'analyze', 'symbols' ) def __init__(self, domain, analyze=False): self.domain = domain self.analyze = analyze # init Nitro self.nitro = Nitro(self.domain) if analyze: vcpus_info = self.domain.vcpus() self.nb_vcpu = len(vcpus_info[0]) # create on syscall stack per vcpu self.syscall_stack = {} for vcpu_nb in range(self.nb_vcpu): self.syscall_stack[vcpu_nb] = [] self.sdt = None self.load_symbols() # run libvmi helper subprocess self.libvmi = Libvmi(domain.name()) self.processes = {} self.hooks = { SyscallDirection.enter: {}, SyscallDirection.exit: {} } self.stats = defaultdict(int) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.stop() def stop(self): if self.analyze: # print stats if some errors happened if self.stats: logging.info(json.dumps(self.stats, indent=4)) self.libvmi.destroy() self.nitro.stop() def load_symbols(self): # we need to put the ram dump in our own directory # because otherwise it will be created in /tmp # and later owned by root with TemporaryDirectory() as tmp_dir: with NamedTemporaryFile(dir=tmp_dir) as ram_dump: # chmod to be r/w by everyone os.chmod(ram_dump.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) # take a ram dump logging.info('Dumping physical memory to {}'.format(ram_dump.name)) flags = libvirt.VIR_DUMP_MEMORY_ONLY dumpformat = libvirt.VIR_DOMAIN_CORE_DUMP_FORMAT_RAW self.domain.coreDumpWithFormat(ram_dump.name, dumpformat, flags) # build symbols.py absolute path script_dir = os.path.dirname(os.path.realpath(__file__)) symbols_script_path = os.path.join(script_dir, GETSYMBOLS_SCRIPT) # call rekall on ram dump logging.info('Extracting symbols with Rekall') python2 = shutil.which('python2') symbols_process = [python2, symbols_script_path, ram_dump.name] output = subprocess.check_output(symbols_process) logging.info('Loading symbols') # load output as json symbols = json.loads(output.decode('utf-8')) # load ssdt entries nt_ssdt = {'ServiceTable': {}, 'ArgumentTable': {}} win32k_ssdt = {'ServiceTable': {}, 'ArgumentTable': {}} self.sdt = [nt_ssdt, win32k_ssdt] cur_ssdt = None for e in symbols['syscall_table']: if isinstance(e, list) and e[0] == 'r': if e[1]["divider"] is not None: # new table m = re.match(r'Table ([0-9]) @ .*', e[1]["divider"]) idx = int(m.group(1)) cur_ssdt = self.sdt[idx]['ServiceTable'] else: entry = e[1]["entry"] full_name = e[1]["symbol"]["symbol"] # add entry to our current ssdt cur_ssdt[entry] = full_name logging.debug('Add SSDT entry [{}] -> {}'.format(entry, full_name)) # save rekall symbols self.symbols = symbols def process_event(self, event): if not self.analyze: raise RuntimeError('Syscall analyzing is disabled in the backend') # invalidate libvmi cache self.libvmi.v2pcache_flush() self.libvmi.pidcache_flush() self.libvmi.rvacache_flush() self.libvmi.symcache_flush() # rebuild context cr3 = event.sregs.cr3 # 1 find process process = self.associate_process(cr3) # 2 find syscall if event.direction == SyscallDirection.exit: try: syscall = self.syscall_stack[event.vcpu_nb].pop() # replace register values syscall.event = event except IndexError: # build a new syscall object using 'Unknown' as syscall name syscall = Syscall(event, 'Unknown', process, self.nitro) else: syscall_name = self.get_syscall_name(event.regs.rax) # build syscall syscall = Syscall(event, syscall_name, process, self.nitro) # push syscall to the stack to retrieve it at exit self.syscall_stack[event.vcpu_nb].append(syscall) # dispatch on the hooks self.dispatch_hooks(syscall) return syscall def dispatch_hooks(self, syscall): try: hook = self.hooks[syscall.event.direction][syscall.name] except KeyError: pass else: try: logging.debug('Processing hook {} - {}'.format(syscall.event.direction.name, hook.__name__)) hook(syscall, self) except InconsistentMemoryError: self.stats['memory_access_error'] += 1 logging.exception('Memory access error') except LibvmiError: self.stats['libvmi_failure'] += 1 logging.exception('VMI_FAILURE') # misc failures except ValueError: self.stats['misc_error'] += 1 logging.exception('Misc error') except Exception: logging.exception('Unknown error while processing hook') else: self.stats['hooks_completed'] += 1 finally: self.stats['hooks_processed'] += 1 def define_hook(self, name, callback, direction=SyscallDirection.enter): logging.info('Defining hook on {}'.format(name)) self.hooks[direction][name] = callback def undefine_hook(self, name, direction=SyscallDirection.enter): logging.info('Removing hook on {}'.format(name)) self.hooks[direction].pop(name) def associate_process(self, cr3): try: p = self.processes[cr3] except KeyError: p = self.find_eprocess(cr3) # index by cr3 or pid # a callback might want to search by pid self.processes[cr3] = p self.processes[p.pid] = p return p def find_eprocess(self, cr3): # read PsActiveProcessHead list_entry ps_head = self.libvmi.translate_ksym2v('PsActiveProcessHead') flink = self.libvmi.read_addr_ksym('PsActiveProcessHead') while flink != ps_head: # get start of EProcess start_eproc = flink - self.symbols['offsets']['EPROCESS']['ActiveProcessLinks'] # move to start of DirectoryTableBase directory_table_base_off = start_eproc + self.symbols['offsets']['KPROCESS']['DirectoryTableBase'] # read directory_table_base directory_table_base = self.libvmi.read_addr_va(directory_table_base_off, 0) # compare to our cr3 if cr3 == directory_table_base: eprocess = Process(cr3, start_eproc, self.libvmi, self.symbols) return eprocess # read new flink flink = self.libvmi.read_addr_va(flink, 0) raise RuntimeError('Process not found') def get_syscall_name(self, rax): ssn = rax & 0xFFF idx = (rax & 0x3000) >> 12 try: syscall_name = self.sdt[idx]['ServiceTable'][ssn] except (KeyError, IndexError): # this code should not be reached, because there is only 2 SSDT's defined in Windows (Nt and Win32k) # the 2 others are NULL syscall_name = 'Table{}!Unknown'.format(idx) return syscall_name