Skip to content

Commit

Permalink
drakrun: Attach profiles to analyses (#504)
Browse files Browse the repository at this point in the history
* Declare more usermode profiles to be generated by drakpdb.
* Add the configuration knob to attach profiles to analyses.
* Extend profile metadata wil new field - DLLPath - containing original path in Windows
* Generate missing usermode profiles on postupgrade
  • Loading branch information
chivay authored Apr 23, 2021
1 parent a7a6f71 commit 3ac6cf2
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 64 deletions.
2 changes: 2 additions & 0 deletions drakrun/drakrun/config.dist.ini
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ syscall_filter=
; protects against API hammering techniques, default 0 (means: off)
; anti_hammering_threshold=0

; (advanced) Attach DLL profiles to analyses
; attach_profiles=0

[drakvuf_plugins]
; list of enabled DRAKVUF plugins that are used by default,
Expand Down
64 changes: 56 additions & 8 deletions drakrun/drakrun/drakpdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
from construct import Struct, Const, Bytes, Int32ul, Int16ul, CString, EnumIntegerString
from requests import HTTPError
from tqdm import tqdm
from typing import NamedTuple
from typing import NamedTuple, Optional, List

DLL = NamedTuple("DLL", [("path", str), ("dest", str), ("arg", Optional[str])])


def dll_pair(name: str, extension: str = "dll") -> List[DLL]:
return [
DLL(f"Windows/System32/{name}.{extension}", f"{name}_profile", None),
DLL(f"Windows/SysWOW64/{name}.{extension}", f"wow_{name}_profile", None),
]

DLL = NamedTuple("DLL", [("path", str), ("dest", str), ("arg", str)])

# profile file list, without 'C:\' and with '/' instead of '\'
dll_file_list = [
Expand All @@ -26,13 +33,47 @@
DLL("Windows/System32/KernelBase.dll", "kernelbase_profile", "--json-kernelbase"),
DLL("Windows/SysWOW64/kernel32.dll", "wow_kernel32_profile", "--json-wow-kernel32"),
DLL("Windows/System32/IPHLPAPI.DLL", "iphlpapi_profile", "--json-iphlpapi"),
DLL("Windows/SysWOW64/IPHLPAPI.DLL", "wow_iphlpapi_profile", None),
DLL("Windows/System32/mpr.dll", "mpr_profile", "--json-mpr"),
DLL("Windows/SysWOW64/mpr.dll", "wow_mpr_profile", None),
DLL("Windows/System32/ntdll.dll", "ntdll_profile", "--json-ntdll"),
DLL("Windows/System32/ole32.dll", "ole32_profile", "--json-ole32"),
DLL("Windows/SysWOW64/ole32.dll", "wow_ole32_profile", "--json-wow-ole32"),
# Don't use DRAKVUF arguments, they're used by wmimon which is compiled out
# DLL("Windows/System32/ole32.dll", "ole32_profile", "--json-ole32"),
# DLL("Windows/SysWOW64/ole32.dll", "wow_ole32_profile", "--json-wow-ole32"),
*dll_pair("ole32"),
DLL("Windows/System32/combase.dll", "combase_profile", "--json-combase"),
DLL("Windows/Microsoft.NET/Framework/v4.0.30319/clr.dll", "clr_profile", "--json-clr"),
DLL("Windows/Microsoft.NET/Framework/v2.0.50727/mscorwks.dll", "mscorwks_profile", "--json-mscorwks")
DLL("Windows/Microsoft.NET/Framework/v2.0.50727/mscorwks.dll", "mscorwks_profile", "--json-mscorwks"),
*dll_pair("Wldap32"),
*dll_pair("comctl32"),
*dll_pair("crypt32"),
*dll_pair("dnsapi"),
*dll_pair("gdi32"),
*dll_pair("imagehlp"),
*dll_pair("imm32"),
*dll_pair("msacm32"),
*dll_pair("msvcrt"),
*dll_pair("netapi32"),
*dll_pair("oleaut32"),
*dll_pair("powrprof"),
*dll_pair("psapi"),
*dll_pair("rpcrt4"),
*dll_pair("secur32"),
*dll_pair("SensApi"),
*dll_pair("shell32"),
*dll_pair("shlwapi"),
*dll_pair("urlmon"),
*dll_pair("user32"),
*dll_pair("userenv"),
*dll_pair("version"),
*dll_pair("winhttp"),
*dll_pair("wininet"),
*dll_pair("winmm"),
*dll_pair("winspool", extension="drv"),
*dll_pair("ws2_32"),
*dll_pair("wsock32"),
*dll_pair("wtsapi32"),
# GdiPlus ?
]


Expand Down Expand Up @@ -243,12 +284,15 @@ def process_struct(struct_info):
except AttributeError:
pass

fields = [struct.name for struct in ss.values()]
field_info = {ss[field].name: [ss[field].offset, get_field_type_info(ss[field])] for field in fields}
field_info = {}
for name, field in ss.items():
typ = get_field_type_info(field)
field_info[name] = (field.offset, typ)

return [struct_info.size, field_info]


def make_pdb_profile(filepath):
def make_pdb_profile(filepath, dll_origin_path=None):
pdb = pdbparse.parse(filepath)

try:
Expand Down Expand Up @@ -321,6 +365,10 @@ def make_pdb_profile(filepath):
"Type": "Profile",
"Version": pdb.STREAM_PDB.Version
}

