diff --git a/windows/winobject/evtlog.py b/windows/winobject/evtlog.py new file mode 100644 index 0000000..1f645f4 --- /dev/null +++ b/windows/winobject/evtlog.py @@ -0,0 +1,503 @@ +import ctypes +import os.path +import xml.dom.minidom +from contextlib import contextmanager + +import windows +import windows.generated_def as gdef +from windows import winproxy + + +# Helpers + +@contextmanager +def ClosingEvtHandle(handle): + try: + yield handle + finally: + winproxy.EvtClose(handle) + + +# low-level api helpers +def queryinfo(handle, propertyid): + size = 0x1000 + buffer = ctypes.create_string_buffer(size) + evt = ImprovedEVT_VARIANT.from_buffer(buffer) + res = gdef.DWORD() + windows.winproxy.EvtGetLogInfo(handle, propertyid, size, evt, res) + return evt + +def arrayproperty(handle, property, index, flags=0): + size = 0x1000 + buffer = ctypes.create_string_buffer(size) + evt = ImprovedEVT_VARIANT.from_buffer(buffer) + res = gdef.DWORD() + windows.winproxy.EvtGetObjectArrayProperty(handle, property, index, flags, size, evt, res) + return evt + +def generate_query_function(query_function): + def generated_query_function(handle, propertyid, flags=0): + size = 0x10000 + buffer = ctypes.create_string_buffer(size) + evt = ImprovedEVT_VARIANT.from_buffer(buffer) + res = gdef.DWORD() + query_function(handle, propertyid, flags, size, evt, res) + return evt + return generated_query_function + +chaninfo = generate_query_function(winproxy.EvtGetChannelConfigProperty) +eventinfo = generate_query_function(winproxy.EvtGetEventMetadataProperty) +publishinfo = generate_query_function(winproxy.EvtGetPublisherMetadataProperty) + + +# Class high-level API +class EvtQuery(gdef.EVT_HANDLE): + TIMEOUT = 0x1000 + + def __init__(self, handle=0, channel=None): + super(EvtQuery, self).__init__(handle) + self.channel = channel + + def __next__(self): + try: + event = EvtEvent(channel=self.channel) + ret = gdef.DWORD() + windows.winproxy.EvtNext(self, 1, event, self.TIMEOUT, 0, ret) + except WindowsError as e: + if e.winerror == gdef.ERROR_NO_MORE_ITEMS: + raise StopIteration + raise + assert ret.value == 1 + return event + + def __iter__(self): + return self + + next = __next__ # Yep.. real name is 'next' in Py2 :D + + def all(self): # SqlAlchemy like :) + return list(self) + + +class EvtEvent(gdef.EVT_HANDLE): + def __init__(self, handle=0, channel=None): + super(EvtEvent, self).__init__(handle) + self.channel = channel + + def render(self, ctx, rtype): + size = 0x10000 + buffer = ctypes.c_buffer(size) + rsize = gdef.DWORD() + elementnb = gdef.DWORD() + try: + windows.winproxy.EvtRender(ctx, self, rtype, size, buffer, rsize, elementnb) + except WindowsError as e: + if e.winerror != gdef.ERROR_INSUFFICIENT_BUFFER: + raise + size = rsize.value + buffer = ctypes.c_buffer(size) + windows.winproxy.EvtRender(ctx, self, rtype, size, buffer, rsize, elementnb) + # Adapting return value type + if rtype != gdef.EvtRenderEventValues: + # import pdb;pdb.set_trace() + # assert elementnb.value == 1 + return buffer[:rsize.value] + # print("Got <{0}> elt".format(elementnb.value)) + return list((ImprovedEVT_VARIANT * elementnb.value).from_buffer(buffer)) + + def render_xml(self): + xml = self.render(None, 1).decode("utf-16") + assert xml[-1] == "\x00" + return xml[:-1] + + def value(self, name, **kwargs): + values = self.get_values((name,), **kwargs) + assert len(values) == 1 + return values[0] + + def get_values(self, values, flags=gdef.EvtRenderContextValues): + nbelt = len(values) + pwstr_values = tuple(gdef.LPWSTR(v) for v in values) + pwstr_rarray = (gdef.LPWSTR * nbelt)(*pwstr_values) + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa385352(v=vs.85).aspx + # An array of XPath expressions that uniquely identify a node or attribute in the event that you want to render. + # Each value wil return 1 node :) + ctx = windows.winproxy.EvtCreateRenderContext(nbelt, pwstr_rarray, gdef.EvtRenderContextValues) + result = self.render(ctx, gdef.EvtRenderEventValues) + return [r.value for r in result] + + def system(self): # POC: use this for all @property based on system data ? + ctx = windows.winproxy.EvtCreateRenderContext(0, None, gdef.EvtRenderContextSystem) + result = self.render(ctx, gdef.EvtRenderEventValues) + return [r.value for r in result] + + def user(self): + ctx = windows.winproxy.EvtCreateRenderContext(0, None, gdef.EvtRenderContextUser) + result = self.render(ctx, gdef.EvtRenderEventValues) + return [r.value for r in result] + + # Properties arround common Event/System values + @property + def id(self): + return self.value("Event/System/EventID") + + @property + def version(self): + return self.value("Event/System/Version") + + @property + def level(self): + return self.value("Event/System/Level") + + @property + def opcode(self): + return self.value("Event/System/Opcode") + + @property + def time_created(self): + return self.value("Event/System/TimeCreated/@SystemTime") + + @property + def metadata(self): + "HEAVY CALL" + return self.channel.get_event_metadata(self.id) + + # Test + @property + def data(self): # user/event specifique data + return {k:v for k,v in zip(self.metadata.user_data, self.user())} + + + def __repr__(self): + creation_time = windows.utils.datetime_from_filetime(self.time_created) + return '<{0} id="{self.id}" time="{creation_time}">'.format(type(self).__name__, self=self, creation_time=creation_time) + + +class ImprovedEVT_VARIANT(gdef.EVT_VARIANT): + VALUE_MAPPER = { + gdef.EvtVarTypeNull : 'NoneValue', + gdef.EvtVarTypeString : 'StringVal', + gdef.EvtVarTypeAnsiString : 'AnsiStringVal', + gdef.EvtVarTypeSByte : 'SByteVal', + gdef.EvtVarTypeByte : 'ByteVal', + gdef.EvtVarTypeInt16 : 'Int16Val', + gdef.EvtVarTypeUInt16 : 'UInt16Val', + gdef.EvtVarTypeInt32 : 'Int32Val', + gdef.EvtVarTypeUInt32 : 'UInt32Val', + gdef.EvtVarTypeInt64 : 'Int64Val', + gdef.EvtVarTypeUInt64 : 'UInt64Val', + gdef.EvtVarTypeSingle : 'SingleVal', + gdef.EvtVarTypeDouble : 'DoubleVal', + gdef.EvtVarTypeBoolean : 'BooleanVal', + gdef.EvtVarTypeBinary : 'BinaryVal', + gdef.EvtVarTypeGuid : 'GuidVal', + gdef.EvtVarTypeSizeT : 'SizeTVal', + gdef.EvtVarTypeFileTime : 'FileTimeVal', + gdef.EvtVarTypeSysTime : 'SysTimeVal', + gdef.EvtVarTypeSid : 'SidVal', + gdef.EvtVarTypeHexInt32 : 'BinaryVal', + gdef.EvtVarTypeHexInt64 : 'BinaryVal', + gdef.EvtVarTypeEvtHandle : 'EvtHandleVal', + gdef.EvtVarTypeEvtXml : 'XmlVal', + } + NoneValue = None + @property + def Type(self): + raw_type = super(ImprovedEVT_VARIANT, self).Type + return gdef.EVT_VARIANT_TYPE.mapper[raw_type] + + @property + def value(self): # Prototype !! + attrname = self.VALUE_MAPPER[self.Type] + # print("Resolve type <{0}> -> {1}".format(self.Type, attrname)) + return getattr(self, attrname) + + def __repr__(self): + return "<{0} of type={1}>".format(type(self).__name__, self.Type) + + +def channels(): + h = windows.winproxy.EvtOpenChannelEnum(None, 0) + size = 0x1000 + buffer = ctypes.create_unicode_buffer(size) + ressize = gdef.DWORD() + try: + while True: + try: + windows.winproxy.EvtNextChannelPath(h, size, buffer, ressize) + except WindowsError as e: + if e.winerror != gdef.ERROR_NO_MORE_ITEMS: + raise + return + assert buffer[ressize.value - 1] == "\x00" + yield buffer[:ressize.value - 1] + finally: # TODO: ctx manager + windows.winproxy.EvtClose(h) + + +def publishers(): + h = windows.winproxy.EvtOpenPublisherEnum(None, 0) + size = 0x1000 + buffer = ctypes.create_unicode_buffer(size) + ressize = gdef.DWORD() + try: + while True: + try: + windows.winproxy.EvtNextPublisherId(h, size, buffer, ressize) + except WindowsError as e: + if e.winerror != gdef.ERROR_NO_MORE_ITEMS: + raise + return + assert buffer[ressize.value - 1] == "\x00" + yield buffer[:ressize.value - 1] + finally: # TODO: ctx manager + windows.winproxy.EvtClose(h) + + + +# x = windows.winproxy.EvtQuery(None, + # "Microsoft-Windows-Windows Firewall With Advanced Security/Firewall", + # "Event/System[EventID=2004 or EventID=2006]", + # gdef.EvtQueryChannelPath + gdef.EvtQueryForwardDirection) +# print(x) +# eq = EvtQuery(x) +# event = next(eq) + +# xx = gdef.LPWSTR("ModifyingApplication") +# xx = gdef.LPWSTR("Event/System/Channel") +# xx = gdef.LPWSTR('Event/EventData/Data[@Name="ModifyingApplication"]') +# xx = gdef.LPWSTR('Event/System/EventID') + +# event = next(eq) + +# list(channels()) + +class EvtChannel(object): + DEFAULT_QUERY_FLAGS = gdef.EvtQueryChannelPath + gdef.EvtQueryForwardDirection + + def __init__(self, name): + self.name = name + self.event_metadata_by_id = {} + + def query(self, ids=None, filter=None): + if ids is not None: + if isinstance(ids, (long, int)): + ids = (ids,) + ids_filter = " or ".join("EventID={0}".format(id) for id in ids) + filter = "Event/System[{0}]".format(ids_filter) + query_handle = winproxy.EvtQuery(None, self.name, filter, self.DEFAULT_QUERY_FLAGS) + return EvtQuery(query_handle, self) + + @property + def config(self): + return ChannelConfig.from_channel_name(self.name) + + def get_event_metadata(self, id): + try: + return self.event_metadata_by_id[id] + except KeyError as e: + pass + + pub_metada = self.config.publisher.metadata + self.event_metadata_by_id = {evtm.id: evtm for evtm in pub_metada.events_metadata} + return self.event_metadata_by_id[id] + + + def __repr__(self): + return '<{0} "{1}">'.format(type(self).__name__, self.name) + + +class EvtFile(EvtChannel): + DEFAULT_QUERY_FLAGS = gdef.EvtQueryFilePath + gdef.EvtQueryForwardDirection + + @property + def config(self): + raise NotImplementedError("Cannot retrieve the configuration of an EvtFile") + + +class ChannelConfig(gdef.EVT_HANDLE): + def __init__(self, handle, name=None): + super(ChannelConfig, self).__init__(handle) + self.name = name + + @classmethod + def from_channel_name(cls, channel_name): + return cls(winproxy.EvtOpenChannelConfig(None, channel_name, 0), channel_name) + + @property + def publisher(self): + return EvtPublisher(chaninfo(self, gdef.EvtChannelConfigOwningPublisher).value) + + def __repr__(self): + return '<{0} "{1}">'.format(type(self).__name__, self.name) + + +class EvtPublisher(object): + def __init__(self, name): + self.name = name + + @property + def metadata(self): + return PublisherMetadata.from_publisher_name(self.name) + + def __repr__(self): + return '<{0} "{1}">'.format(type(self).__name__, self.name) + + +class PublisherMetadata(gdef.EVT_HANDLE): + def __init__(self, handle, name=None): + super(PublisherMetadata, self).__init__(handle) + self.name = name + + @classmethod + def from_publisher_name(cls, publisher_name): + return cls(winproxy.EvtOpenPublisherMetadata(None, publisher_name, None, 0, 0), publisher_name) + + @property + def chanrefs(self): + return PropertyArray(publishinfo(self, gdef.EvtPublisherMetadataChannelReferences).value) + + @property + def events_metadata(self): + eh = winproxy.EvtOpenEventMetadataEnum(self, 0) + with ClosingEvtHandle(eh): + while True: + try: + nh = windows.winproxy.EvtNextEventMetadata(eh, 0) + yield EventMetadata(nh) + except WindowsError as e: + if e.winerror != gdef.ERROR_NO_MORE_ITEMS: + raise + break + + def channel_name_by_id(self): + chansref = self.chanrefs + channame_by_value_id = {} + for i in range(chansref.size): + value = chansref.propery(gdef.EvtPublisherMetadataChannelReferenceID, i) + name = chansref.propery(gdef.EvtPublisherMetadataChannelReferencePath, i) + channame_by_value_id[value] = name + return channame_by_value_id + + def message(self, msgid): + size = 0x1000 + buffer = ctypes.c_buffer(size) + sbuff = ctypes.cast(buffer, gdef.LPWSTR) + outsize = gdef.DWORD() + try: + winproxy.EvtFormatMessage(self, None, msgid, 0, None, gdef.EvtFormatMessageId, size, sbuff, outsize) + except WindowsError as e: + if e.winerror != gdef.ERROR_EVT_UNRESOLVED_VALUE_INSERT: + raise + return sbuff.value + + def __repr__(self): + return '<{0} "{1}">'.format(type(self).__name__, self.name) + + +class PropertyArray(gdef.EVT_OBJECT_ARRAY_PROPERTY_HANDLE): + @property + def size(self): + array_size = gdef.DWORD() + windows.winproxy.EvtGetObjectArraySize(self, array_size) + return array_size.value + + def propery(self, type, index): + return arrayproperty(self, type, index).value + +class EventMetadata(gdef.EVT_HANDLE): + @property + def id(self): + return eventinfo(self, gdef.EventMetadataEventID).value + + @property + def channel_id(self): + return eventinfo(self, gdef.EventMetadataEventChannel).value + + @property + def message_id(self): + return eventinfo(self, gdef.EventMetadataEventMessageID).value + + @property + def template(self): + return eventinfo(self, gdef.EventMetadataEventTemplate).value + + @property + def user_data(self): + result = [] + xmltemplate = xml.dom.minidom.parseString(self.template) + for data in xmltemplate.getElementsByTagName("data"): + result.append(data.attributes["name"].value) + return result + + +class EvtlogManager(object): + "TODO: doc -> the frontend API object" + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa385784(v=vs.85).aspx + + def open_channel(self, channel_name): + chan = EvtChannel(channel_name) + chan.config # Force to retrieve a handle (check channel exists) + return chan + + def open_evtx_file(self, filename): + # We don't check if file exists because of 32->64 redirection + # Do a check with FSRedirection disabled ? + with windows.utils.DisableWow64FsRedirection(): + if not os.path.exists(filename): + raise WindowsError(gdef.ERROR_FILE_NOT_FOUND, "Could not find file <{0}>".format(filename)) + file = EvtFile(filename) + return file + + def open_publisher(self, publisher_name): + publisher = EvtPublisher(publisher_name) + publisher.metadata # Force to retrieve a handle (check channel exists) + return publisher + + def __getitem__(self, name): + try: + return self.open_channel(name) + except WindowsError as e: + pass + + try: + return self.open_publisher(name) + except WindowsError as e: + pass + # Raise FILE_NOT_FOUND if not found (last chance) + return self.open_evtx_file(name) + + + + +# CHANNAME = "Microsoft-Windows-Windows Firewall With Advanced Security/Firewall" +# CHANNAME = r"Microsoft-Windows-Windows Defender/Operational" + +# print("Working of channel: <{0}>".format(CHANNAME)) + +# channel = EvtChannel(CHANNAME) +# chanconf = channel.config +# publisher = chanconf.publisher + +# open publisher metadata + +# print("Provider is <{0}>".format(publisher.name)) + +# pmd = publisher.metadata +# chansref = pmd.chanrefs + +# channame_by_value_id = pmd.channel_name_by_id() + +# for event_metadata in pmd.events_metadata: + # id = event_metadata.id + # if id == 2004: + # print("LOL") + # chan = event_metadata.channel_id + # channame = channame_by_value_id[chan] + # print(" * {0}) {1}".format(id, channame)) + # if "unexpected" in pmd.message(event_metadata.message_id).lower(): + # print("UNEXPECTED in message :D") + + +# query = channel.query(ids=5008) +# evts = list(query)