def analyze_sample(self, sample_path, workdir, outdir, start_command, timeout): analysis_info = dict() dns_server = self.config.config['drakrun'].get('dns_server', '8.8.8.8') drakmon_log_fp = os.path.join(outdir, "drakmon.log") with self.run_vm() as vm, \ graceful_exit(start_dnsmasq(self.instance_id, dns_server)), \ graceful_exit(start_tcpdump_collector(self.instance_id, outdir)), \ open(drakmon_log_fp, "wb") as drakmon_log: analysis_info[ 'snapshot_version'] = vm.backend.get_vm0_snapshot_time() kernel_profile = os.path.join(PROFILE_DIR, "kernel.json") self.log.info("Copying sample to VM...") injector = Injector(self.vm_name, self.runtime_info, kernel_profile) result = injector.write_file( sample_path, f"%USERPROFILE%\\Desktop\\{os.path.basename(sample_path)}") injected_fn = json.loads(result.stdout)['ProcessName'] if "%f" not in start_command: self.log.warning("No file name in start command") cur_start_command = start_command.replace("%f", injected_fn) # don't include our internal maintanance commands analysis_info['start_command'] = cur_start_command self.log.info("Using command: %s", cur_start_command) if self.net_enable: self.log.info("Setting up network...") injector.create_process("cmd /C ipconfig /renew >nul", wait=True, timeout=120) drakvuf_cmd = self.build_drakvuf_cmdline( timeout=timeout, cwd=subprocess.list2cmdline([ntpath.dirname(injected_fn)]), full_cmd=cur_start_command, dump_dir=os.path.join(outdir, "dumps"), ipt_dir=os.path.join(outdir, "ipt"), workdir=workdir, ) try: subprocess.run(drakvuf_cmd, stdout=drakmon_log, check=True, timeout=timeout + 60) except subprocess.TimeoutExpired: self.log.exception("DRAKVUF timeout expired") return analysis_info
class DrakmonShell: def __init__(self, vm_id: int, dns: str): self.cleanup(vm_id) install_info = InstallInfo.load() backend = get_storage_backend(install_info) generate_vm_conf(install_info, vm_id) self.vm = VirtualMachine(backend, vm_id) self._dns = dns with open(Path(PROFILE_DIR) / "runtime.json", "r") as f: self.runtime_info = RuntimeInfo.load(f) self.desktop = WinPath(r"%USERPROFILE%") / "Desktop" self.kernel_profile = Path(PROFILE_DIR) / "kernel.json" self.injector = Injector( self.vm.vm_name, self.runtime_info, self.kernel_profile, ) setup_vm_network(vm_id, True, find_default_interface(), dns) def cleanup(self, vm_id: int): logging.info(f"Ensuring that drakrun@{vm_id} service is stopped...") try: subprocess.run( ["systemctl", "stop", f"drakrun@{vm_id}"], stderr=subprocess.STDOUT, check=True, ) except subprocess.CalledProcessError: raise Exception(f"drakrun@{vm_id} not stopped") def drakvuf(self, plugins, timeout=60): d = tempfile.TemporaryDirectory(prefix="drakvuf_") workdir = Path(d.name) log = open(workdir / "drakmon.log", "wb") cmd = ["drakvuf"] cmd.extend([ "-o", "json", "F", "-j", "5", "-t", str(timeout), "-i", str(self.runtime_info.inject_pid), "-k", str(self.runtime_info.vmi_offsets.kpgd), "-r", self.kernel_profile, "-d", self.vm.vm_name, "--dll-hooks-list", Path(ETC_DIR) / "hooks.txt", ]) if "memdump" in plugins: dumps = workdir / "dumps" dumps.mkdir() cmd.extend(["--memdump-dir", dumps]) if "ipt" in plugins: ipt = workdir / "ipt" ipt.mkdir() cmd.extend(["--ipt-dir", ipt]) for chk in (["-a", plugin] for plugin in plugins): cmd.extend(chk) subprocess.run(cmd, stdout=log) return d def help(self): usage = dedent("""\ Available commands: - copy(file_path) # copy file onto vm desktop - mount(iso_path) # mount iso, useful for installing software, e.g. office - drakvuf(plugins) # start drakvuf with provided set of plugins - run(cmd) # run command inside vm - exit() # exit playground """) print(usage) def copy(self, local): local = Path(local) self.injector.write_file(local, self.desktop / local.name) def mount(self, local_iso_path, drive=FIRST_CDROM_DRIVE): local_iso_path = Path(local_iso_path) insert_cd(self.vm.vm_name, drive, local_iso_path) def run(self, cmd): self.injector.create_process(cmd) def __enter__(self): self.vm.restore() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.vm.destroy() delete_vm_network(self.vm.vm_id, True, find_default_interface(), self._dns)
def analyze_sample(self, sample_path, workdir, outdir, start_command, timeout): analysis_info = dict() dns_server = self.config.config["drakrun"].get("dns_server", "8.8.8.8") drakmon_log_fp = os.path.join(outdir, "drakmon.log") with self.run_vm() as vm, graceful_exit( start_dnsmasq(self.instance_id, dns_server)), graceful_exit( start_tcpdump_collector(self.instance_id, outdir)), open( drakmon_log_fp, "wb") as drakmon_log: analysis_info[ "snapshot_version"] = vm.backend.get_vm0_snapshot_time() kernel_profile = os.path.join(PROFILE_DIR, "kernel.json") self.log.info("Copying sample to VM...") injector = Injector(self.vm_name, self.runtime_info, kernel_profile) result = injector.write_file( sample_path, f"%USERPROFILE%\\Desktop\\{os.path.basename(sample_path)}") try: injected_fn = json.loads(result.stdout)["ProcessName"] except ValueError as e: self.log.error( "JSON decode error occurred when tried to parse injector's logs." ) self.log.error(f"Raw log line: {result.stdout}") raise e # don't include our internal maintanance commands start_command = start_command.replace("%f", injected_fn) analysis_info["start_command"] = start_command self.log.info("Using command: %s", start_command) if self.net_enable: self.log.info("Setting up network...") injector.create_process("cmd /C ipconfig /renew >nul", wait=True, timeout=120) task_quality = self.current_task.headers.get("quality", "high") requested_plugins = self.current_task.payload.get( "plugins", self.active_plugins["_all_"]) analysis_info["plugins"] = self.get_plugin_list( task_quality, requested_plugins) drakvuf_cmd = self.build_drakvuf_cmdline( timeout=timeout, cwd=subprocess.list2cmdline([ntpath.dirname(injected_fn)]), full_cmd=start_command, dump_dir=os.path.join(outdir, "dumps"), ipt_dir=os.path.join(outdir, "ipt"), workdir=workdir, enabled_plugins=analysis_info["plugins"], ) try: subprocess.run(drakvuf_cmd, stdout=drakmon_log, check=True, timeout=timeout + 60) except subprocess.CalledProcessError as e: # see DRAKVUF src/exitcodes.h for more details INJECTION_UNSUCCESSFUL = 4 if e.returncode == INJECTION_UNSUCCESSFUL: self.log_startup_failure(drakmon_log_fp) raise e except subprocess.TimeoutExpired as e: self.log.exception("DRAKVUF timeout expired") raise e return analysis_info
def process(self): sample = self.current_task.get_resource("sample") self.log.info("hostname: {}".format(socket.gethostname())) sha256sum = hashlib.sha256(sample.content).hexdigest() magic_output = magic.from_buffer(sample.content) self.log.info("running sample sha256: {}".format(sha256sum)) timeout = self.current_task.payload.get( 'timeout') or self.default_timeout hard_time_limit = 60 * 20 if timeout > hard_time_limit: self.log.error( "Tried to run the analysis for more than hard limit of %d seconds", hard_time_limit) return self.log.info(f"analysis UID: {self.analysis_uid}") self.rs.set(f"drakvnc:{self.analysis_uid}", self.instance_id, ex=3600) # 1h workdir = f"/tmp/drakrun/{self.vm_name}" extension = self.current_task.headers.get("extension", "exe").lower() if '(DLL)' in magic_output: extension = 'dll' self.log.info("Running file as %s", extension) file_name = self.current_task.payload.get("file_name", "malwar") + f".{extension}" # Alphanumeric, dot, underscore, dash if not re.match(r"^[a-zA-Z0-9\._\-]+$", file_name): self.log.error("Filename contains invalid characters") return self.log.info("Using file name %s", file_name) # Save sample to disk here as some branches of _get_start_command require file path. try: shutil.rmtree(workdir) except Exception as e: print(e) os.makedirs(workdir, exist_ok=True) with open(os.path.join(workdir, file_name), 'wb') as f: f.write(sample.content) start_command = self.current_task.payload.get( "start_command", self._get_start_command(extension, sample, os.path.join(workdir, file_name))) if not start_command: self.log.error( "Unable to run malware sample, could not generate any suitable command to run it." ) return outdir = os.path.join(workdir, 'output') os.mkdir(outdir) os.mkdir(os.path.join(outdir, 'dumps')) os.mkdir(os.path.join(outdir, 'ipt')) metadata = { "sample_sha256": sha256sum, "magic_output": magic_output, "time_started": int(time.time()) } with open(os.path.join(outdir, 'sample_sha256.txt'), 'w') as f: f.write(hashlib.sha256(sample.content).hexdigest()) watcher_tcpdump = None watcher_dnsmasq = None for _ in range(3): try: self.log.info("Running VM {}".format(self.instance_id)) watcher_dnsmasq = start_dnsmasq( self.instance_id, self.config.config['drakrun'].get('dns_server', '8.8.8.8')) snapshot_version = self.run_vm() metadata['snapshot_version'] = snapshot_version watcher_tcpdump = start_tcpdump_collector( self.instance_id, outdir) self.log.info("running monitor {}".format(self.instance_id)) hooks_list = os.path.join(ETC_DIR, "hooks.txt") kernel_profile = os.path.join(PROFILE_DIR, "kernel.json") dump_dir = os.path.join(outdir, "dumps") ipt_dir = os.path.join(outdir, "ipt") drakmon_log_fp = os.path.join(outdir, "drakmon.log") self.log.info("Copying sample to VM...") injector = Injector(self.vm_name, self.runtime_info, kernel_profile) result = injector.write_file( os.path.join(workdir, file_name), f"%USERPROFILE%\\Desktop\\{file_name}") injected_fn = json.loads(result.stdout)['ProcessName'] net_enable = int(self.config.config['drakrun'].get( 'net_enable', '0')) if "%f" not in start_command: self.log.warning("No file name in start command") cwd = subprocess.list2cmdline([ntpath.dirname(injected_fn)]) cur_start_command = start_command.replace("%f", injected_fn) # don't include our internal maintanance commands metadata['start_command'] = cur_start_command if net_enable: self.log.info("Setting up network...") injector.create_process("cmd /C ipconfig /renew >nul", wait=True, timeout=120) full_cmd = cur_start_command self.log.info("Using command: %s", full_cmd) task_quality = self.current_task.headers.get("quality", "high") drakvuf_cmd = ["drakvuf"] + self.generate_plugin_cmdline(task_quality) + \ ["-o", "json" "-j", "5", "-t", str(timeout), "-i", str(self.runtime_info.inject_pid), "-k", hex(self.runtime_info.vmi_offsets.kpgd), "-d", self.vm_name, "--dll-hooks-list", hooks_list, "--memdump-dir", dump_dir, "-r", kernel_profile, "-e", full_cmd, "-c", cwd] if self.config.config['drakrun'].getboolean('enable_ipt', fallback=False): drakvuf_cmd.extend(["--ipt-dir", ipt_dir]) drakvuf_cmd.extend(self.get_profile_list()) syscall_filter = self.config.config['drakrun'].get( 'syscall_filter', None) if syscall_filter: drakvuf_cmd.extend(["-S", syscall_filter]) with open(drakmon_log_fp, "wb") as drakmon_log: drakvuf = subprocess.Popen(drakvuf_cmd, stdout=drakmon_log) try: exit_code = drakvuf.wait(timeout + 60) except subprocess.TimeoutExpired as e: logging.error( "BUG: Monitor command doesn\'t terminate automatically after timeout expires." ) logging.error("Trying to terminate DRAKVUF...") drakvuf.terminate() drakvuf.wait(10) logging.error( "BUG: Monitor command also doesn\'t terminate after sending SIGTERM." ) drakvuf.kill() drakvuf.wait() logging.error("Monitor command was forcefully killed.") raise e if exit_code != 0: raise subprocess.CalledProcessError( exit_code, drakvuf_cmd) break except subprocess.CalledProcessError: self.log.info("Something went wrong with the VM {}".format( self.instance_id), exc_info=True) finally: try: subprocess.run(["xl", "destroy", self.vm_name], cwd=workdir, check=True) except subprocess.CalledProcessError: self.log.info("Failed to destroy VM {}".format( self.instance_id), exc_info=True) if watcher_dnsmasq: watcher_dnsmasq.terminate() else: self.log.info( "Failed to analyze sample after 3 retries, giving up.") return self.log.info("waiting for tcpdump to exit") if watcher_tcpdump: try: watcher_tcpdump.wait(timeout=60) except subprocess.TimeoutExpired: self.log.exception("tcpdump doesn't exit cleanly after 60s") self.crop_dumps(os.path.join(outdir, 'dumps'), os.path.join(outdir, 'dumps.zip')) if self.config.config['drakrun'].getboolean('enable_ipt', fallback=False): self.compress_ipt(os.path.join(outdir, 'ipt'), os.path.join(outdir, 'ipt.zip')) self.log.info("uploading artifacts") metadata['time_finished'] = int(time.time()) with open(os.path.join(outdir, 'metadata.json'), 'w') as f: f.write(json.dumps(metadata)) payload = {"analysis_uid": self.analysis_uid} payload.update(metadata) headers = dict(self.headers) headers["quality"] = self.current_task.headers.get("quality", "high") t = Task(headers, payload=payload) for resource in self.upload_artifacts(self.analysis_uid, workdir): t.add_payload(resource.name, resource) t.add_payload('sample', sample) self.send_task(t)
class DrakmonShell: def __init__(self, vm_id: int, dns: str): self.cleanup(vm_id) install_info = InstallInfo.load() backend = get_storage_backend(install_info) generate_vm_conf(install_info, vm_id) self.vm = VirtualMachine(backend, vm_id) self._dns = dns with open(Path(PROFILE_DIR) / "runtime.json", 'r') as f: self.runtime_info = RuntimeInfo.load(f) self.desktop = WinPath(r"%USERPROFILE%") / "Desktop" self.kernel_profile = Path(PROFILE_DIR) / "kernel.json" self.injector = Injector( self.vm.vm_name, self.runtime_info, self.kernel_profile, ) setup_vm_network(vm_id, True, find_default_interface(), dns) def cleanup(self, vm_id: int): logging.info(f"Ensuring that drakrun@{vm_id} service is stopped...") try: subprocess.run(['systemctl', 'stop', f'drakrun@{vm_id}'], stderr=subprocess.STDOUT, check=True) except subprocess.CalledProcessError: raise Exception(f"drakrun@{vm_id} not stopped") def drakvuf(self, plugins, timeout=60): d = tempfile.TemporaryDirectory(prefix="drakvuf_") workdir = Path(d.name) log = open(workdir / "drakmon.log", "wb") cmd = ["drakvuf"] cmd.extend([ "-o", "json", "F", "-j", "5", "-t", str(timeout), "-i", str(self.runtime_info.inject_pid), "-k", str(self.runtime_info.vmi_offsets.kpgd), "-r", self.kernel_profile, "-d", self.vm.vm_name, "--dll-hooks-list", Path(ETC_DIR) / "hooks.txt", ]) if "memdump" in plugins: dumps = workdir / "dumps" dumps.mkdir() cmd.extend(["--memdump-dir", dumps]) if "ipt" in plugins: ipt = workdir / "ipt" ipt.mkdir() cmd.extend(["--ipt-dir", ipt]) for chk in (["-a", plugin] for plugin in plugins): cmd.extend(chk) subprocess.run(cmd, stdout=log) return d def copy(self, local): local = Path(local) self.injector.write_file(local, self.desktop / local.name) def run(self, cmd): self.injector.create_process(cmd) def __enter__(self): self.vm.restore() return self def __exit__(self, exc_type, exc_value, exc_traceback): self.vm.destroy() delete_vm_network(self.vm.vm_id, True, find_default_interface(), self._dns)