if dll_origin_path:
profile["$METADATA"]["DLLPath"] = str(dll_origin_path)

return json.dumps(profile, indent=4, sort_keys=True)


Expand Down
161 changes: 109 additions & 52 deletions drakrun/drakrun/draksetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
from requests import RequestException
from minio import Minio
from minio.error import NoSuchKey
from drakrun.drakpdb import fetch_pdb, make_pdb_profile, dll_file_list, pdb_guid
from drakrun.drakpdb import fetch_pdb, make_pdb_profile, dll_file_list, pdb_guid, DLL
from drakrun.config import InstallInfo, LIB_DIR, VOLUME_DIR, PROFILE_DIR, ETC_DIR, VM_CONFIG_DIR
from drakrun.networking import setup_vm_network, start_dnsmasq, delete_vm_network, stop_dnsmasq
from drakrun.storage import get_storage_backend, REGISTERED_BACKEND_NAMES
from drakrun.injector import Injector
from drakrun.vm import generate_vm_conf, FIRST_CDROM_DRIVE, SECOND_CDROM_DRIVE, get_all_vm_conf, delete_vm_conf, VirtualMachine
from drakrun.util import RuntimeInfo, VmiOffsets, safe_delete
from tqdm import tqdm
from pathlib import PureWindowsPath
from pathlib import Path, PureWindowsPath
import traceback


Expand Down Expand Up @@ -101,6 +101,22 @@ def stop_all_drakruns():
raise Exception("Drakrun services not stopped")


