diff --git a/analyzer/windows/modules/auxiliary/amsi.py b/analyzer/windows/modules/auxiliary/amsi.py deleted file mode 100644 index 9e20572065b..00000000000 --- a/analyzer/windows/modules/auxiliary/amsi.py +++ /dev/null @@ -1,1234 +0,0 @@ -#!/usr/bin/env python - -# This has been adapted from https://github.com/fireeye/pywintrace, consolidated in to -# 1 file, with unnecessary parts left out. Some changes have been made to limit the -# amount of code needed at the expense of some flexibility that is unnecessary for -# our purposes. - -######################################################################## -# Modifications Copyright 2024 Secureworks, Inc. -# -# Copyright 2017 FireEye Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -######################################################################## - -import ctypes as ct -import ctypes.wintypes as wt -import functools -import json -import logging -import sys -import threading -import uuid -from contextlib import suppress - -logger = logging.getLogger(__name__) - -# common.py - -MAX_UINT = (2**32) - 1 - - -def convert_bool_str(input_string): - """ - Helper to convert a string representation of a boolean to a real bool(tm). - """ - if input_string.lower() in ("1", "true"): - return True - return False - - -def rel_ptr_to_str(base, offset): - """ - Helper function to convert a relative offset to a string to the actual string. - """ - return ct.cast(rel_ptr_to_ptr(base, offset), ct.c_wchar_p).value - - -def rel_ptr_to_ptr(base, offset): - """ - Helper function to convert a relative offset to a void pointer. - """ - return ct.cast((ct.cast(base, ct.c_voidp).value + offset), ct.c_voidp) - - -class SYSTEMTIME(ct.Structure): - _fields_ = [ - ("wYear", wt.WORD), - ("wMonth", wt.WORD), - ("wDayOfWeek", wt.WORD), - ("wDay", wt.WORD), - ("wHour", wt.WORD), - ("wMinute", wt.WORD), - ("wSecond", wt.WORD), - ("wMilliseconds", wt.WORD), - ] - - -class TIME_ZONE_INFORMATION(ct.Structure): - _fields_ = [ - ("Bias", ct.c_long), - ("StandardName", ct.c_wchar * 32), - ("StandardDate", SYSTEMTIME), - ("StandardBias", ct.c_long), - ("DaylightName", ct.c_wchar * 32), - ("DaylightDate", SYSTEMTIME), - ("DaylightBias", ct.c_long), - ] - - -# in6addr.py - - -class in6_addr(ct.Structure): - _fields_ = [("Byte", ct.c_byte * 16)] - - -IN6_ADDR = in6_addr - -# GUID.py - - -class GUID(ct.Structure): - _fields_ = [ - ("Data1", ct.c_ulong), - ("Data2", ct.c_ushort), - ("Data3", ct.c_ushort), - ("Data4", ct.c_byte * 8), - ] - - def __init__(self, name): - ct.oledll.ole32.CLSIDFromString(name, ct.byref(self)) - - def __str__(self): - p = ct.c_wchar_p() - ct.oledll.ole32.StringFromCLSID(ct.byref(self), ct.byref(p)) - result = p.value - ct.windll.ole32.CoTaskMemFree(p) - return result - - -# wmistr.py - -WNODE_FLAG_TRACED_GUID = 0x00020000 - - -class WNODE_HEADER(ct.Structure): - _fields_ = [ - ("BufferSize", ct.c_ulong), - ("ProviderId", ct.c_ulong), - ("HistoricalContext", ct.c_uint64), - ("TimeStamp", wt.LARGE_INTEGER), - ("Guid", GUID), - ("ClientContext", ct.c_ulong), - ("Flags", ct.c_ulong), - ] - - -# evntprov.py - - -class EVENT_DESCRIPTOR(ct.Structure): - _fields_ = [ - ("Id", ct.c_ushort), - ("Version", ct.c_ubyte), - ("Channel", ct.c_ubyte), - ("Level", ct.c_ubyte), - ("Opcode", ct.c_ubyte), - ("Task", ct.c_ushort), - ("Keyword", ct.c_ulonglong), - ] - - -class EVENT_FILTER_DESCRIPTOR(ct.Structure): - _fields_ = [("Ptr", ct.c_ulonglong), ("Size", ct.c_ulong), ("Type", ct.c_ulong)] - - -# evntcons.py - -EVENT_HEADER_FLAG_EXTENDED_INFO = 0x01 -EVENT_HEADER_FLAG_32_BIT_HEADER = 0x20 -PROCESS_TRACE_MODE_REAL_TIME = 0x00000100 -PROCESS_TRACE_MODE_EVENT_RECORD = 0x10000000 - - -class ETW_BUFFER_CONTEXT(ct.Structure): - _fields_ = [ - ("ProcessorNumber", ct.c_ubyte), - ("Alignment", ct.c_ubyte), - ("LoggerId", ct.c_ushort), - ] - - -class EVENT_HEADER(ct.Structure): - _fields_ = [ - ("Size", ct.c_ushort), - ("HeaderType", ct.c_ushort), - ("Flags", ct.c_ushort), - ("EventProperty", ct.c_ushort), - ("ThreadId", ct.c_ulong), - ("ProcessId", ct.c_ulong), - ("TimeStamp", wt.LARGE_INTEGER), - ("ProviderId", GUID), - ("EventDescriptor", EVENT_DESCRIPTOR), - ("KernelTime", ct.c_ulong), - ("UserTime", ct.c_ulong), - ("ActivityId", GUID), - ] - - -class EVENT_HEADER_EXTENDED_DATA_ITEM(ct.Structure): - _fields_ = [ - ("Reserved1", ct.c_ushort), - ("ExtType", ct.c_ushort), - ("Linkage", ct.c_ushort), # struct{USHORT :1, USHORT :15} - ("DataSize", ct.c_ushort), - ("DataPtr", ct.c_ulonglong), - ] - - -class EVENT_RECORD(ct.Structure): - _fields_ = [ - ("EventHeader", EVENT_HEADER), - ("BufferContext", ETW_BUFFER_CONTEXT), - ("ExtendedDataCount", ct.c_ushort), - ("UserDataLength", ct.c_ushort), - ("ExtendedData", ct.POINTER(EVENT_HEADER_EXTENDED_DATA_ITEM)), - ("UserData", ct.c_void_p), - ("UserContext", ct.c_void_p), - ] - - -# evntrace.py - -EVENT_CONTROL_CODE_DISABLE_PROVIDER = 0 -EVENT_CONTROL_CODE_ENABLE_PROVIDER = 1 -EVENT_TRACE_CONTROL_STOP = 1 -EVENT_TRACE_REAL_TIME_MODE = 0x00000100 # Real time mode on -TRACEHANDLE = ct.c_ulonglong -INVALID_PROCESSTRACE_HANDLE = TRACEHANDLE(-1) -TRACE_LEVEL_INFORMATION = 4 - - -class ENABLE_TRACE_PARAMETERS(ct.Structure): - _fields_ = [ - ("Version", ct.c_ulong), - ("EnableProperty", ct.c_ulong), - ("ControlFlags", ct.c_ulong), - ("SourceId", GUID), - ("EnableFilterDesc", ct.POINTER(EVENT_FILTER_DESCRIPTOR)), - ("FilterDescCount", ct.c_ulong), - ] - - -class EVENT_TRACE_HEADER_CLASS(ct.Structure): - _fields_ = [("Type", ct.c_ubyte), ("Level", ct.c_ubyte), ("Version", ct.c_uint16)] - - -class EVENT_TRACE_HEADER(ct.Structure): - _fields_ = [ - ("Size", ct.c_ushort), - ("HeaderType", ct.c_ubyte), - ("MarkerFlags", ct.c_ubyte), - ("Class", EVENT_TRACE_HEADER_CLASS), - ("ThreadId", ct.c_ulong), - ("ProcessId", ct.c_ulong), - ("TimeStamp", wt.LARGE_INTEGER), - ("Guid", GUID), - ("ClientContext", ct.c_ulong), - ("Flags", ct.c_ulong), - ] - - -class EVENT_TRACE(ct.Structure): - _fields_ = [ - ("Header", EVENT_TRACE_HEADER), - ("InstanceId", ct.c_ulong), - ("ParentInstanceId", ct.c_ulong), - ("ParentGuid", GUID), - ("MofData", ct.c_void_p), - ("MofLength", ct.c_ulong), - ("ClientContext", ct.c_ulong), - ] - - -class TRACE_LOGFILE_HEADER(ct.Structure): - _fields_ = [ - ("BufferSize", ct.c_ulong), - ("MajorVersion", ct.c_byte), - ("MinorVersion", ct.c_byte), - ("SubVersion", ct.c_byte), - ("SubMinorVersion", ct.c_byte), - ("ProviderVersion", ct.c_ulong), - ("NumberOfProcessors", ct.c_ulong), - ("EndTime", wt.LARGE_INTEGER), - ("TimerResolution", ct.c_ulong), - ("MaximumFileSize", ct.c_ulong), - ("LogFileMode", ct.c_ulong), - ("BuffersWritten", ct.c_ulong), - ("StartBuffers", ct.c_ulong), - ("PointerSize", ct.c_ulong), - ("EventsLost", ct.c_ulong), - ("CpuSpeedInMHz", ct.c_ulong), - ("LoggerName", ct.c_wchar_p), - ("LogFileName", ct.c_wchar_p), - ("TimeZone", TIME_ZONE_INFORMATION), - ("BootTime", wt.LARGE_INTEGER), - ("PerfFreq", wt.LARGE_INTEGER), - ("StartTime", wt.LARGE_INTEGER), - ("ReservedFlags", ct.c_ulong), - ("BuffersLost", ct.c_ulong), - ] - - -# This must be "forward declared", because of the callback type below, -# which is contained in the ct.Structure. -class EVENT_TRACE_LOGFILE(ct.Structure): - pass - - -# The type for event trace callbacks. -EVENT_RECORD_CALLBACK = ct.WINFUNCTYPE(None, ct.POINTER(EVENT_RECORD)) -EVENT_TRACE_BUFFER_CALLBACK = ct.WINFUNCTYPE(ct.c_ulong, ct.POINTER(EVENT_TRACE_LOGFILE)) - -EVENT_TRACE_LOGFILE._fields_ = [ - ("LogFileName", ct.c_wchar_p), - ("LoggerName", ct.c_wchar_p), - ("CurrentTime", ct.c_longlong), - ("BuffersRead", ct.c_ulong), - ("ProcessTraceMode", ct.c_ulong), - ("CurrentEvent", EVENT_TRACE), - ("LogfileHeader", TRACE_LOGFILE_HEADER), - ("BufferCallback", EVENT_TRACE_BUFFER_CALLBACK), - ("BufferSize", ct.c_ulong), - ("Filled", ct.c_ulong), - ("EventsLost", ct.c_ulong), - ("EventRecordCallback", EVENT_RECORD_CALLBACK), - ("IsKernelTrace", ct.c_ulong), - ("Context", ct.c_void_p), -] - - -class EVENT_TRACE_PROPERTIES(ct.Structure): - _fields_ = [ - ("Wnode", WNODE_HEADER), - ("BufferSize", ct.c_ulong), - ("MinimumBuffers", ct.c_ulong), - ("MaximumBuffers", ct.c_ulong), - ("MaximumFileSize", ct.c_ulong), - ("LogFileMode", ct.c_ulong), - ("FlushTimer", ct.c_ulong), - ("EnableFlags", ct.c_ulong), - ("AgeLimit", ct.c_ulong), - ("NumberOfBuffers", ct.c_ulong), - ("FreeBuffers", ct.c_ulong), - ("EventsLost", ct.c_ulong), - ("BuffersWritten", ct.c_ulong), - ("LogBuffersLost", ct.c_ulong), - ("RealTimeBuffersLost", ct.c_ulong), - ("LoggerThreadId", wt.HANDLE), - ("LogFileNameOffset", ct.c_ulong), - ("LoggerNameOffset", ct.c_ulong), - ] - - -StartTraceW = ct.windll.advapi32.StartTraceW -StartTraceW.argtypes = [ - ct.POINTER(TRACEHANDLE), - ct.c_wchar_p, - ct.POINTER(EVENT_TRACE_PROPERTIES), -] -StartTraceW.restype = ct.c_ulong - -EnableTraceEx2 = ct.windll.advapi32.EnableTraceEx2 -EnableTraceEx2.argtypes = [ - TRACEHANDLE, - ct.POINTER(GUID), - ct.c_ulong, - ct.c_char, - ct.c_ulonglong, - ct.c_ulonglong, - ct.c_ulong, - ct.POINTER(ENABLE_TRACE_PARAMETERS), -] -EnableTraceEx2.restype = ct.c_ulong - -OpenTraceW = ct.windll.advapi32.OpenTraceW -OpenTraceW.argtypes = [ct.POINTER(EVENT_TRACE_LOGFILE)] -OpenTraceW.restype = TRACEHANDLE - -ControlTraceW = ct.windll.advapi32.ControlTraceW -ControlTraceW.argtypes = [ - TRACEHANDLE, - ct.c_wchar_p, - ct.POINTER(EVENT_TRACE_PROPERTIES), - ct.c_ulong, -] -ControlTraceW.restype = ct.c_ulong - -ProcessTrace = ct.windll.advapi32.ProcessTrace -ProcessTrace.argtypes = [ - ct.POINTER(TRACEHANDLE), - ct.c_ulong, - ct.POINTER(wt.FILETIME), - ct.POINTER(wt.FILETIME), -] -ProcessTrace.restype = ct.c_ulong - -CloseTrace = ct.windll.advapi32.CloseTrace -CloseTrace.argtypes = [TRACEHANDLE] -CloseTrace.restype = ct.c_ulong - -# tdh.py - -DECODING_SOURCE = ct.c_uint -ERROR_SUCCESS = 0x0 -ERROR_ALREADY_EXISTS = 0xB7 -ERROR_INSUFFICIENT_BUFFER = 0x7A -ERROR_NOT_FOUND = 0x490 -MAP_FLAGS = ct.c_uint -PROPERTY_FLAGS = ct.c_uint -TDH_CONTEXT_TYPE = ct.c_uint - -TDH_INTYPE_NULL = 0 -TDH_INTYPE_UNICODESTRING = 1 -TDH_INTYPE_ANSISTRING = 2 -TDH_INTYPE_INT8 = 3 -TDH_INTYPE_UINT8 = 4 -TDH_INTYPE_INT16 = 5 -TDH_INTYPE_UINT16 = 6 -TDH_INTYPE_INT32 = 7 -TDH_INTYPE_UINT32 = 8 -TDH_INTYPE_INT64 = 9 -TDH_INTYPE_UINT64 = 10 -TDH_INTYPE_FLOAT = 11 -TDH_INTYPE_DOUBLE = 12 -TDH_INTYPE_BOOLEAN = 13 -TDH_INTYPE_BINARY = 14 -TDH_INTYPE_GUID = 15 -TDH_INTYPE_POINTER = 16 -TDH_INTYPE_FILETIME = 17 -TDH_INTYPE_SYSTEMTIME = 18 -TDH_INTYPE_SID = 19 -TDH_INTYPE_HEXINT32 = 20 -TDH_INTYPE_HEXINT64 = 21 -TDH_INTYPE_COUNTEDSTRING = 300 -TDH_INTYPE_COUNTEDANSISTRING = 301 -TDH_INTYPE_REVERSEDCOUNTEDSTRING = 302 -TDH_INTYPE_REVERSEDCOUNTEDANSISTRING = 303 -TDH_INTYPE_NONNULLTERMINATEDSTRING = 304 -TDH_INTYPE_NONNULLTERMINATEDANSISTRING = 305 -TDH_INTYPE_UNICODECHAR = 306 -TDH_INTYPE_ANSICHAR = 307 -TDH_INTYPE_SIZET = 308 -TDH_INTYPE_HEXDUMP = 309 -TDH_INTYPE_WBEMSID = 310 - -TDH_OUTTYPE_NULL = 0 -TDH_OUTTYPE_STRING = 1 -TDH_OUTTYPE_DATETIME = 2 -TDH_OUTTYPE_BYTE = 3 -TDH_OUTTYPE_UNSIGNEDBYTE = 4 -TDH_OUTTYPE_SHORT = 5 -TDH_OUTTYPE_UNSIGNEDSHORT = 6 -TDH_OUTTYPE_INT = 7 -TDH_OUTTYPE_UNSIGNEDINT = 8 -TDH_OUTTYPE_LONG = 9 -TDH_OUTTYPE_UNSIGNEDLONG = 10 -TDH_OUTTYPE_FLOAT = 11 -TDH_OUTTYPE_DOUBLE = 12 -TDH_OUTTYPE_BOOLEAN = 13 -TDH_OUTTYPE_GUID = 14 -TDH_OUTTYPE_HEXBINARY = 15 -TDH_OUTTYPE_HEXINT8 = 16 -TDH_OUTTYPE_HEXINT16 = 17 -TDH_OUTTYPE_HEXINT32 = 18 -TDH_OUTTYPE_HEXINT64 = 19 -TDH_OUTTYPE_PID = 20 -TDH_OUTTYPE_TID = 21 -TDH_OUTTYPE_PORT = 22 -TDH_OUTTYPE_IPV4 = 23 -TDH_OUTTYPE_IPV6 = 24 -TDH_OUTTYPE_SOCKETADDRESS = 25 -TDH_OUTTYPE_CIMDATETIME = 26 -TDH_OUTTYPE_ETWTIME = 27 -TDH_OUTTYPE_XML = 28 -TDH_OUTTYPE_ERRORCODE = 29 -TDH_OUTTYPE_WIN32ERROR = 30 -TDH_OUTTYPE_NTSTATUS = 31 -TDH_OUTTYPE_HRESULT = 32 -TDH_OUTTYPE_CULTURE_INSENSITIVE_DATETIME = 33 -TDH_OUTTYPE_JSON = 34 -TDH_OUTTYPE_REDUCEDSTRING = 300 -TDH_OUTTYPE_NOPRIN = 301 - -PropertyStruct = 0x1 -PropertyParamLength = 0x2 - -TDH_CONVERTER_LOOKUP = { - TDH_OUTTYPE_UNSIGNEDBYTE: int, - TDH_OUTTYPE_INT: int, - TDH_OUTTYPE_UNSIGNEDINT: int, - TDH_OUTTYPE_LONG: int, - TDH_OUTTYPE_UNSIGNEDLONG: int, - TDH_OUTTYPE_FLOAT: float, - TDH_OUTTYPE_DOUBLE: float, - TDH_OUTTYPE_BOOLEAN: convert_bool_str, -} - - -class EVENT_MAP_ENTRY(ct.Structure): - _fields_ = [("OutputOffset", ct.c_ulong), ("InputOffset", ct.c_ulong)] - - -class EVENT_MAP_INFO(ct.Structure): - _fields_ = [ - ("NameOffset", ct.c_ulong), - ("Flag", MAP_FLAGS), - ("EntryCount", ct.c_ulong), - ("FormatStringOffset", ct.c_ulong), - ("MapEntryArray", EVENT_MAP_ENTRY * 0), - ] - - -class PROPERTY_DATA_DESCRIPTOR(ct.Structure): - _fields_ = [ - ("PropertyName", ct.c_ulonglong), - ("ArrayIndex", ct.c_ulong), - ("Reserved", ct.c_ulong), - ] - - -class nonStructType(ct.Structure): - _fields_ = [ - ("InType", ct.c_ushort), - ("OutType", ct.c_ushort), - ("MapNameOffset", ct.c_ulong), - ] - - -class structType(ct.Structure): - _fields_ = [ - ("StructStartIndex", wt.USHORT), - ("NumOfStructMembers", wt.USHORT), - ("padding", wt.ULONG), - ] - - -class epi_u1(ct.Union): - _fields_ = [("nonStructType", nonStructType), ("structType", structType)] - - -class epi_u2(ct.Union): - _fields_ = [("count", wt.USHORT), ("countPropertyIndex", wt.USHORT)] - - -class epi_u3(ct.Union): - _fields_ = [("length", wt.USHORT), ("lengthPropertyIndex", wt.USHORT)] - - -class epi_u4(ct.Union): - _fields_ = [("Reserved", wt.ULONG), ("Tags", wt.ULONG)] - - -class EVENT_PROPERTY_INFO(ct.Structure): - _fields_ = [ - ("Flags", PROPERTY_FLAGS), - ("NameOffset", ct.c_ulong), - ("epi_u1", epi_u1), - ("epi_u2", epi_u2), - ("epi_u3", epi_u3), - ("epi_u4", epi_u4), - ] - - -class TDH_CONTEXT(ct.Structure): - _fields_ = [ - ("ParameterValue", ct.c_ulonglong), - ("ParameterType", TDH_CONTEXT_TYPE), - ("ParameterSize", ct.c_ulong), - ] - - -class TRACE_EVENT_INFO(ct.Structure): - _fields_ = [ - ("ProviderGuid", GUID), - ("EventGuid", GUID), - ("EventDescriptor", EVENT_DESCRIPTOR), - ("DecodingSource", DECODING_SOURCE), - ("ProviderNameOffset", ct.c_ulong), - ("LevelNameOffset", ct.c_ulong), - ("ChannelNameOffset", ct.c_ulong), - ("KeywordsNameOffset", ct.c_ulong), - ("TaskNameOffset", ct.c_ulong), - ("OpcodeNameOffset", ct.c_ulong), - ("EventMessageOffset", ct.c_ulong), - ("ProviderMessageOffset", ct.c_ulong), - ("BinaryXMLOffset", ct.c_ulong), - ("BinaryXMLSize", ct.c_ulong), - ("ActivityIDNameOffset", ct.c_ulong), - ("RelatedActivityIDNameOffset", ct.c_ulong), - ("PropertyCount", ct.c_ulong), - ("TopLevelPropertyCount", ct.c_ulong), - ("Flags", ct.c_ulong), - ("EventPropertyInfoArray", EVENT_PROPERTY_INFO * 0), - ] - - -TdhFormatProperty = ct.windll.Tdh.TdhFormatProperty -TdhFormatProperty.argtypes = [ - ct.POINTER(TRACE_EVENT_INFO), - ct.POINTER(EVENT_MAP_INFO), - ct.c_ulong, - ct.c_ushort, - ct.c_ushort, - ct.c_ushort, - ct.c_ushort, - ct.POINTER(ct.c_byte), - ct.POINTER(ct.c_ulong), - ct.c_wchar_p, - ct.POINTER(ct.c_ushort), -] -TdhFormatProperty.restype = ct.c_ulong - -TdhGetEventInformation = ct.windll.Tdh.TdhGetEventInformation -TdhGetEventInformation.argtypes = [ - ct.POINTER(EVENT_RECORD), - ct.c_ulong, - ct.POINTER(TDH_CONTEXT), - ct.POINTER(TRACE_EVENT_INFO), - ct.POINTER(ct.c_ulong), -] -TdhGetEventInformation.restype = ct.c_ulong - -TdhGetEventMapInformation = ct.windll.Tdh.TdhGetEventMapInformation -TdhGetEventMapInformation.argtypes = [ - ct.POINTER(EVENT_RECORD), - wt.LPWSTR, - ct.POINTER(EVENT_MAP_INFO), - ct.POINTER(ct.c_ulong), -] -TdhGetEventMapInformation.restype = ct.c_ulong - -TdhGetPropertySize = ct.windll.Tdh.TdhGetPropertySize -TdhGetPropertySize.argtypes = [ - ct.POINTER(EVENT_RECORD), - ct.c_ulong, - ct.POINTER(TDH_CONTEXT), - ct.c_ulong, - ct.POINTER(PROPERTY_DATA_DESCRIPTOR), - ct.POINTER(ct.c_ulong), -] -TdhGetPropertySize.restype = ct.c_ulong - -TdhGetProperty = ct.windll.Tdh.TdhGetProperty -TdhGetProperty.argtypes = [ - ct.POINTER(EVENT_RECORD), - ct.c_ulong, - ct.POINTER(TDH_CONTEXT), - ct.c_ulong, - ct.POINTER(PROPERTY_DATA_DESCRIPTOR), - ct.c_ulong, - ct.POINTER(ct.c_byte), -] -TdhGetProperty.restype = ct.c_ulong - -# etw.py - - -class ProviderInfo: - def __init__(self, name, guid): - self.name = name - self.guid = guid - self.level = TRACE_LEVEL_INFORMATION - self.any_bitmask = 0 - self.all_bitmask = 0 - - -class EventProvider: - def __init__(self, session_name, session_properties, providers): - self.session_name = session_name - self.session_properties = session_properties - self.providers = providers - self.session_handle = TRACEHANDLE() - - def start(self): - status = StartTraceW( - ct.byref(self.session_handle), - self.session_name, - self.session_properties.get(), - ) - if status != ERROR_SUCCESS: - raise ct.WinError(status) - - for provider in self.providers: - status = EnableTraceEx2( - self.session_handle, - ct.byref(provider.guid), - EVENT_CONTROL_CODE_ENABLE_PROVIDER, - provider.level, - provider.any_bitmask, - provider.all_bitmask, - 0, - None, - ) - if status != ERROR_SUCCESS: - raise ct.WinError(status) - - def stop(self): - """ - Wraps the necessary processes needed for stopping an ETW provider session. - - :return: Does not return anything - """ - # don't stop if we don't have a handle, or it's the kernel trace and we started it ourself - if self.session_handle.value == 0: - return - - for provider in self.providers: - status = EnableTraceEx2( - self.session_handle, - ct.byref(provider.guid), - EVENT_CONTROL_CODE_DISABLE_PROVIDER, - provider.level, - provider.any_bitmask, - provider.all_bitmask, - 0, - None, - ) - if status != ERROR_SUCCESS: - raise ct.WinError(status) - - status = ControlTraceW( - self.session_handle, - self.session_name, - self.session_properties.get(), - EVENT_TRACE_CONTROL_STOP, - ) - if status != ERROR_SUCCESS: - raise ct.WinError(status) - - CloseTrace(self.session_handle) - - -class EventConsumer: - """ - Wraps all interactions with Event Tracing for Windows (ETW) event consumers. This includes - starting and stopping the consumer. Additionally, each consumer begins processing events in - a separate thread and uses a callback to process any events it receives in this thread -- those - methods are implemented here as well. - - N.B. If using this class, do not call start() and stop() directly. Only use through via ctxmgr - """ - - def __init__(self, logger_name, event_callback=None): - self.trace_handle = None - self.process_thread = None - self.logger_name = logger_name - self.end_capture = threading.Event() - self.event_callback = event_callback - self.vfield_length = None - self.index = 0 - - self.trace_logfile = EVENT_TRACE_LOGFILE() - self.trace_logfile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD - self.trace_logfile.LoggerName = logger_name - self.trace_logfile.EventRecordCallback = EVENT_RECORD_CALLBACK(self._processEvent) - - def start(self): - """ - Starts a trace consumer. - - :return: Returns True on Success or False on Failure - """ - self.trace_handle = OpenTraceW(ct.byref(self.trace_logfile)) - if self.trace_handle == INVALID_PROCESSTRACE_HANDLE: - raise ct.WinError() - - # For whatever reason, the restype is ignored - self.trace_handle = TRACEHANDLE(self.trace_handle) - self.process_thread = threading.Thread(target=self._run, args=(self.trace_handle, self.end_capture)) - self.process_thread.daemon = True - self.process_thread.start() - - def stop(self): - """ - Stops a trace consumer. - - :return: Returns True on Success or False on Failure - """ - # Signal to the thread that we are reading to stop processing events. - self.end_capture.set() - - # Call CloseTrace to cause ProcessTrace to return (unblock) - CloseTrace(self.trace_handle) - - # If ProcessThread is actively parsing an event, we want to give it a chance to finish - # before pulling the rug out from underneath it. - self.process_thread.join() - - @staticmethod - def _run(trace_handle, end_capture): - """ - Because ProcessTrace() blocks, this function is used to spin off new threads. - - :param trace_handle: The handle for the trace consumer that we want to begin processing. - :param end_capture: A callback function which determines what should be done with the results. - :return: Does not return a value. - """ - while True: - if ERROR_SUCCESS != ProcessTrace(ct.byref(trace_handle), 1, None, None): - end_capture.set() - - if end_capture.is_set(): - break - - @staticmethod - def _getEventInformation(record): - """ - Initially we are handed an EVENT_RECORD structure. While this structure technically contains - all of the information necessary, TdhGetEventInformation parses the structure and simplifies it - so we can more effectively parse and handle the various fields. - - :param record: The EventRecord structure for the event we are parsing - :return: Returns a pointer to a TRACE_EVENT_INFO structure or None on error. - """ - info = ct.POINTER(TRACE_EVENT_INFO)() - buffer_size = wt.DWORD() - - # Call TdhGetEventInformation once to get the required buffer size and again to actually populate the structure. - status = TdhGetEventInformation(record, 0, None, None, ct.byref(buffer_size)) - if ERROR_INSUFFICIENT_BUFFER == status: - info = ct.cast((ct.c_byte * buffer_size.value)(), ct.POINTER(TRACE_EVENT_INFO)) - status = TdhGetEventInformation(record, 0, None, info, ct.byref(buffer_size)) - - if ERROR_SUCCESS != status: - raise ct.WinError(status) - - return info - - @staticmethod - def _getMapInfo(record, info, event_property): - """ - When parsing a field in the event property structure, there may be a mapping between a given - name and the structure it represents. If it exists, we retrieve that mapping here. - - Because this may legitimately return a NULL value we return a tuple containing the success or - failure status as well as either None (NULL) or an EVENT_MAP_INFO pointer. - - :param record: The EventRecord structure for the event we are parsing - :param info: The TraceEventInfo structure for the event we are parsing - :param event_property: The EVENT_PROPERTY_INFO structure for the TopLevelProperty of the event we are parsing - :return: A tuple of the map_info structure and boolean indicating whether we succeeded or not - """ - map_name = rel_ptr_to_str(info, event_property.epi_u1.nonStructType.MapNameOffset) - map_size = wt.DWORD() - map_info = ct.POINTER(EVENT_MAP_INFO)() - - status = TdhGetEventMapInformation(record, map_name, None, ct.byref(map_size)) - if ERROR_INSUFFICIENT_BUFFER == status: - map_info = ct.cast((ct.c_char * map_size.value)(), ct.POINTER(EVENT_MAP_INFO)) - status = TdhGetEventMapInformation(record, map_name, map_info, ct.byref(map_size)) - - if ERROR_SUCCESS == status: - return map_info, True - - # ERROR_NOT_FOUND is actually a perfectly acceptable status - if ERROR_NOT_FOUND == status: - return None, True - - # We actually failed. - raise ct.WinError() - - @staticmethod - def _getPropertyLength(record, info, event_property): - """ - Each property encountered when parsing the top level property has an associated length. If the - length is available, retrieve it here. In some cases, the length is 0. This can signify that - we are dealing with a variable length field such as a structure, an IPV6 data, or a string. - - :param record: The EventRecord structure for the event we are parsing - :param info: The TraceEventInfo structure for the event we are parsing - :param event_property: The EVENT_PROPERTY_INFO structure for the TopLevelProperty of the event we are parsing - :return: Returns the length of the property as a c_ulong() or None on error - """ - flags = event_property.Flags - - if flags & PropertyParamLength: - data_descriptor = PROPERTY_DATA_DESCRIPTOR() - event_property_array = ct.cast(info.contents.EventPropertyInfoArray, ct.POINTER(EVENT_PROPERTY_INFO)) - j = wt.DWORD(event_property.epi_u3.length) - property_size = ct.c_ulong() - length = wt.DWORD() - - # Setup the PROPERTY_DATA_DESCRIPTOR structure - data_descriptor.PropertyName = ct.cast(info, ct.c_voidp).value + event_property_array[j.value].NameOffset - data_descriptor.ArrayIndex = MAX_UINT - - status = TdhGetPropertySize(record, 0, None, 1, ct.byref(data_descriptor), ct.byref(property_size)) - if ERROR_SUCCESS != status: - raise ct.WinError(status) - - status = TdhGetProperty( - record, - 0, - None, - 1, - ct.byref(data_descriptor), - property_size, - ct.cast(ct.byref(length), ct.POINTER(ct.c_byte)), - ) - if ERROR_SUCCESS != status: - raise ct.WinError(status) - return length.value - - in_type = event_property.epi_u1.nonStructType.InType - out_type = event_property.epi_u1.nonStructType.OutType - - # This is a special case in which the input and output types dictate the size - if (in_type == TDH_INTYPE_BINARY) and (out_type == TDH_OUTTYPE_IPV6): - return ct.sizeof(IN6_ADDR) - - return event_property.epi_u3.length - - def _unpackSimpleType(self, record, info, event_property): - """ - This method handles dumping all simple types of data (i.e., non-struct types). - - :param record: The EventRecord structure for the event we are parsing - :param info: The TraceEventInfo structure for the event we are parsing - :param event_property: The EVENT_PROPERTY_INFO structure for the TopLevelProperty of the event we are parsing - :return: Returns a key-value pair as a dictionary. If we fail, the dictionary is {} - """ - # Get the EVENT_MAP_INFO, if it is present. - map_info, success = self._getMapInfo(record, info, event_property) - if not success: - return {} - - # Get the length of the value of the property we are dealing with. - property_length = self._getPropertyLength(record, info, event_property) - if property_length is None: - return {} - # The version of the Python interpreter may be different than the system architecture. - if record.contents.EventHeader.Flags & EVENT_HEADER_FLAG_32_BIT_HEADER: - ptr_size = 4 - else: - ptr_size = 8 - - name_field = rel_ptr_to_str(info, event_property.NameOffset) - if property_length == 0 and self.vfield_length is not None: - if self.vfield_length == 0: - self.vfield_length = None - return {name_field: None} - - # If vfield_length isn't 0, we should be able to parse the property. - property_length = self.vfield_length - - # After calling the TdhFormatProperty function, use the UserDataConsumed parameter value to set the new values - # of the UserData and UserDataLength parameters (Subtract UserDataConsumed from UserDataLength and use - # UserDataLength to increment the UserData pointer). - - # All of the variables needed to actually use TdhFormatProperty retrieve the value - user_data = record.contents.UserData + self.index - user_data_remaining = record.contents.UserDataLength - self.index - - # if there is no data remaining then return - if user_data_remaining <= 0: - logger.warning("No more user data left, returning none for field %s", str(name_field)) - return {name_field: None} - - in_type = event_property.epi_u1.nonStructType.InType - out_type = event_property.epi_u1.nonStructType.OutType - formatted_data_size = wt.DWORD() - formatted_data = wt.LPWSTR() - user_data_consumed = ct.c_ushort() - - status = TdhFormatProperty( - info, - map_info, - ptr_size, - in_type, - out_type, - ct.c_ushort(property_length), - user_data_remaining, - ct.cast(user_data, ct.POINTER(ct.c_byte)), - ct.byref(formatted_data_size), - None, - ct.byref(user_data_consumed), - ) - - if status == ERROR_INSUFFICIENT_BUFFER: - formatted_data = ct.cast((ct.c_char * formatted_data_size.value)(), wt.LPWSTR) - status = TdhFormatProperty( - info, - map_info, - ptr_size, - in_type, - out_type, - ct.c_ushort(property_length), - user_data_remaining, - ct.cast(user_data, ct.POINTER(ct.c_byte)), - ct.byref(formatted_data_size), - formatted_data, - ct.byref(user_data_consumed), - ) - - if status != ERROR_SUCCESS: - # We can handle this error and still capture the data. - logger.warning("Failed to get data field data for %s, incrementing by reported size", str(name_field)) - self.index += property_length - return {name_field: None} - - # Increment where we are in the user data segment that we are parsing. - self.index += user_data_consumed.value - - if name_field.lower().endswith("length"): - try: - self.vfield_length = int(formatted_data.value, 10) - except ValueError: - logger.warning("Setting vfield_length to None") - self.vfield_length = None - - data = formatted_data.value - # Convert the formatted data if necessary - if isinstance(data, str): - if out_type >= TDH_OUTTYPE_BYTE and out_type <= TDH_OUTTYPE_UNSIGNEDLONG: - with suppress(Exception): - data = int(data) - - if out_type in TDH_CONVERTER_LOOKUP and type(data) is TDH_CONVERTER_LOOKUP[out_type]: - data = TDH_CONVERTER_LOOKUP[out_type](data) - - return {name_field: data} - - def _unpackComplexType(self, record, info, event_property): - """ - A complex type (e.g., a structure with sub-properties) can only contain simple types. Loop over all - sub-properties and dump the property name and value. - - :param record: The EventRecord structure for the event we are parsing - :param info: The TraceEventInfo structure for the event we are parsing - :param event_property: The EVENT_PROPERTY_INFO structure for the TopLevelProperty of the event we are parsing - :return: A dictionary of the property and value for the event we are parsing - """ - out = {} - - array_size = self._getArraySize(record, info, event_property) - if array_size is None: - return {} - - for _ in range(array_size): - start_index = event_property.epi_u1.structType.StructStartIndex - last_member = start_index + event_property.epi_u1.structType.NumOfStructMembers - - for j in range(start_index, last_member): - # Because we are no longer dealing with the TopLevelProperty, we need to get the event_property_array - # again so we can get the EVENT_PROPERTY_INFO structure of the sub-property we are currently parsing. - event_property_array = ct.cast( - info.contents.EventPropertyInfoArray, - ct.POINTER(EVENT_PROPERTY_INFO), - ) - - key, value = self._unpackSimpleType(record, info, event_property_array[j]) - if key is None and value is None: - break - - out[key] = value - - return out - - def _processEvent(self, record): - """ - This is a callback function that fires whenever an event needs handling. It iterates through the structure to - parse the properties of each event. If a user defined callback is specified it then passes the parsed data to - it. - - - :param record: The EventRecord structure for the event we are parsing - :return: Nothing - """ - parsed_data = {} - - event_id = record.contents.EventHeader.EventDescriptor.Id - # set task name to provider guid for the time being - task_name = str(record.contents.EventHeader.ProviderId) - - # add all header fields from EVENT_HEADER structure - # https://msdn.microsoft.com/en-us/library/windows/desktop/aa363759(v=vs.85).aspx - out = { - "EventHeader": { - "Size": record.contents.EventHeader.Size, - "HeaderType": record.contents.EventHeader.HeaderType, - "Flags": record.contents.EventHeader.Flags, - "EventProperty": record.contents.EventHeader.EventProperty, - "ThreadId": record.contents.EventHeader.ThreadId, - "ProcessId": record.contents.EventHeader.ProcessId, - "TimeStamp": record.contents.EventHeader.TimeStamp, - "ProviderId": task_name, - "EventDescriptor": { - "Id": event_id, - "Version": record.contents.EventHeader.EventDescriptor.Version, - "Channel": record.contents.EventHeader.EventDescriptor.Channel, - "Level": record.contents.EventHeader.EventDescriptor.Level, - "Opcode": record.contents.EventHeader.EventDescriptor.Opcode, - "Task": record.contents.EventHeader.EventDescriptor.Task, - "Keyword": record.contents.EventHeader.EventDescriptor.Keyword, - }, - "KernelTime": record.contents.EventHeader.KernelTime, - "UserTime": record.contents.EventHeader.UserTime, - "ActivityId": str(record.contents.EventHeader.ActivityId), - }, - "Task Name": task_name, - } - - try: - info = self._getEventInformation(record) - - # Some events do not have an associated task_name value. In this case, we should use the provider - # name instead. - if info.contents.TaskNameOffset == 0: - task_name = rel_ptr_to_str(info, info.contents.ProviderNameOffset) - else: - task_name = rel_ptr_to_str(info, info.contents.TaskNameOffset) - - task_name = task_name.strip().upper() - - # Add a description for the event, if present - if info.contents.EventMessageOffset: - description = rel_ptr_to_str(info, info.contents.EventMessageOffset) - else: - description = "" - - user_data = record.contents.UserData - if user_data is None: - user_data = 0 - - end_of_user_data = user_data + record.contents.UserDataLength - self.index = 0 - self.vfield_length = None - property_array = ct.cast(info.contents.EventPropertyInfoArray, ct.POINTER(EVENT_PROPERTY_INFO)) - - for i in range(info.contents.TopLevelPropertyCount): - # If the user_data is the same value as the end_of_user_data, we are ending with a 0-length - # field. Though not documented, this is completely valid. - if user_data == end_of_user_data: - break - - # Determine whether we are processing a simple type or a complex type and act accordingly - if property_array[i].Flags & PropertyStruct: - field = self._unpackComplexType(record, info, property_array[i]) - else: - field = self._unpackSimpleType(record, info, property_array[i]) - - parsed_data.update(field) - - # Add the description field in - parsed_data["Description"] = description - parsed_data["Task Name"] = task_name - # Add ExtendedData if any - if record.contents.EventHeader.Flags & EVENT_HEADER_FLAG_EXTENDED_INFO: - parsed_data["EventExtendedData"] = self._parseExtendedData(record) - except Exception as e: - logger.warning("Unable to parse event: %s", str(e)) - - try: - out.update(parsed_data) - # Call the user's specified callback function - if self.event_callback: - self.event_callback(out) - except Exception as e: - logger.exception("Exception during callback: %s", str(e)) - - -class TraceProperties: - def __init__(self): - buf_size = ct.sizeof(EVENT_TRACE_PROPERTIES) + 2 * ct.sizeof(ct.c_wchar) * 1024 - self._buf = (ct.c_char * buf_size)() - self._props = ct.cast(ct.pointer(self._buf), ct.POINTER(EVENT_TRACE_PROPERTIES)) - self._props.contents.BufferSize = 1024 - self._props.contents.Wnode.Flags = WNODE_FLAG_TRACED_GUID - self._props.contents.LogFileMode = EVENT_TRACE_REAL_TIME_MODE - self._props.contents.Wnode.BufferSize = buf_size - self._props.contents.LoggerNameOffset = ct.sizeof(EVENT_TRACE_PROPERTIES) - - def get(self): - return self._props - - -class AMSI: - def __init__(self, event_callback=None): - try: - self.providers = [ProviderInfo("AMSI", GUID("{2A576B87-09A7-520E-C21A-4942F0271D67}"))] - except OSError as err: - raise OSError("AMSI not supported on this platform") from err - self.provider = None - self.properties = TraceProperties() - self.session_name = str(uuid.uuid4()) - self.running = False - self.event_callback = event_callback - self.trace_logfile = None - - def __enter__(self): - self.start() - return self - - def __exit__(self, exc, ex, tb): - self.stop() - - def start(self): - if self.provider is None: - self.provider = EventProvider(self.session_name, self.properties, self.providers) - - if not self.running: - self.running = True - try: - self.provider.start() - except OSError as err: - if err.winerror != ERROR_ALREADY_EXISTS: - raise err - - # Start the consumer - self.consumer = EventConsumer( - self.session_name, - self.event_callback, - ) - self.consumer.start() - - def stop(self): - """ - Stops the current consumer and provider. - - :return: Does not return anything. - """ - - if self.provider: - self.running = False - self.provider.stop() - self.consumer.stop() - - -def jsonldump(obj, fp): - """Write each event object on its own line.""" - json.dump(obj, fp) - fp.write("\n") - - -def main(): - with AMSI(event_callback=functools.partial(jsonldump, fp=sys.stdout)): - print("Listening for AMSI events. Press enter to stop...") - sys.stdin.readline() - - -if __name__ == "__main__": - main() diff --git a/analyzer/windows/modules/auxiliary/amsi_collector.py b/analyzer/windows/modules/auxiliary/amsi_collector.py deleted file mode 100644 index c847ec79c9a..00000000000 --- a/analyzer/windows/modules/auxiliary/amsi_collector.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python - -import binascii -import functools -import logging -import os -import tempfile -import threading - -from lib.common.abstracts import Auxiliary -from lib.common.results import upload_buffer_to_host, upload_to_host - -from .amsi import AMSI, jsonldump - -logger = logging.getLogger(__name__) - - -class AMSICollector(Auxiliary, threading.Thread): - def __init__(self, options, config): - Auxiliary.__init__(self, options, config) - threading.Thread.__init__(self) - self.enabled = config.amsi - self.stop_event = threading.Event() - self.tmpfile = None - self.upload_prefix = "aux/amsi" - - def handle_event(self, event, logfh=None): - """ - Process the AMSI event by appending a line to amsi.jsonl file containing its metadata. - That will get uploaded after we finish collecting events. - - Upload the content of the event as its own file to be stored. - """ - # https://redcanary.com/blog/amsi/ has some useful info on the event record fields. - content = event.pop("content", None) - if not content: - return - - if logfh: - jsonldump(event, fp=logfh) - - dump_path = f"{self.upload_prefix}/{event['hash'][2:].lower()}" - decoded_content = binascii.unhexlify(content[2:]) - if event.get("appname", "") in ("DotNet", "coreclr"): - # The content is the full in-memory .NET assembly PE. - pass - else: - # The content is UTF-16 encoded text. We'll store it as utf-8, just like all other text files. - decoded_content = decoded_content.decode("utf-16", errors="replace").encode("utf-8") - upload_buffer_to_host(decoded_content, dump_path) - - def stop(self): - self.stop_event.set() - - def run(self): - if not self.enabled: - return - - try: - with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as fil: - self.tmpfile = fil.name - amsi = AMSI(event_callback=functools.partial(self.handle_event, logfh=fil)) - logger.info("AMSI: Starting to listen for events.") - try: - with amsi: - self.stop_event.wait() - logger.info("AMSI: Stopping event consumer.") - except PermissionError as err: - raise PermissionError( - "This module must be run with Administrator privilege in order to collect AMSI events." - ) from err - except Exception: - logger.exception("AMSI: Exception raised.") - raise - - def finish(self): - """Upload the file that contains the metadata for all of the events.""" - if not self.tmpfile or not os.path.exists(self.tmpfile): - return - try: - if os.stat(self.tmpfile).st_size > 0: - upload_to_host(self.tmpfile, f"{self.upload_prefix}/amsi.jsonl") - else: - logger.debug("AMSI: no AMSI events were collected.") - except Exception: - logger.exception("AMSI: Exception was raised while uploading amsi.jsonl") - raise - finally: - os.unlink(self.tmpfile) - self.tmpfile = None diff --git a/analyzer/windows/modules/auxiliary/amsi_etw.py b/analyzer/windows/modules/auxiliary/amsi_etw.py new file mode 100644 index 00000000000..684e2cbcca7 --- /dev/null +++ b/analyzer/windows/modules/auxiliary/amsi_etw.py @@ -0,0 +1,197 @@ +""" +This module captures AMSI events via ETW, uploading script contents (powershell, WMI, macros, etc) +to aux/amsi_etw and saving trace details to be reported by the amsi_etw processing module. + +It is a reimplementation of the SecureWorks amsi_collector and amsi modules, adapted to +use the CCCS event tracing module format. + +Installation of the pywintrace python library on the guest is mandatory. +Setting the option 'amsi_etw_assemblies=1' during tasking will cause full CLR assemblies +to be collected as well. +""" +import json +import logging +import os +import tempfile +import binascii + +from lib.common.abstracts import Auxiliary +from lib.common.results import upload_buffer_to_host, upload_to_host +from lib.core.config import Config + +log = logging.getLogger(__name__) + +ETW = False +HAVE_ETW = False +try: + from etw import ETW, ProviderInfo + from etw.GUID import GUID + + HAVE_ETW = True +except ImportError as e: + log.debug( + "Could not load auxiliary module AMSI_ETW due to '%s'\nIn order to use AMSI_ETW functionality, it " + "is required to have pywintrace setup in python", str(e) + ) + +if HAVE_ETW: + + class ETW_provider(ETW): + def __init__( + self, + ring_buf_size=1024, + max_str_len=1024, + min_buffers=0, + max_buffers=0, + filters=None, + event_callback=None, + logfile=None, + upload_prefix="aux/amsi_etw", + upload_assemblies=False + ): + """ + Initializes an instance of AMSI_ETW. The default parameters represent a very typical use case and should not be + overridden unless the user knows what they are doing. + + :param ring_buf_size: The size of the ring buffer used for capturing events. + :param max_str_len: The maximum length of the strings the proceed the structure. + Unless you know what you are doing, do not modify this value. + :param min_buffers: The minimum number of buffers for an event tracing session. + Unless you know what you are doing, do not modify this value. + :param max_buffers: The maximum number of buffers for an event tracing session. + Unless you know what you are doing, do not modify this value. + :param filters: List of filters to apply to capture. + :param logfile: Path to logfile. + :param upload_prefix: Path to upload results to. Must be approved in resultserver.py. + :param upload_assemblies: Whether to also upload the content of dotnet assemblies. + """ + self.upload_prefix = upload_prefix + self.log_file = logfile + self.event_callback = self.on_event + self.upload_assemblies = upload_assemblies + + providers = [ + ProviderInfo( + "AMSI", + GUID("{2A576B87-09A7-520E-C21A-4942F0271D67}"), + level=255, + any_keywords=None, + all_keywords=None, + ) + ] + self.event_id_filters = [1101] + super().__init__( + session_name="ETW_AMSI", + ring_buf_size=ring_buf_size, + max_str_len=max_str_len, + min_buffers=min_buffers, + max_buffers=max_buffers, + event_callback=self.event_callback, + task_name_filters=filters, + providers=providers, + event_id_filters=self.event_id_filters, + ) + + def on_event(self, event_tufo): + """ + Starts the capture using ETW. + :param event_tufo: tufo containing event information + :param logfile: Path to logfile. + :return: Does not return anything. + """ + event_id, event = event_tufo + content = event.pop("content", None) + if content: + dump_path = f"{self.upload_prefix}/{event['hash'][2:].lower()}" + decoded_content = binascii.unhexlify(content[2:]) + if event.get("appname", "") in ("DotNet", "coreclr"): + # The content is the full in-memory .NET assembly PE. + if self.upload_assemblies: + event['dump_path'] = dump_path+".bin" + upload_buffer_to_host(decoded_content, event['dump_path']) + else: + log.debug("Skipping upload of %d byte CLR assembly - amsi_etw_assemblies option was not set", len(decoded_content)) + else: + # The content is UTF-16 encoded text. We'll store it as utf-8, just like all other text files. + decoded_content = decoded_content.decode("utf-16", errors="replace").encode("utf-8") + event['dump_path'] = dump_path+".txt" + upload_buffer_to_host(decoded_content, event['dump_path']) + + if self.log_file: + # Write the event metadata as a line in the jsonl log file. + json.dump(event, self.log_file) + self.log_file.write("\n") + + def start(self): + # do pre-capture setup + self.do_capture_setup() + super().start() + + def stop(self): + super().stop() + # do post-capture teardown + self.do_capture_teardown() + + def do_capture_setup(self): + # do whatever setup for capture here + pass + + def do_capture_teardown(self): + # do whatever for capture teardown here + pass + + class AMSI_ETW(Auxiliary): + """ETW logging""" + + def __init__(self, options, config): + Auxiliary.__init__(self, options, config) + + self.config = Config(cfg="analysis.conf") + self.enabled = self.config.amsi_etw + self.do_run = self.enabled + self.upload_prefix = "aux/amsi_etw" + self.upload_assemblies = options.get("amsi_etw_assemblies", False) + if self.upload_assemblies: + log.debug("Will upload Dotnet assembly content") + else: + log.debug("Will discard Dotnet assembly content") + + if HAVE_ETW: + self.log_file = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) + self.capture = ETW_provider(logfile=self.log_file, upload_prefix=self.upload_prefix, + upload_assemblies=self.upload_assemblies) + + def start(self): + if not self.enabled or not HAVE_ETW: + return False + try: + log.debug("Starting AMSI ETW") + # Start AMSI_ETW_provider in the background + self.capture.start() + except Exception as e: + log.exception("An error occurred while starting AMSI ETW: %s", e) + return True + + def stop(self): + if not HAVE_ETW: + return + log.debug("Stopping AMSI_ETW...") + self.capture.stop() + + """Upload the file that contains the metadata for all of the events.""" + if not self.log_file or not os.path.exists(self.log_file.name): + log.debug("No logfile to upload") + return + self.log_file.close() + + try: + if os.stat(self.log_file.name).st_size > 0: + upload_to_host(self.log_file.name, f"{self.upload_prefix}/amsi.jsonl") + else: + log.debug("No AMSI events were collected.") + except Exception: + log.exception("Exception was raised while uploading amsi.jsonl") + raise + finally: + os.unlink(self.log_file.name) + self.log_file = None diff --git a/analyzer/windows/modules/auxiliary/dns_etw.py b/analyzer/windows/modules/auxiliary/dns_etw.py index 093d50f7288..e86a79a4e84 100644 --- a/analyzer/windows/modules/auxiliary/dns_etw.py +++ b/analyzer/windows/modules/auxiliary/dns_etw.py @@ -182,7 +182,13 @@ def __init__(self, options, config): self.config = Config(cfg="analysis.conf") self.enabled = self.config.dns_etw self.do_run = self.enabled - self.output_dir = "C:\\\\etw_dns" + + self.output_dir = "C:\\etw_dns\\" + try: + os.mkdir(self.output_dir) + except FileExistsError: + pass + self.log_file = os.path.join(self.output_dir, "dns_provider.log") if HAVE_ETW: self.capture = ETW_provider(logfile=self.log_file, level=255, no_conout=True) @@ -204,7 +210,7 @@ def start(self): def stop(self): if not HAVE_ETW: return - log.debug("Stopping!!!") + log.debug("Stopping DNS_ETW...") self.capture.stop() files_to_upload = set() diff --git a/conf/default/auxiliary.conf.default b/conf/default/auxiliary.conf.default index bc2c09d6362..85e9d63bf0d 100644 --- a/conf/default/auxiliary.conf.default +++ b/conf/default/auxiliary.conf.default @@ -17,7 +17,6 @@ # Modules to be enabled or not inside of the VM [auxiliary_modules] -amsi = no browser = yes curtain = no digisig = yes @@ -46,8 +45,10 @@ tracee_linux = no sslkeylogfile = no # Requires setting up browser extension, check extra/browser_extension browsermonitor = no +# ETW logging modules require pywintrace to be installed on the guest VM wmi_etw = no dns_etw = no +amsi_etw = no watchdownloads = no [AzSniffer] diff --git a/lib/cuckoo/core/resultserver.py b/lib/cuckoo/core/resultserver.py index 9635e882406..890752b34c9 100644 --- a/lib/cuckoo/core/resultserver.py +++ b/lib/cuckoo/core/resultserver.py @@ -61,7 +61,7 @@ RESULT_UPLOADABLE = ( b"CAPE", b"aux", - b"aux/amsi", + b"aux/amsi_etw", b"browser", b"curtain", b"debugger", diff --git a/modules/processing/amsi.py b/modules/processing/amsi_etw.py similarity index 87% rename from modules/processing/amsi.py rename to modules/processing/amsi_etw.py index ee33c5eb30f..cff0ed6edff 100644 --- a/modules/processing/amsi.py +++ b/modules/processing/amsi_etw.py @@ -8,11 +8,11 @@ log = logging.getLogger(__name__) -class Amsi(Processing): - key = "amsi" +class AMSI_ETW(Processing): + key = "amsi_etw" def run(self): - jsonl_file = os.path.join(self.aux_path, "amsi", "amsi.jsonl") + jsonl_file = os.path.join(self.aux_path, "amsi_etw", "amsi.jsonl") if not os.path.exists(jsonl_file) or os.stat(jsonl_file).st_size == 0: return None @@ -46,10 +46,12 @@ def decode_event(cls, event): "kernel_time": header["KernelTime"], "user_time": header["UserTime"], "activity_id": header["ActivityId"], - "scan_result": cls.scan_result_to_str(event["scanResult"]), + "scan_result": cls.scan_result_to_str(int(event["scanResult"])), "app_name": event["appname"], "content_name": event["contentname"], "content_filtered": event["contentFiltered"], + "content_size": int(event["contentsize"]), + "dump_path": event.get("dump_path", ""), "hash": event["hash"][2:].lower(), } diff --git a/utils/go-fetcher/go.mod b/utils/go-fetcher/go.mod index c4e730b2880..437941bf7e2 100644 --- a/utils/go-fetcher/go.mod +++ b/utils/go-fetcher/go.mod @@ -13,7 +13,8 @@ require ( github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect diff --git a/utils/go-fetcher/go.sum b/utils/go-fetcher/go.sum index 9059a83939f..ee070a9a5c0 100644 --- a/utils/go-fetcher/go.sum +++ b/utils/go-fetcher/go.sum @@ -7,8 +7,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= -github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= diff --git a/uv.lock b/uv.lock index c346d47b277..294a0a4ae81 100644 --- a/uv.lock +++ b/uv.lock @@ -397,7 +397,7 @@ requires-dist = [ { name = "sqlalchemy-utils", specifier = "==0.41.1" }, { name = "tldextract", specifier = ">=5.1.2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.18.2" }, - { name = "werkzeug", specifier = "==3.1.4" }, + { name = "werkzeug", specifier = "==3.1.5" }, { name = "yara-python", specifier = "==4.5.1" }, ] provides-extras = ["maco"] @@ -4259,7 +4259,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -4267,9 +4267,9 @@ dependencies = [ { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] @@ -4481,14 +4481,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.4" +version = "3.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] [[package]]