def start_enabled_drakruns():
logging.info("Starting previously stopped drakruns")
enabled_services = set(list(get_enabled_drakruns()))
wait_processes(
"start services",
[
subprocess.Popen(
["systemctl", "start", service],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
for service in enabled_services
],
)


def cleanup_postinstall_files():
for profile in os.listdir(PROFILE_DIR):
safe_delete(os.path.join(PROFILE_DIR, profile))
Expand Down Expand Up @@ -393,57 +409,55 @@ def send_usage_report(report):
logging.exception("Failed to send usage report. This is not a serious problem.")


def create_rekall_profiles(injector: Injector):
def create_rekall_profile(injector: Injector, file: DLL):
tmp = None

for file in dll_file_list:
try:
logging.info(f"Fetching rekall profile for {file.path}")

local_dll_path = os.path.join(PROFILE_DIR, file.dest)
guest_dll_path = str(PureWindowsPath("C:/", file.path))

cmd = injector.read_file(guest_dll_path, local_dll_path)
out = json.loads(cmd.stdout.decode())
if out["Status"] == "Error" and out["Error"] in ["ERROR_FILE_NOT_FOUND", "ERROR_PATH_NOT_FOUND"]:
raise FileNotFoundError
if out["Status"] != "Success":
logging.debug("stderr: " + cmd.stderr.decode())
logging.debug(out)
# Take care if the error message is changed
raise Exception("Some error occurred in injector")

guid = pdb_guid(local_dll_path)
tmp = fetch_pdb(guid["filename"], guid["GUID"], PROFILE_DIR)

logging.debug("Parsing PDB into JSON profile...")
profile = make_pdb_profile(tmp)
with open(os.path.join(PROFILE_DIR, f"{file.dest}.json"), 'w') as f:
f.write(profile)
except json.JSONDecodeError:
logging.debug(f"stdout: {cmd.stdout}")
logging.debug(f"stderr: {cmd.stderr}")
logging.debug(traceback.format_exc())
raise Exception(f"Failed to parse json response on {file.path}")
except FileNotFoundError:
logging.warning(f"Failed to copy file {file.path}, skipping...")
except RuntimeError:
logging.warning(f"Failed to fetch profile for {file.path}, skipping...")
except Exception as e:
try:
logging.info(f"Fetching rekall profile for {file.path}")

local_dll_path = os.path.join(PROFILE_DIR, file.dest)
guest_dll_path = str(PureWindowsPath("C:/", file.path))

cmd = injector.read_file(guest_dll_path, local_dll_path)
out = json.loads(cmd.stdout.decode())
if out["Status"] == "Error" and out["Error"] in ["ERROR_FILE_NOT_FOUND", "ERROR_PATH_NOT_FOUND"]:
raise FileNotFoundError
if out["Status"] != "Success":
logging.debug("stderr: " + cmd.stderr.decode())
logging.debug(out)
# Take care if the error message is changed
if str(e) == "Some error occurred in injector":
raise
else:
logging.warning(f"Unexpected exception while creating rekall profile for {file.path}, skipping...")
# Can help in debugging
logging.debug("stderr: " + cmd.stderr.decode())
logging.debug(out)
logging.debug(traceback.format_exc())
finally:
safe_delete(local_dll_path)
# was crashing here if the first file reached some exception
if tmp is not None:
safe_delete(os.path.join(PROFILE_DIR, tmp))
raise Exception("Some error occurred in injector")

guid = pdb_guid(local_dll_path)
tmp = fetch_pdb(guid["filename"], guid["GUID"], PROFILE_DIR)

logging.debug("Parsing PDB into JSON profile...")
profile = make_pdb_profile(tmp, dll_origin_path=guest_dll_path)
with open(os.path.join(PROFILE_DIR, f"{file.dest}.json"), 'w') as f:
f.write(profile)
except json.JSONDecodeError:
logging.debug(f"stdout: {cmd.stdout}")
logging.debug(f"stderr: {cmd.stderr}")
logging.debug(traceback.format_exc())
raise Exception(f"Failed to parse json response on {file.path}")
except FileNotFoundError:
logging.warning(f"Failed to copy file {file.path}, skipping...")
except RuntimeError:
logging.warning(f"Failed to fetch profile for {file.path}, skipping...")
except Exception as e:
# Take care if the error message is changed
if str(e) == "Some error occurred in injector":
raise
else:
logging.warning(f"Unexpected exception while creating rekall profile for {file.path}, skipping...")
# Can help in debugging
logging.debug("stderr: " + cmd.stderr.decode())
logging.debug(out)
logging.debug(traceback.format_exc())
finally:
safe_delete(local_dll_path)
# was crashing here if the first file reached some exception
if tmp is not None:
safe_delete(os.path.join(PROFILE_DIR, tmp))


def extract_explorer_pid(
Expand Down Expand Up @@ -590,7 +604,8 @@ def postinstall(report, generate_usermode):
injector = Injector('vm-0', runtime_info, kernel_profile)
if generate_usermode:
try:
create_rekall_profiles(injector)
for file in dll_file_list:
create_rekall_profile(injector, file)
except RuntimeError as e:
logging.warning("Generating usermode profiles failed")
logging.exception(e)
Expand Down Expand Up @@ -618,8 +633,23 @@ def postinstall(report, generate_usermode):
logging.info(" # draksetup scale <number of instances>")


def profile_exists(profile: DLL) -> bool:
return (Path(PROFILE_DIR) / f"{profile.dest}.json").is_file()


def create_missing_profiles(injector: Injector):
# Ensure that all declared usermode profiles exist
# This is important when upgrade defines new entries in dll_file_list
for profile in dll_file_list:
if not profile_exists(profile):
create_rekall_profile(injector, profile)


@click.command(help='Perform tasks after drakrun upgrade')
def postupgrade():
if not check_root():
return

with open(os.path.join(ETC_DIR, 'scripts/cfg.template'), 'r') as f:
template = f.read()

Expand All @@ -632,6 +662,33 @@ def postupgrade():

detect_defaults()

install_info = InstallInfo.try_load()
if not install_info:
logging.info("Postupgrade done. DRAKVUF Sandbox not installed.")
return

# Prepare injector
with open(os.path.join(PROFILE_DIR, "runtime.json"), 'r') as runtime_f:
runtime_info = RuntimeInfo.load(runtime_f)
kernel_profile = os.path.join(PROFILE_DIR, "kernel.json")
injector = Injector('vm-1', runtime_info, kernel_profile)

stop_all_drakruns()

# Use vm-1 for generating profiles
out_interface = conf['drakrun'].get('out_interface', '')
dns_server = conf['drakrun'].get('dns_server', '')
setup_vm_network(vm_id=1, net_enable=False, out_interface=out_interface, dns_server=dns_server)
backend = get_storage_backend(install_info)
vm = VirtualMachine(backend, 1)
vm.restore()

create_missing_profiles(injector)

vm.destroy()
delete_vm_network(vm_id=1, net_enable=False, out_interface=out_interface, dns_server=dns_server)
start_enabled_drakruns()


def get_enabled_drakruns():
for fn in os.listdir("/etc/systemd/system/default.target.wants"):
Expand Down
21 changes: 20 additions & 1 deletion drakrun/drakrun/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
import json
import re
import functools
import tempfile
from io import StringIO
from typing import List, Dict
from stat import S_ISREG, ST_CTIME, ST_MODE, ST_SIZE
from configparser import NoOptionError
from itertools import chain
from pathlib import Path

import pefile
import magic
import ntpath
from karton.core import Karton, Config, Task, LocalResource
from karton.core import Karton, Config, Task, LocalResource, Resource

import drakrun.office as d_office
from drakrun.version import __version__ as DRAKRUN_VERSION
Expand Down Expand Up @@ -338,6 +340,17 @@ def upload_artifacts(self, analysis_uid, outdir, subdir=''):
elif os.path.isdir(file_path):
yield from self.upload_artifacts(analysis_uid, outdir, os.path.join(subdir, fn))

def build_profile_payload(self) -> Dict[str, LocalResource]:
with tempfile.TemporaryDirectory() as tmp_path:
tmp_dir = Path(tmp_path)

for profile in dll_file_list:
fpath = Path(PROFILE_DIR) / f"{profile.dest}.json"
if fpath.is_file():
shutil.copy(fpath, tmp_dir / fpath.name)

return Resource.from_directory(name="profiles", directory_path=tmp_dir)

def send_analysis(self, sample, outdir, metadata, quality):
payload = {"analysis_uid": self.analysis_uid}
payload.update(metadata)
Expand All @@ -355,6 +368,10 @@ def send_analysis(self, sample, outdir, metadata, quality):
if self.test_run:
task.add_payload('testcase', self.current_task.payload['testcase'])

if self.config.config.getboolean("drakrun", "attach_profiles", fallback=False):
self.log.info("Uploading profiles...")
task.add_payload("profiles", self.build_profile_payload())

self.log.info("Uploading artifacts...")
for resource in self.upload_artifacts(self.analysis_uid, outdir):
task.add_payload(resource.name, resource)
Expand All @@ -368,6 +385,8 @@ def get_profile_list() -> List[str]:
out = []

for profile in dll_file_list:
if profile.arg is None:
continue
if f"{profile.dest}.json" in files:
out.extend([profile.arg, os.path.join(PROFILE_DIR, f"{profile.dest}.json")])

Expand Down
6 changes: 3 additions & 3 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ def drakmon_vm():
apt_install(c, ["redis-server"])
apt_install(c, DRAKMON_DEPS)

for d in debs:
dpkg_install(c, d.name)

# add ISO image to make xen happy
c.run(
"genisoimage -o /root/SW_DVD5_Win_Pro_7w_SP1_64BIT_Polish_-2_MLF_X17-59386.ISO /dev/null"
)

for d in debs:
dpkg_install(c, d.name)

# add xen bridge
c.run("brctl addbr drak0")
c.run("systemctl enable drakrun@1")
Expand Down

0 comments on commit 3ac6cf2

Please sign in to comment.