From 9715544dec48f8fdbd70be16ec9c630dff9cd623 Mon Sep 17 00:00:00 2001 From: neitsa Date: Mon, 13 Feb 2023 12:38:46 +0100 Subject: [PATCH 01/26] export maximum user land address, which is needed to resolve symbols. --- procmon_parser/__init__.py | 6 ++++++ procmon_parser/logs.py | 6 ++++++ procmon_parser/stream_logs_format.py | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/procmon_parser/__init__.py b/procmon_parser/__init__.py index 390bc44..45a88ae 100644 --- a/procmon_parser/__init__.py +++ b/procmon_parser/__init__.py @@ -44,6 +44,12 @@ def __getitem__(self, index): def __len__(self): return self._struct_readear.number_of_events + @property + def maximum_application_address(self): + """Return the highest possible user land address. + """ + return self._struct_readear.maximum_application_address + def processes(self): """Return a list of all the known processes in the log file """ diff --git a/procmon_parser/logs.py b/procmon_parser/logs.py index 113c10b..06d6c93 100644 --- a/procmon_parser/logs.py +++ b/procmon_parser/logs.py @@ -309,6 +309,12 @@ def get_event_at_offset(self, offset): def number_of_events(self): return self.header.number_of_events + @property + def maximum_application_address(self): + """Return the highest possible user land address. + """ + return self.header.maximum_application_address + def processes(self): """Return a list of all the known processes in the log file """ diff --git a/procmon_parser/stream_logs_format.py b/procmon_parser/stream_logs_format.py index 83f318f..c756554 100644 --- a/procmon_parser/stream_logs_format.py +++ b/procmon_parser/stream_logs_format.py @@ -36,7 +36,9 @@ def __init__(self, io): # Docs of this table's layout are in "docs\PML Format.md" self.icon_table_offset = read_u64(stream) - stream.seek(12, 1) # Unknown fields + self.maximum_application_address = read_u64(stream) + + self.os_version_info_size = read_u32(stream) self.windows_major_number = read_u32(stream) self.windows_minor_number = read_u32(stream) self.windows_build_number = read_u32(stream) From ab25bc1bc13ce57a1fe5152aba87d5983f2ec6c5 Mon Sep 17 00:00:00 2001 From: neitsa Date: Mon, 13 Feb 2023 12:40:07 +0100 Subject: [PATCH 02/26] Add dbghelp wrapper. --- .../symbol_resolver/win/__init__.py | 0 procmon_parser/symbol_resolver/win/dbghelp.py | 333 ++++++++++++++++++ .../symbol_resolver/win/kernel32.py | 13 + .../symbol_resolver/win/win_types.py | 20 ++ 4 files changed, 366 insertions(+) create mode 100644 procmon_parser/symbol_resolver/win/__init__.py create mode 100644 procmon_parser/symbol_resolver/win/dbghelp.py create mode 100644 procmon_parser/symbol_resolver/win/kernel32.py create mode 100644 procmon_parser/symbol_resolver/win/win_types.py diff --git a/procmon_parser/symbol_resolver/win/__init__.py b/procmon_parser/symbol_resolver/win/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py new file mode 100644 index 0000000..2bb850b --- /dev/null +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +"""Module wrapper around DbgHelp.dll Windows library. + +DbgHelp.dll is the main library for resolving symbols. +""" +import ctypes +import dataclasses +import logging +import pathlib +import enum + +import _ctypes # only used for typing as ctypes doesn't export inner types. + +from procmon_parser.symbol_resolver.win.win_types import ( + HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD) + +logger = logging.getLogger(__name__) + +# +# Callback Functions needed by some DbgHelp APIs. +# + +# PFINDFILEINPATHCALLBACK; used with the SymFindFileInPath function. +# https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nc-dbghelp-pfindfileinpathcallback +PFINDFILEINPATHCALLBACK = ctypes.WINFUNCTYPE(BOOL, PCSTR, PVOID, use_last_error=False) + + +# +# Structures used by DbgHelp APIs. +# + + +class MODLOAD_DATA(ctypes.Structure): # noqa + """Contains module data. Used by SymLoadModuleExW. + + See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-modload_data + """ + _fields_ = ( + ("ssize", DWORD), + ("ssig", DWORD), + ("data", PVOID), + ("size", DWORD), + ("flags", DWORD), + ) + + +PMODLOAD_DATA = ctypes.POINTER(MODLOAD_DATA) + + +class SYMBOL_INFOW(ctypes.Structure): # noqa + """Contains symbol information. Used by SymFromAddrW. + + See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_infow + """ + BUFFER_NUM_ELEMENTS = 468 + + _fields_ = ( + ("SizeOfStruct", ULONG), + ("TypeIndex", ULONG), + ("Reserved", ULONG64 * 2), + ("Index", ULONG), + ("Size", ULONG), + ("ModBase", ULONG64), + ("Flags", ULONG), + ("Value", ULONG64), + ("Address", ULONG64), + ("Register", ULONG), + ("Scope", ULONG), + ("Tag", ULONG), + ("NameLen", ULONG), + ("MaxNameLen", ULONG), + ("Name", WCHAR * BUFFER_NUM_ELEMENTS) + ) + + +PSYMBOL_INFOW = ctypes.POINTER(SYMBOL_INFOW) + + +class IMAGEHLP_LINEW64(ctypes.Structure): # noqa + """Represents a source file line. Used by SymGetLineFromAddrW64. + + See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_linew64 + """ + _fields_ = ( + ("SizeOfStruct", DWORD), + ("Key", PVOID), + ("LineNumber", DWORD), + ("FileName", PWSTR), + ("Address", DWORD64) + ) + + +PIMAGEHLP_LINEW64 = ctypes.POINTER(IMAGEHLP_LINEW64) + + +# +# Functions descriptors +# + + +@dataclasses.dataclass +class _FunctionDescriptor: + """Class used to describe a Windows API function wrt its ctypes bindings.""" + name: str + parameter_types: tuple[_ctypes._SimpleCData] | None = None + return_type: _ctypes._SimpleCData | None = None + aliases: list[str] | None = None + + +# list of function (descriptors) from DbgHelp.dll +_functions_descriptors: list[_FunctionDescriptor] = [ + # SymInitializeW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-syminitializew + _FunctionDescriptor("SymInitializeW", + (HANDLE, PCSTR, BOOL), + BOOL, + ["SymInitialize"]), + # SymCleanup + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symcleanup + _FunctionDescriptor("SymCleanup", + (HANDLE,), + BOOL), + # SymSetOptions + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetoptions + _FunctionDescriptor("SymSetOptions", + (DWORD,), + DWORD), + # SymSetSearchPathW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetsearchpathw + _FunctionDescriptor("SymSetSearchPathW", + (HANDLE, PCWSTR), + BOOL, + ["SymSetSearchPath"]), + # SymFindFileInPathW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symfindfileinpathw + _FunctionDescriptor("SymFindFileInPathW", + (HANDLE, PCWSTR, PCWSTR, PVOID, DWORD, DWORD, DWORD, PWSTR, PFINDFILEINPATHCALLBACK, PVOID), + BOOL, + ["SymFindFileInPath"]), + # SymLoadModuleExW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symloadmoduleexw + _FunctionDescriptor("SymLoadModuleExW", + (HANDLE, HANDLE, PCWSTR, PCWSTR, DWORD64, DWORD, PMODLOAD_DATA, DWORD), + DWORD64, + ["SymLoadModuleEx"]), + + # SymFromAddrW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symfromaddrw + _FunctionDescriptor("SymFromAddrW", + (HANDLE, DWORD64, PDWORD64, PSYMBOL_INFOW), + BOOL, + ["SymFromAddr"]), + + # SymGetLineFromAddrW64 + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symgetlinefromaddrw64 + _FunctionDescriptor("SymGetLineFromAddrW64", + (HANDLE, DWORD64, PDWORD, PIMAGEHLP_LINEW64), + BOOL), + + # SymGetLinePrevW64 + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symgetlineprevw64 + _FunctionDescriptor("SymGetLinePrevW64", + (HANDLE, PIMAGEHLP_LINEW64), + BOOL), + + # SymGetSourceFileW + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symgetsourcefilew + _FunctionDescriptor("SymGetSourceFileW", + (HANDLE, ULONG64, PCWSTR, PCWSTR, PWSTR, DWORD), + BOOL, + ["SymGetSourceFile"] + ), +] + + +# +# Constants +# + +class SYMOPT(enum.IntFlag): + """Options that are set/returned by SymSetOptions() & SymGetOptions(); these are used as a mask. + + Notes: + This is a made up enum since constants are just `#define` in dbghelp.h. This prevents to have to import all + constants though. + """ + SYMOPT_CASE_INSENSITIVE = 0x00000001 + SYMOPT_UNDNAME = 0x00000002 + SYMOPT_DEFERRED_LOADS = 0x00000004 + SYMOPT_NO_CPP = 0x00000008 + SYMOPT_LOAD_LINES = 0x00000010 + SYMOPT_OMAP_FIND_NEAREST = 0x00000020 + SYMOPT_LOAD_ANYTHING = 0x00000040 + SYMOPT_IGNORE_CVREC = 0x00000080 + SYMOPT_NO_UNQUALIFIED_LOADS = 0x00000100 + SYMOPT_FAIL_CRITICAL_ERRORS = 0x00000200 + SYMOPT_EXACT_SYMBOLS = 0x00000400 + SYMOPT_ALLOW_ABSOLUTE_SYMBOLS = 0x00000800 + SYMOPT_IGNORE_NT_SYMPATH = 0x00001000 + SYMOPT_INCLUDE_32BIT_MODULES = 0x00002000 + SYMOPT_PUBLICS_ONLY = 0x00004000 + SYMOPT_NO_PUBLICS = 0x00008000 + SYMOPT_AUTO_PUBLICS = 0x00010000 + SYMOPT_NO_IMAGE_SEARCH = 0x00020000 + SYMOPT_SECURE = 0x00040000 + SYMOPT_NO_PROMPTS = 0x00080000 + SYMOPT_OVERWRITE = 0x00100000 + SYMOPT_IGNORE_IMAGEDIR = 0x00200000 + SYMOPT_FLAT_DIRECTORY = 0x00400000 + SYMOPT_FAVOR_COMPRESSED = 0x00800000 + SYMOPT_ALLOW_ZERO_ADDRESS = 0x01000000 + SYMOPT_DISABLE_SYMSRV_AUTODETECT = 0x02000000 + SYMOPT_READONLY_CACHE = 0x04000000 + SYMOPT_SYMPATH_LAST = 0x08000000 + SYMOPT_DISABLE_FAST_SYMBOLS = 0x10000000 + SYMOPT_DISABLE_SYMSRV_TIMEOUT = 0x20000000 + SYMOPT_DISABLE_SRVSTAR_ON_STARTUP = 0x40000000 + SYMOPT_DEBUG = 0x80000000 + + +class SSRVOPT(enum.IntFlag): + """Symbol Server Options; used by functions such as SymFindFileInPathW. + + Notes: + This is a made up enum since constants are just `#define` in dbghelp.h. This prevents to have to import all + constants though. + """ + SSRVOPT_CALLBACK = 0x00000001 + SSRVOPT_DWORD = 0x00000002 + SSRVOPT_DWORDPTR = 0x00000004 + SSRVOPT_GUIDPTR = 0x00000008 + SSRVOPT_OLDGUIDPTR = 0x00000010 + SSRVOPT_UNATTENDED = 0x00000020 + SSRVOPT_NOCOPY = 0x00000040 + SSRVOPT_GETPATH = 0x00000040 + SSRVOPT_PARENTWIN = 0x00000080 + SSRVOPT_PARAMTYPE = 0x00000100 + SSRVOPT_SECURE = 0x00000200 + SSRVOPT_TRACE = 0x00000400 + SSRVOPT_SETCONTEXT = 0x00000800 + SSRVOPT_PROXY = 0x00001000 + SSRVOPT_DOWNSTREAM_STORE = 0x00002000 + SSRVOPT_OVERWRITE = 0x00004000 + SSRVOPT_RESETTOU = 0x00008000 + SSRVOPT_CALLBACKW = 0x00010000 + SSRVOPT_FLAT_DEFAULT_STORE = 0x00020000 + SSRVOPT_PROXYW = 0x00040000 + SSRVOPT_MESSAGE = 0x00080000 + SSRVOPT_SERVICE = 0x00100000 # deprecated + SSRVOPT_FAVOR_COMPRESSED = 0x00200000 + SSRVOPT_STRING = 0x00400000 + SSRVOPT_WINHTTP = 0x00800000 + SSRVOPT_WININET = 0x01000000 + SSRVOPT_DONT_UNCOMPRESS = 0x02000000 + SSRVOPT_DISABLE_PING_HOST = 0x04000000 + SSRVOPT_DISABLE_TIMEOUT = 0x08000000 + SSRVOPT_ENABLE_COMM_MSG = 0x10000000 + + +class DbgHelp: + """Main wrapper around DbgHelp.dll library functions. + + Examples: + ``` + # functions can be called as attributes from the class instance, as long as they have a function descriptor. + ret_val = DbgHelp.SymInitialize(0xdeadbeef, None, False) + if ret_val == 0: + # log error + pass + ``` + """ + + def __init__(self, dbghelp_path: pathlib.Path) -> None: + """Class init. + + Args: + dbghelp_path: Path to the dbghelp.dll library. + """ + if not dbghelp_path.is_file(): + raise ValueError(f"The given path '{dbghelp_path}' is not a file.") + + self._dll_path = dbghelp_path + + # Dictionary of functions; key is str (function name), value is ctypes function pointer. + self._functions: dict[str: _ctypes.CFuncPtr] = dict() + + # DLL instance + self._dbghelp = ctypes.WinDLL(str(dbghelp_path), use_last_error=True) + + # resolve all needed functions. + self._resolve_functions(_functions_descriptors) + + def __getitem__(self, item: str): + return self._functions[item] + + def __getattr__(self, item: str): + return self[item] + + def _resolve_functions(self, function_descriptors: list[_FunctionDescriptor]) -> None: + """[internal] Resolve functions, for the given DLL, from the list of `_FunctionsDescriptor`. + + Raises: + AttributeError: A given function was not found. + """ + for function_descriptor in function_descriptors: + self._register_function(function_descriptor) + + def _register_function(self, function_descriptor: _FunctionDescriptor) -> None: + """[internal] Build a function ctypes wrapping from its function descriptor. + + Args: + function_descriptor: An instance of a _FunctionDescriptor that describes a function ctypes wrapping. + + Raises: + AttributeError: A given function was not found. + """ + try: + function_pointer = getattr(self._dbghelp, function_descriptor.name) + except AttributeError: + # We land here if the function can't be found in the given DLL. + # note: it raises from quite deep inside ctypes if the function can't be resolved, which might be confusing. + # Log it now and re-raise. + logger.error(f"The function {function_descriptor.name} was not found in the DLL: '{self._dll_path!r}'.") + raise + if function_descriptor.parameter_types: + function_pointer.argtypes = function_descriptor.parameter_types + if function_descriptor.return_type: + function_pointer.restype = function_descriptor.return_type + self._functions.update({function_descriptor.name: function_pointer}) + if function_descriptor.aliases: + for alias in function_descriptor.aliases: + self._functions.update({alias: function_pointer}) diff --git a/procmon_parser/symbol_resolver/win/kernel32.py b/procmon_parser/symbol_resolver/win/kernel32.py new file mode 100644 index 0000000..57910ba --- /dev/null +++ b/procmon_parser/symbol_resolver/win/kernel32.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import ctypes +from .win_types import BOOL, LPWSTR + +_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + +# SetEnvironmentVariableW +# https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew +SetEnvironmentVariableW = _kernel32.SetEnvironmentVariableW +SetEnvironmentVariableW.argtypes = (LPWSTR, LPWSTR) +SetEnvironmentVariableW.restype = BOOL +SetEnvironmentVariable = SetEnvironmentVariableW diff --git a/procmon_parser/symbol_resolver/win/win_types.py b/procmon_parser/symbol_resolver/win/win_types.py new file mode 100644 index 0000000..f18ba52 --- /dev/null +++ b/procmon_parser/symbol_resolver/win/win_types.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +"""Module that declares various Windows types for ctypes. +""" +import ctypes + +HANDLE = ctypes.c_void_p +BOOL = ctypes.c_long +PCSTR = PCWSTR = PSTR = PWSTR = LPWSTR = ctypes.c_wchar_p +DWORD = ctypes.c_uint32 +DWORD64 = ctypes.c_uint64 +ULONG = ctypes.c_uint32 +ULONG64 = ctypes.c_uint64 +CHAR = ctypes.c_char +WCHAR = ctypes.c_wchar + +# pointer types +PVOID = ctypes.c_void_p +PDWORD = ctypes.POINTER(DWORD) +PDWORD64 = ctypes.POINTER(DWORD64) From b4858ae397467424ff6bef33447889dc2d5c3328 Mon Sep 17 00:00:00 2001 From: neitsa Date: Mon, 13 Feb 2023 12:41:04 +0100 Subject: [PATCH 03/26] Add symbol resolver. --- procmon_parser/symbol_resolver/__init__.py | 0 .../symbol_resolver/symbol_resolver.py | 561 ++++++++++++++++++ 2 files changed, 561 insertions(+) create mode 100644 procmon_parser/symbol_resolver/__init__.py create mode 100644 procmon_parser/symbol_resolver/symbol_resolver.py diff --git a/procmon_parser/symbol_resolver/__init__.py b/procmon_parser/symbol_resolver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py new file mode 100644 index 0000000..f5776e7 --- /dev/null +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import ctypes +import dataclasses +import enum +import logging +import os +import platform +import re +import sys +import winreg + +import procmon_parser + +if sys.platform != "win32": + raise RuntimeError("Symbol Resolver can only be used on Windows Operating Systems.") + +if sys.version_info < (3, 5, 0): + raise RuntimeError("Symbol Resolver can only be called from python 3.5 +.") + +import pathlib +import typing + +from procmon_parser.symbol_resolver.win.dbghelp import ( + DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT) +from procmon_parser.symbol_resolver.win.win_types import PVOID, HANDLE, DWORD64, DWORD + + +if typing.TYPE_CHECKING: + from procmon_parser import ProcmonLogsReader + from procmon_parser.logs import Event + from procmon_parser.logs import Module + +logger = logging.getLogger(__name__) + + +@enum.unique +class FrameType(enum.Enum): + KERNEL = enum.auto() + USER = enum.auto() + + @staticmethod + def from_address(address: int, max_user_address: int): + return FrameType.KERNEL if address > max_user_address else FrameType.USER + + +@dataclasses.dataclass +class StackTraceFrameInformation: + frame_type: FrameType + frame_number: int + address: int + module: procmon_parser.Module | None = None + symbol_info: SYMBOL_INFOW | None = None + displacement: int | None = None + line_info: IMAGEHLP_LINEW64 | None = None + line_displacement: int | None = None + source_file_path: pathlib.Path | None = None + + def __post_init__(self): + if self.line_info: + logger.debug(f"StackTraceFrameInformation: {self.line_info.FileName}") + + @property + def frame(self) -> str: + return f"{self.frame_type.name[0]} {self.frame_number}" + + @property + def location(self) -> str: + if self.symbol_info is None: + return f"{self.address:#x}" + + # symbolic information (symbol + asm offset) + sym_str = f"{self.symbol_info.Name} + {self.displacement:#x}" + if self.line_info is None: + return sym_str + + # line information + path = self.source_file_path if self.source_file_path else self.line_info.FileName + line_str = f"{path} ({self.line_info.LineNumber}; col: {self.line_displacement})" + + return f"{sym_str}, {line_str}" + + @property + def module_name(self) -> str: + if not self.module.path: + return "" + + return pathlib.Path(self.module.path).name + + @property + def path(self) -> str: + if not self.module.path: + return "" + + return self.module.path + + def __repr__(self) -> str: + return f"{self.frame} {self.module_name} {self.location} {self.address:#x} {self.path}" + + +class StackTraceInformation: + @staticmethod + def pretty_print(resolved_stack_trace: list[StackTraceFrameInformation]): + max_frame = max([len(ssi.frame) for ssi in resolved_stack_trace]) + max_module = max([len(ssi.module_name) for ssi in resolved_stack_trace]) + max_location = max([len(ssi.location) for ssi in resolved_stack_trace]) + max_address = max([len(f"{ssi.address:#x}") for ssi in resolved_stack_trace]) + + output = list() + for ssi in resolved_stack_trace: + output.append(f"{ssi.frame:<{max_frame}} {ssi.module_name:<{max_module}} {ssi.location:<{max_location}} " + f"0x{ssi.address:<{max_address}x} {ssi.path}") + + return '\n'.join(output) + + +class SymbolResolver: + def __init__(self, + plr: "ProcmonLogsReader", + dll_dir_path: str | pathlib.Path | None = None, + skip_symsrv: bool = False) -> None: + # Check if we can find the needed DLLs if not path has been provided. + # Both DLLs are needed to resolve symbolic information. + # * 'dbghelp.dll' contains the functionalities to resolve symbols. + # * 'symsrv.dll' downloads symbol from the symbol store. + if dll_dir_path is None: + dll_dir_path = next( + (v for v in [DbgHelpUtils.find_debugging_tools(), DbgHelpUtils.find_windbg_preview()] if v is not None), + None) + if not dll_dir_path: + raise ValueError("You need to provide a valid path to 'dbghelp.dll' and 'symsrv.dll'.") + else: + # just check that the given dir contains dbghelp and symsrv. + if not dll_dir_path.is_dir(): + raise ValueError(f"The given path '{dll_dir_path}' is not a directory.") + files_to_check = ["dbghelp.dll"] + if not skip_symsrv: + files_to_check.append("symsrv.dll") + if not all((dll_dir_path / file_name).is_file() for file_name in files_to_check): + raise ValueError(f"The given path must be a path to a directory containing: {files_to_check!r}.") + self.dll_dir_path = dll_dir_path + + # _NT_SYMBOL_PATH is needed to store symbols locally. If it's not set, we need to set it. + symbol_path = os.environ.get("_NT_SYMBOL_PATH", None) + if symbol_path is None: + # resolve TEMP folder and set it at the symbol path. + os.environ["_NT_SYMBOL_PATH"] = f"srv*{os.environ['TEMP']}https://msdl.microsoft.com/download/symbols" + logger.debug(f"NT_SYMBOL_PATH: {os.environ['_NT_SYMBOL_PATH']}") + + # DbgHelp wrapper instance initialisation + self._dbghelp = DbgHelp(self.dll_dir_path / "dbghelp.dll") + self._dbghelp.SymSetOptions( + SYMOPT.SYMOPT_CASE_INSENSITIVE | SYMOPT.SYMOPT_UNDNAME | SYMOPT.SYMOPT_DEFERRED_LOADS | + SYMOPT.SYMOPT_LOAD_LINES | SYMOPT.SYMOPT_OMAP_FIND_NEAREST | SYMOPT.SYMOPT_FAIL_CRITICAL_ERRORS | + SYMOPT.SYMOPT_INCLUDE_32BIT_MODULES | SYMOPT.SYMOPT_AUTO_PUBLICS) # 0x12237. + self._dbghelp_pid = 0 # TODO: get rid of it, just call SymCleanup when exiting from resolve_stack_trace() + + # maximum user-address, used to discern between user and kernel modules (which don't change between processes). + self._max_user_address: int = plr.maximum_application_address + + # Keep track of all system modules. + for process in plr.processes(): + # Can't remember if System pid has always been 4. + # Just check its name (doesn't end with .exe) and Company. That's foolproof enough. + if process.process_name in ["System"] and process.company.lower().startswith("microsoft"): + self.system_modules = process.modules + break + + def __del__(self): + # TODO: remove this if you remove self._dbghelp_pid + if self._dbghelp_pid != 0: + self._dbghelp.SymCleanup(self._dbghelp_pid) + + def find_module(self, event, address: int) -> procmon_parser.Module | None: + """Try to find the corresponding module given an address from an event stack trace. + + Args: + event: The event from which the address belongs to. + address: The address to be resolved to its containing module. + + Returns: + If the address lies inside a known module, the module is returned, otherwise the function returns None. + """ + def is_kernel(addr: int) -> bool: + return addr > self._max_user_address + + def find_module_from_list(addr: int, modules: list['Module']) -> typing.Optional['Module']: + for m in modules: + base = m.base_address + end = m.base_address + m.size + if base <= addr < end: + return m + return None + + # get the right modules depending on the address type. + # kernel address: check modules in the system process. + # user land address: check modules in the process itself. + module_source = self.system_modules if is_kernel(address) else event.process.modules + module = find_module_from_list(address, module_source) + return module # may be None. + + def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrameInformation]: + """Resolve the stack trace of an event to include symbolic information. + + Args: + event: The event for which the stack trace should be resolved. + + Notes: + The `ProcmonLogsReader` instance must be instantiated with `should_get_stacktrace` set to True (default). + + Examples: + p = pathlib.Path(r"C:\temp\Logfile.PML") + + with p.open("rb") as f: + log_reader = ProcmonLogsReader(f, should_get_stacktrace=True) + sym_resolver = SymbolResolver(log_reader) + for i, event in enumerate(plr): + print(f"{i:04x} {event!r}") + sym_resolver.resolve_stack_trace(event) + + + Returns: + TODO: stack trace information. + """ + if not event.stacktrace or event.stacktrace is None: + raise RuntimeError("Trying to resolve a stack trace while there is no stack trace.") + + # keep track of dbghelp initialization with the given process. + pid = event.process.pid + if self._dbghelp_pid != 0: + # TODO: don't cleanup if it's the same pid, and the same process? + self._dbghelp.SymCleanup(self._dbghelp_pid) + self._dbghelp.SymInitialize(pid, None, False) + self._dbghelp_pid = pid + + logger.debug(f"Stack Trace frames: {len(event.stacktrace)}") + logger.debug(f"PID: {pid:#08x}") + + # Resolve each of the addresses in the stack trace, frame by frame. + for frame_number, address in enumerate(event.stacktrace): + frame_type = FrameType.from_address(address, self._max_user_address) + logger.debug(f"{'-' * 79}\nStack Frame: {frame_number:04} type: {frame_type}") + + # find the module that contains the given address. It might not be found. + logger.debug(f"Address: {address:#016x}") + module = self.find_module(event, address) + if not module: + yield StackTraceFrameInformation(frame_type, frame_number, address) + continue + + logger.debug(f"Address: {address:#016x} --> Module: {module!r}") + + # We have the address and the module name. Get the corresponding file from the Symbol store! + # Once we have the file, we'll be able to query the symbol for the address. + found_file = ctypes.create_unicode_buffer(260 * 2) + module_id = PVOID(module.timestamp) + search_path = None # use the default search path provided to SymInitialize. + + # We give it two tries: + # 1. The module is an MS module, in which case it's going to be resolved pretty much automatically. + # 1.a if it's not an MS module it's going to fail. + # 2. If it's not an MS module, then we indicate to SymFindFileInPath where to find the binary in SearchPath. + for j in range(2): + ret_val = self._dbghelp.SymFindFileInPathW( + HANDLE(pid), # hProcess + search_path, # SearchPath + module.path, # FileName (PCWSTR: it's fine to pass a python string) + ctypes.byref(module_id), # id + module.size, # two + 0, # three + SSRVOPT.SSRVOPT_GUIDPTR, # flags: ProcMon uses 'SSRVOPT_GUIDPTR' but it's not a GUID??? still works + found_file, # [out] FoundFile + PFINDFILEINPATHCALLBACK(0), # callback (nullptr) + None # context + ) + if not ret_val: + last_err = ctypes.get_last_error() + logger.debug(f"SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).") + if j == 0 and last_err == 2: # ERROR_FILE_NOT_FOUND + # 1st try and file was not found: check if the directory exists. If it is give it another try. + dir_path = pathlib.Path(module.path).parent + if dir_path.is_dir(): + # loop again. + search_path = str(dir_path) + else: + # the file does not exist on the local computer; just get out. + break + else: + # no more tries left or unknown error. + logger.error(f"SymFindFileInPathW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + break + else: + # no error. + break + + if not found_file.value: + yield StackTraceFrameInformation(frame_type, frame_number, address, module) + continue + + logger.debug(f"Found file: {found_file.value}") + + # We have the file from the symbol store, we now 'load' the symbolic module (it does not load it inside + # the process address space) to be able to query the symbol right after that. + module_base = self._dbghelp.SymLoadModuleExW( + pid, # hProcess + None, # hFile + found_file, # ImageName + None, # ModuleName + module.base_address, # BaseOfDll + module.size, # DllSize + None, # Data (nullptr) + 0 # Flags + ) + if module_base == 0: + # the function return 0 (FALSE) and GetLastError will also return 0 if there was no error, but the + # module was already loaded. This is not an error in this case. + last_err = ctypes.get_last_error() + if last_err != 0: # if it's not 0, then it's really an error. + logger.error(f"SymLoadModuleExW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + yield StackTraceFrameInformation(frame_type, frame_number, address, module) + continue + + logger.debug(f"Module Base: {module_base:#x}") + + # Now that we have loaded the symbolic module, we query it with the address (lying inside it) to get the + # name of the symbol and the displacement from the symbol (if any). + displacement = DWORD64(0) + symbol_info = SYMBOL_INFOW() + symbol_info.MaxNameLen = SYMBOL_INFOW.BUFFER_NUM_ELEMENTS + symbol_info.SizeOfStruct = 0x58 + ret_val = self._dbghelp.SymFromAddr( + pid, # hProcess + address, # Address of the symbol + ctypes.byref(displacement), # [out] displacement from the base of the symbol. e.g. 'foo + 0x10' + ctypes.byref(symbol_info) # [in, out] symbol information. + ) + if ret_val == 0: + last_err = ctypes.get_last_error() + logger.error(f"SymFromAddr: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + yield StackTraceFrameInformation(frame_type, frame_number, address, module) + continue + + logger.debug(f"Symbol Name: {symbol_info.Name}; Displacement: {displacement.value:#08x}") + + # In case we have source information, we need to continue to query the symbol to get source information such + # as the source file name and the line number. This obviously fails if there are no symbolic source code + # information. + line_displacement = DWORD(0) + line = IMAGEHLP_LINEW64() + line.SizeOfStruct = ctypes.sizeof(IMAGEHLP_LINEW64) + ret_val = self._dbghelp.SymGetLineFromAddrW64( + pid, # hProcess + address, # Address + ctypes.byref(line_displacement), # Displacement + ctypes.byref(line) # [out] Line + ) + # The above call fails if there are no source code information. This is the default for Windows binaries. + if ret_val == 0: + last_err = ctypes.get_last_error() + logger.debug(f"SymGetLineFromAddrW64 [no source line]: ({last_err:#08x}) " + f"{ctypes.FormatError(last_err)}") + yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, + displacement.value) + continue + + # FIX: If you don't copy the line.FileName buffer, it gets overwritten in the next call to + # SymGetLineFromAddrW64(). After much debugging, the solution is in fact currently written in the + # SymGetLineFromAddrW64() documentation: + # This function returns a pointer to a buffer that may be reused by another function. Therefore, be + # sure to copy the data returned to another buffer immediately. + # The following 2 lines just do that. + file_name = ctypes.create_unicode_buffer(line.FileName) + line.FileName = ctypes.cast(file_name, ctypes.c_wchar_p) + + logger.debug(f"File Name: '{line.FileName}'; Line Number: {line.LineNumber}; " + f"Line Displacement (col): {line_displacement.value}") + + source_file_path_size = DWORD(260) + source_file_path = ctypes.create_unicode_buffer(source_file_path_size.value) + ret_val = self._dbghelp.SymGetSourceFileW( + pid, # hProcess + module.base_address, # Base + None, # Params (never used) + line.FileName, # FileSpec (name of source file) [PCWSTR] + source_file_path, # [out] FilePath: fully qualified path of source file + source_file_path_size # FilePath size (num chars) + ) + if ret_val == 0: + last_err = ctypes.get_last_error() + logger.debug(f"SymGetSourceFileW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + logger.debug(f"--> FileName: {line.FileName}") + yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, + displacement.value, line, line_displacement.value) + continue + + logger.debug(f"source file path: {source_file_path.value}") + + yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value, + line, line_displacement.value, source_file_path.value) + + +class DbgHelpUtils: + """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg + preview are installed on the current system. + """ + @staticmethod + def find_debugging_tools() -> pathlib.Path | None: + """Find the path of the directory containing DbgHelp.dll and Symsrv.dll from the Debugging Tools For Windows + (installed from the Windows SDK). + + Returns: + The path to the DLLs directory (that corresponds to the interpreter architecture) , or None if the Debugging + Tools for Windows are not installed. + """ + sdk_key = r"SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots" + debugger_roots = list() + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sdk_key) as top_key: + _, num_values, _ = winreg.QueryInfoKey(top_key) + for i in range(num_values): + value_name, _, _ = winreg.EnumValue(top_key, i) + if value_name.startswith("{"): # skip GUIDs + continue + if "windowsdebuggersroot" in value_name.lower(): + # 'WindowsDebuggerRoot' key is followed by the SDK major number, i.e. 'WindowsDebuggersRoot10'. + debugger_roots.append(value_name) + except OSError: + return None + + if not debugger_roots: + return None + + # we have the debugger roots, we need to find the latest version. 11 > 10 > 81 > 80 > 7 ... + versions = {} + for debugger_root in debugger_roots: + match = re.search(r"WindowsDebuggersRoot(\d+)", debugger_root) + if not match: + return None + version = float(match.group(1)) + if version > 20.0: + version = version / 10.0 # e.g. 81 -> 8.1 + versions.update({version: match.group(1)}) + + if not versions: + return None + + max_ver = max(versions.keys()) + max_ver_str = versions[max_ver] + + debugger_path: pathlib.Path | None = None + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sdk_key) as top_key: + value, value_type = winreg.QueryValueEx(top_key, f"WindowsDebuggersRoot{max_ver_str}") + if value_type == winreg.REG_SZ: + debugger_path = pathlib.Path(value) + except OSError: + return None + + if not debugger_path or not debugger_path.is_dir(): + return None + + # we have found Windbg installation path; we need to get the correct architecture directory. + lookup = { + "amd64": "x64", + "win32": "x86", + "arm64": "arm64", + "arm32": "arm" + } + + return DbgHelpUtils._arch_dir(debugger_path, lookup) + + @staticmethod + def find_windbg_preview() -> pathlib.Path | None: + """Find the directory path of the DbgHelp.dll and Symsrv.dll from the Windbg preview installation (installed + from the Windows Store). + + Returns: + The path to the DLLs directory (that corresponds to the interpreter architecture), or None if Windbg Preview + directory couldn't be found. + """ + package_key = (r"SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel" + r"\Repository\Packages") + + windbg_location: pathlib.Path | None = None + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, package_key) as top_key: + num_keys, _, _ = winreg.QueryInfoKey(top_key) + for i in range(num_keys): + key_name = winreg.EnumKey(top_key, i) + if "microsoft.windbg" in key_name.lower(): + # found Windbg Preview. Get its installation location. + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, f"{package_key}\\{key_name}") as windbg_key: + install_location, key_type = winreg.QueryValueEx(windbg_key, "PackageRootFolder") + if key_type == winreg.REG_SZ: + windbg_location = pathlib.Path(install_location) + break + except OSError: + return None + + if windbg_location is None or windbg_location.is_dir(): + return None + + # we have found the installation path; we need to get the correct architecture directory. + # One of: 'x86', 'amd64' or 'arm64' (there's no 'arm32' support in Windbg Preview). + lookup = { + "amd64": "amd64", + "win32": "x86", + "arm64": "arm64", + # note: Windbg preview doesn't support arm32. + } + + return DbgHelpUtils._arch_dir(windbg_location, lookup) + + @staticmethod + def _arch_dir(debugger_dir: pathlib.Path, arch_lookup: dict[str, str]) -> pathlib.Path | None: + """[internal] Get the path to the right DLLs (depending on the architecture used by the python interpreter). + + Args: + debugger_dir: The top level directory of the debugger (Windbg / Windbg Preview). + arch_lookup: A dictionary which translate the system architecture to a folder name in the Windbg + installation. + + Returns: + The correct path to the DLLs (dbghelp & symsrv) directory. None if the directory couldn't be found. + """ + machine = platform.machine().lower() + bitness = arch_lookup.get(machine, None) + if bitness is None: + return None + arch_dir = debugger_dir / bitness + if not arch_dir.is_dir(): + return None + + # check that there are both 'dbghelp.dll' and 'symsrv.dll' in the given directory. + if not all((arch_dir / file_name).is_file() for file_name in ("symsrv.dll", "dbghelp.dll")): + return None + + return arch_dir + + +if __name__ == "__main__": + from procmon_parser import ProcmonLogsReader + import pathlib + + logging.basicConfig(level=logging.DEBUG) + + p = pathlib.Path(r"G:\Appdata\Python\procmon-parser\tests\src\FileSystemOperations\x64\Release\Logfile.PML") + + with p.open("rb") as f: + plr = ProcmonLogsReader(f) + sym = SymbolResolver(plr) + for idx, ev in enumerate(plr): + if idx == 68: + # input("press to continue") + # for frame_info in sym.resolve_stack_trace(ev): + # print(f"{frame_info!r}") + + frames = list(sym.resolve_stack_trace(ev)) + print(StackTraceInformation.pretty_print(frames)) + + From f8f86a0f00e5ed652c65bd9d159314ed6e0983bf Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 12:01:40 +0100 Subject: [PATCH 04/26] Remove __main__; remove __post__init__ in StackTraceFrameInformation. --- .../symbol_resolver/symbol_resolver.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index f5776e7..f067758 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -56,10 +56,6 @@ class StackTraceFrameInformation: line_displacement: int | None = None source_file_path: pathlib.Path | None = None - def __post_init__(self): - if self.line_info: - logger.debug(f"StackTraceFrameInformation: {self.line_info.FileName}") - @property def frame(self) -> str: return f"{self.frame_type.name[0]} {self.frame_number}" @@ -536,26 +532,3 @@ def _arch_dir(debugger_dir: pathlib.Path, arch_lookup: dict[str, str]) -> pathli return None return arch_dir - - -if __name__ == "__main__": - from procmon_parser import ProcmonLogsReader - import pathlib - - logging.basicConfig(level=logging.DEBUG) - - p = pathlib.Path(r"G:\Appdata\Python\procmon-parser\tests\src\FileSystemOperations\x64\Release\Logfile.PML") - - with p.open("rb") as f: - plr = ProcmonLogsReader(f) - sym = SymbolResolver(plr) - for idx, ev in enumerate(plr): - if idx == 68: - # input("press to continue") - # for frame_info in sym.resolve_stack_trace(ev): - # print(f"{frame_info!r}") - - frames = list(sym.resolve_stack_trace(ev)) - print(StackTraceInformation.pretty_print(frames)) - - From 60fd8402a018c557fd757f8403a04c311bed0343 Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 12:50:14 +0100 Subject: [PATCH 05/26] Add comments; Rename function: * Add module, class and functions comments. * Rename StackTraceInformation.Pretty_print() to prettify() since it doesn't print anything. --- .../symbol_resolver/symbol_resolver.py | 108 +++++++++++++++--- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index f067758..4cea04b 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # -*- coding:utf-8 -*- +"""Module used to resolve symbolic information for a given a stack trace. +""" import ctypes import dataclasses import enum @@ -36,32 +38,60 @@ @enum.unique class FrameType(enum.Enum): + """Type of frame in a stack trace. Either User (the frame lies in user mode) or Kernel (the frame lies in kernel + mode). + """ KERNEL = enum.auto() USER = enum.auto() @staticmethod - def from_address(address: int, max_user_address: int): + def from_address(address: int, max_user_address: int) -> "FrameType": + """Get the type of frame given an address and the maximum possible user address. + + Args: + address: The address for which the FrameType should be obtained. + max_user_address: The maximum possible user address. + + Returns: + A `FrameType` which corresponds to the given address. + """ return FrameType.KERNEL if address > max_user_address else FrameType.USER @dataclasses.dataclass class StackTraceFrameInformation: + """Contain various symbolic information about a frame in a stacktrace. + """ + #: Type of the frame, either Kernel or User. frame_type: FrameType + #: The frame number (its position in the stack trace). frame_number: int + #: Address at which the frame happens. address: int + #: The module inside which the frame happens. module: procmon_parser.Module | None = None + #: Symbolic information about the frame. symbol_info: SYMBOL_INFOW | None = None + #: The displacement in regard to the symbol. + #: For example if the symbol name is 'foo' and the displacement is 0x10, then the frame happened at 'foo + 0x10'. displacement: int | None = None + #: Line information in regard to the symbol (available only if symbolic source information is present). line_info: IMAGEHLP_LINEW64 | None = None + #: Displacement from the source code line (that is, the column in the source code line). line_displacement: int | None = None + #: The source code full file path at which the frame happened. source_file_path: pathlib.Path | None = None @property def frame(self) -> str: + """Return a string representation of a frame (its `FrameType` and frame number). + """ return f"{self.frame_type.name[0]} {self.frame_number}" @property def location(self) -> str: + """Return a string representation of the symbolic location at which the frame happens. + """ if self.symbol_info is None: return f"{self.address:#x}" @@ -78,6 +108,8 @@ def location(self) -> str: @property def module_name(self) -> str: + """Return a string representation of the frame main module name. + """ if not self.module.path: return "" @@ -85,6 +117,8 @@ def module_name(self) -> str: @property def path(self) -> str: + """Return a string representation of the frame main module fully qualified path. + """ if not self.module.path: return "" @@ -95,8 +129,22 @@ def __repr__(self) -> str: class StackTraceInformation: + """Class used to prettify a whole stack trace so its output if similar to ProcMon's stack trace window tab for an + event. + """ @staticmethod - def pretty_print(resolved_stack_trace: list[StackTraceFrameInformation]): + def prettify(resolved_stack_trace: list[StackTraceFrameInformation]) -> str: + """Prettify a list of `StackTraceFrameInformation` so it's output is similar to the one given by ProcMon. + + Args: + resolved_stack_trace: A list of stack trace frame information. + + Returns: + A string that match closely the output of a stack trace from ProcMon. + """ + if not resolved_stack_trace: + return "" + max_frame = max([len(ssi.frame) for ssi in resolved_stack_trace]) max_module = max([len(ssi.module_name) for ssi in resolved_stack_trace]) max_location = max([len(ssi.location) for ssi in resolved_stack_trace]) @@ -111,10 +159,29 @@ def pretty_print(resolved_stack_trace: list[StackTraceFrameInformation]): class SymbolResolver: + """Main workhorse class for resolving symbolic information from a stack trace. + """ def __init__(self, - plr: "ProcmonLogsReader", + procmon_logs_reader: "ProcmonLogsReader", dll_dir_path: str | pathlib.Path | None = None, skip_symsrv: bool = False) -> None: + """Class Initialisation. + + Args: + procmon_logs_reader: An instance of the `ProcmonLogsReader` class. + dll_dir_path: Path to a directory containing at least `dbghelp.dll`, and optionally `symsrv.dll`. + skip_symsrv: Set to True if symbols are available locally on the machine and `_NT_SYMBOL_PATH` environment + variable is correctly set. This skips the check for `symsrv.dll` presence altogether. + + Notes: + If `dll_dir_path` is None, then the code does its best to find matching installations of the Debugging Tools + for Windows (can be installed from the Windows SDK) and Windbg Preview (installed from the Windows Store). + If neither can be found, the function raises. + + Raises: + ValueError: The provided DLL path is not a valid directory, does not contain the required DLL(s) or the + automatic finder could not find the required DLL. + """ # Check if we can find the needed DLLs if not path has been provided. # Both DLLs are needed to resolve symbolic information. # * 'dbghelp.dll' contains the functionalities to resolve symbols. @@ -143,7 +210,7 @@ def __init__(self, os.environ["_NT_SYMBOL_PATH"] = f"srv*{os.environ['TEMP']}https://msdl.microsoft.com/download/symbols" logger.debug(f"NT_SYMBOL_PATH: {os.environ['_NT_SYMBOL_PATH']}") - # DbgHelp wrapper instance initialisation + # DbgHelp wrapper instance initialisation and symbolic option setting. self._dbghelp = DbgHelp(self.dll_dir_path / "dbghelp.dll") self._dbghelp.SymSetOptions( SYMOPT.SYMOPT_CASE_INSENSITIVE | SYMOPT.SYMOPT_UNDNAME | SYMOPT.SYMOPT_DEFERRED_LOADS | @@ -152,12 +219,12 @@ def __init__(self, self._dbghelp_pid = 0 # TODO: get rid of it, just call SymCleanup when exiting from resolve_stack_trace() # maximum user-address, used to discern between user and kernel modules (which don't change between processes). - self._max_user_address: int = plr.maximum_application_address + self._max_user_address: int = procmon_logs_reader.maximum_application_address # Keep track of all system modules. - for process in plr.processes(): + for process in procmon_logs_reader.processes(): # Can't remember if System pid has always been 4. - # Just check its name (doesn't end with .exe) and Company. That's foolproof enough. + # Just check its name (doesn't end with .exe) and company is MS. That should be foolproof enough. if process.process_name in ["System"] and process.company.lower().startswith("microsoft"): self.system_modules = process.modules break @@ -178,9 +245,12 @@ def find_module(self, event, address: int) -> procmon_parser.Module | None: If the address lies inside a known module, the module is returned, otherwise the function returns None. """ def is_kernel(addr: int) -> bool: + """[Internal] Return whether an address is kernel (True) or not (user mode address: False).""" return addr > self._max_user_address def find_module_from_list(addr: int, modules: list['Module']) -> typing.Optional['Module']: + """[Internal] Return an instance of a Module given an address (if the address lies inside the module). + """ for m in modules: base = m.base_address end = m.base_address + m.size @@ -204,19 +274,25 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame Notes: The `ProcmonLogsReader` instance must be instantiated with `should_get_stacktrace` set to True (default). + Raises: + RuntimeError: the given event des not contain any stack trace information. Be sure to call + `ProcmonLogsReader` with the `should_get_stacktrace` parameter set to True. + Examples: + ```python p = pathlib.Path(r"C:\temp\Logfile.PML") with p.open("rb") as f: log_reader = ProcmonLogsReader(f, should_get_stacktrace=True) sym_resolver = SymbolResolver(log_reader) - for i, event in enumerate(plr): + for i, event in enumerate(log_reader): print(f"{i:04x} {event!r}") - sym_resolver.resolve_stack_trace(event) - + frames = list(sym_resolver.resolve_stack_trace(event)) + print(StackTraceInformation.prettify(frames)) + ``` - Returns: - TODO: stack trace information. + Yields: + An instance of `StackTraceFrameInformation` for each of the frame in the stack trace. """ if not event.stacktrace or event.stacktrace is None: raise RuntimeError("Trying to resolve a stack trace while there is no stack trace.") @@ -273,13 +349,13 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame last_err = ctypes.get_last_error() logger.debug(f"SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).") if j == 0 and last_err == 2: # ERROR_FILE_NOT_FOUND - # 1st try and file was not found: check if the directory exists. If it is give it another try. + # 1st try and file was not found: check if the directory exists. If it is, give it another try. dir_path = pathlib.Path(module.path).parent if dir_path.is_dir(): - # loop again. + # directory exists; loop and try again. search_path = str(dir_path) else: - # the file does not exist on the local computer; just get out. + # the directory doesn't contain the required file on the local computer; just get out. break else: # no more tries left or unknown error. @@ -365,7 +441,7 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame # This function returns a pointer to a buffer that may be reused by another function. Therefore, be # sure to copy the data returned to another buffer immediately. # The following 2 lines just do that. - file_name = ctypes.create_unicode_buffer(line.FileName) + file_name = ctypes.create_unicode_buffer(line.FileName) # noqa line.FileName = ctypes.cast(file_name, ctypes.c_wchar_p) logger.debug(f"File Name: '{line.FileName}'; Line Number: {line.LineNumber}; " From b18d59fb62fb7f9227c74d0a609f3c32991bc623 Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 14:16:37 +0100 Subject: [PATCH 06/26] Add conditional import in procmon_parser init module. --- procmon_parser/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/procmon_parser/__init__.py b/procmon_parser/__init__.py index 45a88ae..3322365 100644 --- a/procmon_parser/__init__.py +++ b/procmon_parser/__init__.py @@ -1,3 +1,5 @@ +import sys + from six import PY2 from procmon_parser.configuration import * @@ -11,6 +13,12 @@ 'Rule', 'Column', 'RuleAction', 'RuleRelation', 'PMLError' ] +if sys.platform == "win32" and sys.version_info >= (3, 5, 0): + from procmon_parser.symbol_resolver.symbol_resolver import ( + SymbolResolver, StackTraceFrameInformation, StackTraceInformation) + + __all__.extend(['SymbolResolver', 'StackTraceFrameInformation', 'StackTraceInformation']) + class ProcmonLogsReader(object): """Reads procmon logs from a stream which in the PML format From 1e24bd9177b9909d52b5b9acd21f6b704534ffd4 Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 14:24:52 +0100 Subject: [PATCH 07/26] Change dbghelp initialization and cleanup. --- .../symbol_resolver/symbol_resolver.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 4cea04b..43717fd 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -216,7 +216,6 @@ def __init__(self, SYMOPT.SYMOPT_CASE_INSENSITIVE | SYMOPT.SYMOPT_UNDNAME | SYMOPT.SYMOPT_DEFERRED_LOADS | SYMOPT.SYMOPT_LOAD_LINES | SYMOPT.SYMOPT_OMAP_FIND_NEAREST | SYMOPT.SYMOPT_FAIL_CRITICAL_ERRORS | SYMOPT.SYMOPT_INCLUDE_32BIT_MODULES | SYMOPT.SYMOPT_AUTO_PUBLICS) # 0x12237. - self._dbghelp_pid = 0 # TODO: get rid of it, just call SymCleanup when exiting from resolve_stack_trace() # maximum user-address, used to discern between user and kernel modules (which don't change between processes). self._max_user_address: int = procmon_logs_reader.maximum_application_address @@ -229,11 +228,6 @@ def __init__(self, self.system_modules = process.modules break - def __del__(self): - # TODO: remove this if you remove self._dbghelp_pid - if self._dbghelp_pid != 0: - self._dbghelp.SymCleanup(self._dbghelp_pid) - def find_module(self, event, address: int) -> procmon_parser.Module | None: """Try to find the corresponding module given an address from an event stack trace. @@ -297,15 +291,11 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame if not event.stacktrace or event.stacktrace is None: raise RuntimeError("Trying to resolve a stack trace while there is no stack trace.") - # keep track of dbghelp initialization with the given process. + # Initialize dbghelp symbolic information. pid = event.process.pid - if self._dbghelp_pid != 0: - # TODO: don't cleanup if it's the same pid, and the same process? - self._dbghelp.SymCleanup(self._dbghelp_pid) self._dbghelp.SymInitialize(pid, None, False) - self._dbghelp_pid = pid - logger.debug(f"Stack Trace frames: {len(event.stacktrace)}") + logger.debug(f"# Stack Trace frames: {len(event.stacktrace)}") logger.debug(f"PID: {pid:#08x}") # Resolve each of the addresses in the stack trace, frame by frame. @@ -470,6 +460,9 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value, line, line_displacement.value, source_file_path.value) + # dbghelp symbol cleanup + self._dbghelp.SymCleanup(pid) + class DbgHelpUtils: """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg From 8f3ccb99420d379aea14873508c81a6ffa466ef8 Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 14:29:21 +0100 Subject: [PATCH 08/26] Fix _NT_SYMBOL_PATH wrong path. --- procmon_parser/symbol_resolver/symbol_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 43717fd..363666a 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -207,7 +207,7 @@ def __init__(self, symbol_path = os.environ.get("_NT_SYMBOL_PATH", None) if symbol_path is None: # resolve TEMP folder and set it at the symbol path. - os.environ["_NT_SYMBOL_PATH"] = f"srv*{os.environ['TEMP']}https://msdl.microsoft.com/download/symbols" + os.environ["_NT_SYMBOL_PATH"] = f"srv*{os.environ['TEMP']}*https://msdl.microsoft.com/download/symbols" logger.debug(f"NT_SYMBOL_PATH: {os.environ['_NT_SYMBOL_PATH']}") # DbgHelp wrapper instance initialisation and symbolic option setting. From 869ba9d4d34920b3e5356602d055cb11a542c0cc Mon Sep 17 00:00:00 2001 From: neitsa Date: Tue, 14 Feb 2023 14:42:04 +0100 Subject: [PATCH 09/26] Fix: prevent None deref if no module information is present. This happens notably in the stack trace during process creation. rename local variable to a more meaningful name. --- .../symbol_resolver/symbol_resolver.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 363666a..b0a0b36 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -110,22 +110,22 @@ def location(self) -> str: def module_name(self) -> str: """Return a string representation of the frame main module name. """ - if not self.module.path: + if self.module is None or not self.module.path: return "" return pathlib.Path(self.module.path).name @property - def path(self) -> str: + def module_path(self) -> str: """Return a string representation of the frame main module fully qualified path. """ - if not self.module.path: + if self.module is None or not self.module.path: return "" return self.module.path def __repr__(self) -> str: - return f"{self.frame} {self.module_name} {self.location} {self.address:#x} {self.path}" + return f"{self.frame} {self.module_name} {self.location} {self.address:#x} {self.module_path}" class StackTraceInformation: @@ -145,15 +145,15 @@ def prettify(resolved_stack_trace: list[StackTraceFrameInformation]) -> str: if not resolved_stack_trace: return "" - max_frame = max([len(ssi.frame) for ssi in resolved_stack_trace]) - max_module = max([len(ssi.module_name) for ssi in resolved_stack_trace]) - max_location = max([len(ssi.location) for ssi in resolved_stack_trace]) - max_address = max([len(f"{ssi.address:#x}") for ssi in resolved_stack_trace]) + max_frame = max(len(stfi.frame) for stfi in resolved_stack_trace) + max_module = max(len(stfi.module_name) for stfi in resolved_stack_trace) + max_location = max(len(stfi.location) for stfi in resolved_stack_trace) + max_address = max(len(f"{stfi.address:#x}") for stfi in resolved_stack_trace) output = list() - for ssi in resolved_stack_trace: - output.append(f"{ssi.frame:<{max_frame}} {ssi.module_name:<{max_module}} {ssi.location:<{max_location}} " - f"0x{ssi.address:<{max_address}x} {ssi.path}") + for stfi in resolved_stack_trace: + output.append(f"{stfi.frame:<{max_frame}} {stfi.module_name:<{max_module}} {stfi.location:<{max_location}} " + f"0x{stfi.address:<{max_address}x} {stfi.module_path}") return '\n'.join(output) From b66af92cbee0e77afe13315c75705c67b082da1c Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 15 Feb 2023 11:56:54 +0100 Subject: [PATCH 10/26] Change check to retrieve system modules from System process. Add a few debug lines to help possible future problems resolution. --- .../symbol_resolver/symbol_resolver.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index b0a0b36..c118c49 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -134,7 +134,7 @@ class StackTraceInformation: """ @staticmethod def prettify(resolved_stack_trace: list[StackTraceFrameInformation]) -> str: - """Prettify a list of `StackTraceFrameInformation` so it's output is similar to the one given by ProcMon. + """Prettify a list of `StackTraceFrameInformation` so its output is similar to the one given by ProcMon. Args: resolved_stack_trace: A list of stack trace frame information. @@ -179,8 +179,11 @@ def __init__(self, If neither can be found, the function raises. Raises: - ValueError: The provided DLL path is not a valid directory, does not contain the required DLL(s) or the - automatic finder could not find the required DLL. + ValueError: + The provided DLL path is not a valid directory, does not contain the required DLL(s) or the automatic + finder could not find the required DLL. + RuntimeError: + The initialisation couldn't get the system modules. """ # Check if we can find the needed DLLs if not path has been provided. # Both DLLs are needed to resolve symbolic information. @@ -224,10 +227,20 @@ def __init__(self, for process in procmon_logs_reader.processes(): # Can't remember if System pid has always been 4. # Just check its name (doesn't end with .exe) and company is MS. That should be foolproof enough. - if process.process_name in ["System"] and process.company.lower().startswith("microsoft"): + if process.process_name in ["System"] and process.user.lower() == "nt authority\\system": self.system_modules = process.modules break + # Defensive check. + if not self.system_modules: + sys_pid = next((p for p in procmon_logs_reader.processes() if p.pid == 4), None) + sys_name = next((p for p in procmon_logs_reader.processes() if p.process_name.lower() == "system"), None) + if sys_pid is not None: + logger.debug(f"Process w/ PID = 4: {sys_pid!r}") + if sys_name is not None: + logger.debug(f"Process w/ Name = 'System': {sys_name!r}") + raise RuntimeError("Could not get system modules.") + def find_module(self, event, address: int) -> procmon_parser.Module | None: """Try to find the corresponding module given an address from an event stack trace. From efbd01c485f1fc4a370bcf64cf3d102fb2880bfb Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 15 Feb 2023 14:18:47 +0100 Subject: [PATCH 11/26] Don't call SymGetSourceFileW if we already have a fully qualified path with SymGetLineFromAddrW64. --- .../symbol_resolver/symbol_resolver.py | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index c118c49..0458077 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -450,28 +450,39 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame logger.debug(f"File Name: '{line.FileName}'; Line Number: {line.LineNumber}; " f"Line Displacement (col): {line_displacement.value}") - source_file_path_size = DWORD(260) - source_file_path = ctypes.create_unicode_buffer(source_file_path_size.value) - ret_val = self._dbghelp.SymGetSourceFileW( - pid, # hProcess - module.base_address, # Base - None, # Params (never used) - line.FileName, # FileSpec (name of source file) [PCWSTR] - source_file_path, # [out] FilePath: fully qualified path of source file - source_file_path_size # FilePath size (num chars) - ) - if ret_val == 0: - last_err = ctypes.get_last_error() - logger.debug(f"SymGetSourceFileW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") - logger.debug(f"--> FileName: {line.FileName}") - yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, - displacement.value, line, line_displacement.value) - continue - - logger.debug(f"source file path: {source_file_path.value}") + # It's possible that the returned line.Filename is already a fully qualified path, in which case there's no + # need to call SymGetSourceFileW, as the latter would be only used to retrieve the fully qualified path. + # We just check that we already have fully qualified path. If it is, then we bail out, otherwise we call + # SymGetSourceFileW. + fully_qualified_source_path = None + if pathlib.Path(line.FileName).is_absolute(): + # we have a fully qualified source file path. + logger.debug(f"source file path [from line.Filename]: {line.FileName}") + fully_qualified_source_path = line.FileName + else: + # we don't have a fully qualified source file path. + source_file_path_size = DWORD(260) + source_file_path = ctypes.create_unicode_buffer(source_file_path_size.value) + ret_val = self._dbghelp.SymGetSourceFileW( + pid, # hProcess + module.base_address, # Base + None, # Params (never used) + line.FileName, # FileSpec (name of source file) [PCWSTR] + source_file_path, # [out] FilePath: fully qualified path of source file + source_file_path_size # FilePath size (num chars) + ) + if ret_val == 0: + last_err = ctypes.get_last_error() + logger.debug(f"SymGetSourceFileW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + logger.debug(f"SymGetSourceFileW failed: using '{line.FileName}' as fallback.") + # use line.FileName as fallback + fully_qualified_source_path = line.FileName + else: + logger.debug(f"source file path [from SymGetSourceFileW]: {source_file_path.value}") + fully_qualified_source_path = source_file_path.value yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value, - line, line_displacement.value, source_file_path.value) + line, line_displacement.value, fully_qualified_source_path) # dbghelp symbol cleanup self._dbghelp.SymCleanup(pid) From a47ec215688816ed6c41ef58feeea7014a3021b9 Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 15 Feb 2023 15:31:29 +0100 Subject: [PATCH 12/26] Remove usage of typing and dataclasses. --- .../symbol_resolver/symbol_resolver.py | 133 ++++++++++-------- procmon_parser/symbol_resolver/win/dbghelp.py | 44 +++--- 2 files changed, 105 insertions(+), 72 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 0458077..1ad107a 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -3,7 +3,6 @@ """Module used to resolve symbolic information for a given a stack trace. """ import ctypes -import dataclasses import enum import logging import os @@ -17,22 +16,14 @@ if sys.platform != "win32": raise RuntimeError("Symbol Resolver can only be used on Windows Operating Systems.") -if sys.version_info < (3, 5, 0): - raise RuntimeError("Symbol Resolver can only be called from python 3.5 +.") - -import pathlib -import typing +if sys.version_info >= (3, 5, 0): + import typing + import pathlib # TODO: to be converted to py 2.7 equivalent. from procmon_parser.symbol_resolver.win.dbghelp import ( DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT) from procmon_parser.symbol_resolver.win.win_types import PVOID, HANDLE, DWORD64, DWORD - -if typing.TYPE_CHECKING: - from procmon_parser import ProcmonLogsReader - from procmon_parser.logs import Event - from procmon_parser.logs import Module - logger = logging.getLogger(__name__) @@ -45,7 +36,8 @@ class FrameType(enum.Enum): USER = enum.auto() @staticmethod - def from_address(address: int, max_user_address: int) -> "FrameType": + def from_address(address, max_user_address): + # type: (int, int) -> "FrameType" """Get the type of frame given an address and the maximum possible user address. Args: @@ -58,38 +50,51 @@ def from_address(address: int, max_user_address: int) -> "FrameType": return FrameType.KERNEL if address > max_user_address else FrameType.USER -@dataclasses.dataclass -class StackTraceFrameInformation: - """Contain various symbolic information about a frame in a stacktrace. - """ - #: Type of the frame, either Kernel or User. - frame_type: FrameType - #: The frame number (its position in the stack trace). - frame_number: int - #: Address at which the frame happens. - address: int - #: The module inside which the frame happens. - module: procmon_parser.Module | None = None - #: Symbolic information about the frame. - symbol_info: SYMBOL_INFOW | None = None - #: The displacement in regard to the symbol. - #: For example if the symbol name is 'foo' and the displacement is 0x10, then the frame happened at 'foo + 0x10'. - displacement: int | None = None - #: Line information in regard to the symbol (available only if symbolic source information is present). - line_info: IMAGEHLP_LINEW64 | None = None - #: Displacement from the source code line (that is, the column in the source code line). - line_displacement: int | None = None - #: The source code full file path at which the frame happened. - source_file_path: pathlib.Path | None = None +class StackTraceFrameInformation(object): + def __init__(self, + frame_type, # type: FrameType + frame_number, # type: int + address, # type: int + module=None, # type: procmon_parser.Module | None + symbol_info=None, # type: SYMBOL_INFOW | None + displacement=None, # type: int | None + line_info=None, # type: IMAGEHLP_LINEW64 | None + line_displacement=None, # type: int | None + source_file_path=None # type: str | None + ): + # type: (...) -> None + """Contain various symbolic information about a frame in a stacktrace. + """ + # Type of the frame, either Kernel or User. + self.frame_type = frame_type + # The frame number (its position in the stack trace). + self.frame_number = frame_number + # Address of the symbol, at which the frame happens. + self.address = address + # The module inside which the frame happens. + self.module = module + # Symbolic information about the frame. + self.symbol_info = symbol_info + # The displacement in regard to the symbol. + # For example if the symbol name is 'foo' and the displacement is 0x10, then the frame happened at 'foo + 0x10'. + self.displacement = displacement + # Line information in regard to the symbol (available only if symbolic source information is present). + self.line_info = line_info + # Displacement from the source code line (that is, the column in the source code line). + self.line_displacement = line_displacement + # The source code full file path at which the frame happened. + self.source_file_path = source_file_path @property - def frame(self) -> str: + def frame(self): + # type: () -> str """Return a string representation of a frame (its `FrameType` and frame number). """ return f"{self.frame_type.name[0]} {self.frame_number}" @property - def location(self) -> str: + def location(self): + # type: () -> str """Return a string representation of the symbolic location at which the frame happens. """ if self.symbol_info is None: @@ -107,7 +112,8 @@ def location(self) -> str: return f"{sym_str}, {line_str}" @property - def module_name(self) -> str: + def module_name(self): + # type: () -> str """Return a string representation of the frame main module name. """ if self.module is None or not self.module.path: @@ -116,7 +122,8 @@ def module_name(self) -> str: return pathlib.Path(self.module.path).name @property - def module_path(self) -> str: + def module_path(self): + # type: () -> str """Return a string representation of the frame main module fully qualified path. """ if self.module is None or not self.module.path: @@ -124,16 +131,19 @@ def module_path(self) -> str: return self.module.path - def __repr__(self) -> str: + def __repr__(self): + # type: () -> str return f"{self.frame} {self.module_name} {self.location} {self.address:#x} {self.module_path}" -class StackTraceInformation: +class StackTraceInformation(object): """Class used to prettify a whole stack trace so its output if similar to ProcMon's stack trace window tab for an event. """ + @staticmethod - def prettify(resolved_stack_trace: list[StackTraceFrameInformation]) -> str: + def prettify(resolved_stack_trace): + # type: (list[StackTraceFrameInformation]) -> str """Prettify a list of `StackTraceFrameInformation` so its output is similar to the one given by ProcMon. Args: @@ -158,13 +168,15 @@ def prettify(resolved_stack_trace: list[StackTraceFrameInformation]) -> str: return '\n'.join(output) -class SymbolResolver: +class SymbolResolver(object): """Main workhorse class for resolving symbolic information from a stack trace. """ + def __init__(self, - procmon_logs_reader: "ProcmonLogsReader", - dll_dir_path: str | pathlib.Path | None = None, - skip_symsrv: bool = False) -> None: + procmon_logs_reader, + dll_dir_path=None, + skip_symsrv=False): + # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool) -> None """Class Initialisation. Args: @@ -241,7 +253,8 @@ def __init__(self, logger.debug(f"Process w/ Name = 'System': {sys_name!r}") raise RuntimeError("Could not get system modules.") - def find_module(self, event, address: int) -> procmon_parser.Module | None: + def find_module(self, event, address): + # type: (procmon_parser.Event, int) -> procmon_parser.Module | None """Try to find the corresponding module given an address from an event stack trace. Args: @@ -251,11 +264,14 @@ def find_module(self, event, address: int) -> procmon_parser.Module | None: Returns: If the address lies inside a known module, the module is returned, otherwise the function returns None. """ - def is_kernel(addr: int) -> bool: + + def is_kernel(addr): + # type: (int) -> bool """[Internal] Return whether an address is kernel (True) or not (user mode address: False).""" return addr > self._max_user_address - def find_module_from_list(addr: int, modules: list['Module']) -> typing.Optional['Module']: + def find_module_from_list(addr, modules): + # type: (int, list[procmon_parser.Module]) -> procmon_parser.Module | None """[Internal] Return an instance of a Module given an address (if the address lies inside the module). """ for m in modules: @@ -272,7 +288,8 @@ def find_module_from_list(addr: int, modules: list['Module']) -> typing.Optional module = find_module_from_list(address, module_source) return module # may be None. - def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrameInformation]: + def resolve_stack_trace(self, event): + # type: (procmon_parser.Event) -> typing.Iterator[StackTraceFrameInformation] """Resolve the stack trace of an event to include symbolic information. Args: @@ -455,7 +472,7 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame # We just check that we already have fully qualified path. If it is, then we bail out, otherwise we call # SymGetSourceFileW. fully_qualified_source_path = None - if pathlib.Path(line.FileName).is_absolute(): + if pathlib.Path(line.FileName).is_absolute(): # noqa # we have a fully qualified source file path. logger.debug(f"source file path [from line.Filename]: {line.FileName}") fully_qualified_source_path = line.FileName @@ -488,12 +505,14 @@ def resolve_stack_trace(self, event: "Event") -> typing.Iterator[StackTraceFrame self._dbghelp.SymCleanup(pid) -class DbgHelpUtils: +class DbgHelpUtils(object): """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg preview are installed on the current system. """ + @staticmethod - def find_debugging_tools() -> pathlib.Path | None: + def find_debugging_tools(): + # type: () -> pathlib.Path | None """Find the path of the directory containing DbgHelp.dll and Symsrv.dll from the Debugging Tools For Windows (installed from the Windows SDK). @@ -559,7 +578,8 @@ def find_debugging_tools() -> pathlib.Path | None: return DbgHelpUtils._arch_dir(debugger_path, lookup) @staticmethod - def find_windbg_preview() -> pathlib.Path | None: + def find_windbg_preview(): + # type: () -> pathlib.Path | None """Find the directory path of the DbgHelp.dll and Symsrv.dll from the Windbg preview installation (installed from the Windows Store). @@ -601,7 +621,8 @@ def find_windbg_preview() -> pathlib.Path | None: return DbgHelpUtils._arch_dir(windbg_location, lookup) @staticmethod - def _arch_dir(debugger_dir: pathlib.Path, arch_lookup: dict[str, str]) -> pathlib.Path | None: + def _arch_dir(debugger_dir, arch_lookup): + # type: (pathlib.Path, dict[str, str]) -> pathlib.Path | None """[internal] Get the path to the right DLLs (depending on the architecture used by the python interpreter). Args: diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 2bb850b..69edf66 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -5,16 +5,19 @@ DbgHelp.dll is the main library for resolving symbols. """ import ctypes -import dataclasses import logging import pathlib +import sys import enum -import _ctypes # only used for typing as ctypes doesn't export inner types. - from procmon_parser.symbol_resolver.win.win_types import ( HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD) +if sys.version_info >= (3, 5, 0): + import typing + if typing.TYPE_CHECKING: + import _ctypes # only used for typing as ctypes doesn't export inner types. + logger = logging.getLogger(__name__) # @@ -98,18 +101,21 @@ class IMAGEHLP_LINEW64(ctypes.Structure): # noqa # Functions descriptors # +class _FunctionDescriptor(object): + __slots__ = ["name", "parameter_types", "return_type", "aliases"] -@dataclasses.dataclass -class _FunctionDescriptor: - """Class used to describe a Windows API function wrt its ctypes bindings.""" - name: str - parameter_types: tuple[_ctypes._SimpleCData] | None = None - return_type: _ctypes._SimpleCData | None = None - aliases: list[str] | None = None + def __init__(self, name, parameter_types=None, return_type=None, aliases=None): + # type: (str, tuple[_ctypes._SimpleCData] | None, _ctypes._SimpleCData | None, list[str] | None) -> None + """Class used to describe a Windows API function wrt its ctypes bindings.""" + self.name = name, + self.parameter_types = parameter_types + self.return_type = return_type + self.aliases = aliases # list of function (descriptors) from DbgHelp.dll -_functions_descriptors: list[_FunctionDescriptor] = [ +# type: list[_FunctionDescriptor] +_functions_descriptors = [ # SymInitializeW # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-syminitializew _FunctionDescriptor("SymInitializeW", @@ -271,7 +277,8 @@ class DbgHelp: ``` """ - def __init__(self, dbghelp_path: pathlib.Path) -> None: + def __init__(self, dbghelp_path): + # type: (pathlib.Path) -> None """Class init. Args: @@ -297,7 +304,8 @@ def __getitem__(self, item: str): def __getattr__(self, item: str): return self[item] - def _resolve_functions(self, function_descriptors: list[_FunctionDescriptor]) -> None: + def _resolve_functions(self, function_descriptors): + # type: (list[_FunctionDescriptor]) -> None """[internal] Resolve functions, for the given DLL, from the list of `_FunctionsDescriptor`. Raises: @@ -306,7 +314,8 @@ def _resolve_functions(self, function_descriptors: list[_FunctionDescriptor]) -> for function_descriptor in function_descriptors: self._register_function(function_descriptor) - def _register_function(self, function_descriptor: _FunctionDescriptor) -> None: + def _register_function(self, function_descriptor) -> None: + # type: (_FunctionDescriptor) -> None """[internal] Build a function ctypes wrapping from its function descriptor. Args: @@ -316,7 +325,10 @@ def _register_function(self, function_descriptor: _FunctionDescriptor) -> None: AttributeError: A given function was not found. """ try: - function_pointer = getattr(self._dbghelp, function_descriptor.name) + # HACK (python 3.10 thinks it's a tuple ???) + func_name = (function_descriptor.name[0] if isinstance(function_descriptor.name, tuple) else + function_descriptor.name) + function_pointer = getattr(self._dbghelp, func_name) except AttributeError: # We land here if the function can't be found in the given DLL. # note: it raises from quite deep inside ctypes if the function can't be resolved, which might be confusing. @@ -327,7 +339,7 @@ def _register_function(self, function_descriptor: _FunctionDescriptor) -> None: function_pointer.argtypes = function_descriptor.parameter_types if function_descriptor.return_type: function_pointer.restype = function_descriptor.return_type - self._functions.update({function_descriptor.name: function_pointer}) + self._functions.update({func_name: function_pointer}) if function_descriptor.aliases: for alias in function_descriptor.aliases: self._functions.update({alias: function_pointer}) From 250dcd75bd547044eeae7d04d16434f9ce90f606 Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 15 Feb 2023 19:15:27 +0100 Subject: [PATCH 13/26] Fix rookie mistake of a comma in ctor... --- procmon_parser/symbol_resolver/win/dbghelp.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 69edf66..6c9ee04 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -107,7 +107,7 @@ class _FunctionDescriptor(object): def __init__(self, name, parameter_types=None, return_type=None, aliases=None): # type: (str, tuple[_ctypes._SimpleCData] | None, _ctypes._SimpleCData | None, list[str] | None) -> None """Class used to describe a Windows API function wrt its ctypes bindings.""" - self.name = name, + self.name = name self.parameter_types = parameter_types self.return_type = return_type self.aliases = aliases @@ -325,10 +325,7 @@ def _register_function(self, function_descriptor) -> None: AttributeError: A given function was not found. """ try: - # HACK (python 3.10 thinks it's a tuple ???) - func_name = (function_descriptor.name[0] if isinstance(function_descriptor.name, tuple) else - function_descriptor.name) - function_pointer = getattr(self._dbghelp, func_name) + function_pointer = getattr(self._dbghelp, function_descriptor.name) except AttributeError: # We land here if the function can't be found in the given DLL. # note: it raises from quite deep inside ctypes if the function can't be resolved, which might be confusing. @@ -339,7 +336,7 @@ def _register_function(self, function_descriptor) -> None: function_pointer.argtypes = function_descriptor.parameter_types if function_descriptor.return_type: function_pointer.restype = function_descriptor.return_type - self._functions.update({func_name: function_pointer}) + self._functions.update({function_descriptor.name: function_pointer}) if function_descriptor.aliases: for alias in function_descriptor.aliases: self._functions.update({alias: function_pointer}) From 28357e1578a7a739e08514537290f7319056ee9d Mon Sep 17 00:00:00 2001 From: neitsa Date: Thu, 16 Feb 2023 14:53:24 +0100 Subject: [PATCH 14/26] Add a way to override _NT_SYMBOL_PATH. --- .../symbol_resolver/symbol_resolver.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 1ad107a..7596a1b 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -175,8 +175,9 @@ class SymbolResolver(object): def __init__(self, procmon_logs_reader, dll_dir_path=None, - skip_symsrv=False): - # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool) -> None + skip_symsrv=False, + symbol_path=None): + # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool, str) -> None """Class Initialisation. Args: @@ -184,6 +185,9 @@ def __init__(self, dll_dir_path: Path to a directory containing at least `dbghelp.dll`, and optionally `symsrv.dll`. skip_symsrv: Set to True if symbols are available locally on the machine and `_NT_SYMBOL_PATH` environment variable is correctly set. This skips the check for `symsrv.dll` presence altogether. + symbol_path: Replace the `_NT_SYMBOL_PATH` environment variable if it exists, or prevent using %TEMP% as + the download location of the symbol files. This must be a string compatible with the `_NT_SYMBOL_PATH` + syntax. Notes: If `dll_dir_path` is None, then the code does its best to find matching installations of the Debugging Tools @@ -219,10 +223,13 @@ def __init__(self, self.dll_dir_path = dll_dir_path # _NT_SYMBOL_PATH is needed to store symbols locally. If it's not set, we need to set it. - symbol_path = os.environ.get("_NT_SYMBOL_PATH", None) - if symbol_path is None: - # resolve TEMP folder and set it at the symbol path. - os.environ["_NT_SYMBOL_PATH"] = f"srv*{os.environ['TEMP']}*https://msdl.microsoft.com/download/symbols" + nt_symbol_path = os.environ.get("_NT_SYMBOL_PATH", None) + if nt_symbol_path is None: + if symbol_path is None: + # resolve TEMP folder and set it at the symbol path. + symbol_path = f"srv*{os.environ['TEMP']}*https://msdl.microsoft.com/download/symbols" + # set symbol path + os.environ["_NT_SYMBOL_PATH"] = symbol_path logger.debug(f"NT_SYMBOL_PATH: {os.environ['_NT_SYMBOL_PATH']}") # DbgHelp wrapper instance initialisation and symbolic option setting. From f52fecc6f6516f6294eab5397fa6c283805c1381 Mon Sep 17 00:00:00 2001 From: neitsa Date: Thu, 16 Feb 2023 15:52:45 +0100 Subject: [PATCH 15/26] Add documentation on symbol resolving. --- docs/StackTraceSymbolicResolution.md | 243 +++++++++++++++++++++ docs/pictures/event.png | Bin 0 -> 13488 bytes docs/pictures/stack_trace_no_symbols.png | Bin 0 -> 72711 bytes docs/pictures/stack_trace_with_symbols.png | Bin 0 -> 94205 bytes 4 files changed, 243 insertions(+) create mode 100644 docs/StackTraceSymbolicResolution.md create mode 100644 docs/pictures/event.png create mode 100644 docs/pictures/stack_trace_no_symbols.png create mode 100644 docs/pictures/stack_trace_with_symbols.png diff --git a/docs/StackTraceSymbolicResolution.md b/docs/StackTraceSymbolicResolution.md new file mode 100644 index 0000000..bdaab9a --- /dev/null +++ b/docs/StackTraceSymbolicResolution.md @@ -0,0 +1,243 @@ +# Resolving Stack Traces With `procmon-parser` + +## Limitations and Constraints + +Symbolic stack trace resolution has the following limitations: + +* It is based on Windows libraries, thus it is only available on **Windows** systems. +* It requires an Internet connection. +* It connects and download files from a Microsoft Server. + - By doing so, it requires you to accept Microsoft License terms. + +## Basics + +All events in a ProcMon trace have a [stack trace](https://en.wikipedia.org/wiki/Stack_trace). + +Below is an example of an event in a ProcMon capture: + +![Event](./pictures/event.png) + +The event captured is a thread creation in `explorer.exe`. + +If you double-click the event, you are brought to a new window with 3 tabs, the interesting one in our case being the +stack trace tab. Below is an example of the aforementioned event with an **unresolved** stack trace: + +![Stack Trace No Symbols](./pictures/stack_trace_no_symbols.png) + +And the same event with a **resolved** stack trace: + +![Stack Trace No Symbols](./pictures/stack_trace_with_symbols.png) + +In the above pictures, after symbolic resolution of the addresses, the latter were resolved to their function names and +offsets and sometimes the source code position where the call happens (frames 12 and 13). + +Stack Traces are composed of frames (there are 25 frames in the above example), and is read from bottom to top: the +oldest call happens at the bottom and goes to the top, traversing all the frames in-between. Once the top function +returns, all the frames are unstacked and eventually the code flow goes back to the first frame (at the bottom). + +## Resolving a Stack Trace with `procmon-parser` + +Resolving a stack trace in `procmon-parser` can be as simple as follows: + +```python +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import pathlib +import sys + +from procmon_parser import ProcmonLogsReader, SymbolResolver, StackTraceInformation + +def main(): + log_file = pathlib.Path(r"c:\temp\Logfile.PML") + + with log_file.open("rb") as f: + procmon_reader = ProcmonLogsReader(f) + symbol_resolver = SymbolResolver(procmon_reader) + for idx, event in enumerate(procmon_reader): + if idx == 213: + frames = list(symbol_resolver.resolve_stack_trace(event)) + print(StackTraceInformation.prettify(frames)) + +if __name__ == "__main__": + sys.exit(main()) +``` + +## Setting Up Stack Trace Resolution In `procmon-parser` + +### Obtaining Required Windows Libraries + +Stack trace resolution uses 2 Windows DLLs: + +* `dbghelp.dll` ([official documentation](https://learn.microsoft.com/en-us/windows/win32/debug/debug-help-library)) + - Provide symbol resolution functionalities. +* `symsrv.dll` ([official documentation](https://learn.microsoft.com/en-us/windows/win32/debug/using-symsrv)) + - Symbol file management (mostly downloading symbolic information from a symbol store). + +While `dbghelp.dll` is provided with Windows systems (it's located in `%WINDIR%\system32`) this DLL might be out of date +on some systems and thus missing various functionalities (as [explained here](https://learn.microsoft.com/en-us/windows/win32/debug/dbghelp-versions)). +`symsrv.dll`, on the other hand, does not ship with Windows systems. + +Both DLLs can be acquired from various Microsoft products, notably: + +* [Debugging Tools for Windows](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/) +* [Windbg Preview](https://apps.microsoft.com/store/detail/windbg-preview/9PGJGD53TN86) +* [Visual Studio](https://visualstudio.microsoft.com/downloads/) + +The official and [**recommended way**](https://learn.microsoft.com/en-us/windows/win32/debug/dbghelp-versions) is to +install the `Debugging Tools For Windows` from the Windows SDK (please note that the SDK installer allows to only +install the *Debugging Tools for Windows* and not the whole SDK). + +**Important**: `procmon-parser` will try to find the correct path to the Debugging Tools For Windows and Windbg Preview +and then automatically provide the path to the DLLs matching the Python interpreter architecture. It does not, however, +try to find the DLLs from a Visual Studio installation. + +Be sure to use the DLLs that matches your interpreter architecture. For example, the *Debugging Tools For Windows* comes +with 4 different architectures: x86, x64, arm(32) and arm64: + +``` +neitsa@lab:c/Program Files (x86)/Windows Kits/10/Debuggers$ tree -L 1 +. +├── Redist +├── arm +├── arm64 +├── ddk +├── inc +├── lib +├── x64 +└── x86 +``` + +You can get your Python interpreter architecture by using the `platform` module for example: + +``` +>>> import platform +>>> platform.architecture() +('64bit', 'WindowsPE') +``` + +Thus, the directory in the *Debugging Tools For Windows* would be the `x64` one since the Python interpreter is a 64-bit +one. + +### Symsrv and Microsoft License Terms + +Microsoft's symbol servers (located at https://msdl.microsoft.com/download/symbols/), provides access to +symbols for the operating system itself. The `symsrv.dll` library requires agreement to Microsoft's +*"Terms of Use for Microsoft Symbols and Binaries."* ([visible here](https://learn.microsoft.com/en-us/legal/windows-sdk/microsoft-symbol-server-license-terms)). + +On your first usage of the symbolic resolution, the `symsrv.dll` may display a prompt requiring you to accept the +aforementioned *Terms of Use* if you wish to continue further. + +To automatically indicate agreement to the terms, you may create a file called `symsrv.yes` (there's no need to put +something in the file) in the same directory as the `symsrv.dll` library (Note that `symsrv.dll` will also recognize a +`symsrv.no` file as indicating that you do not accept the terms; the `.yes` file takes priority over the `.no` file.). + +It is also possible to view the terms from within the WinDbg debugger (included in the *Debugging Tools for Windows*) +by removing any `symsrv.yes` and `symsrv.no` files from WinDbg's directory, setting the symbol path to include +Microsoft's symbol server (using the `.sympath` command), and attempting to load symbols from their server (`.reload` +command). + +## Advanced Usage + +### Symbol Download Location + +The [_NT_SYMBOL_PATH](https://learn.microsoft.com/en-us/windows/win32/debug/using-symsrv#setting-the-symbol-path) +environment variable is the official way to set the location where the symbols are going to be stored. + +Symbols files may need to be downloaded from a symbol store, in which case the following algorithm takes place in the +`SymbolResolver` class: + +* if `_NT_SYMBOL_PATH` environment variable is set: + - Use `_NT_SYMBOL_PATH` location to put symbol files. + - if `symbol_path` constructor argument is set: + - Do not use `_NT_SYMBOL_PATH` but use the provided symbol path instead. +* else + - Use `%TEMP%` directory. + +Note that using the `%TEMP%` directory may require to download the symbol between each computer reboot. The is most of +the time a lengthy operation, even with a fast internet connection. + +The basic syntax of the `_NT_SYMBOL_PATH` environment variable (and therefore the `symbol_path` constructor argument) is +as follows: + +``` +srv**https://msdl.microsoft.com/download/symbols/ +``` + +Where `` must be an **existing directory** which is **writable** by any user. For example: + +``` +srv*c:\symbols*https://msdl.microsoft.com/download/symbols/ +``` + +For more information on the various possibilities for setting up the environment variable, please refer to the +[official documentation](https://learn.microsoft.com/en-us/windows/win32/debug/using-symsrv). + +### Copying DLLs + +If, for any reason, you do not wish to install the *Debugging Tools For Windows* on a particular machine (e.g. a virtual +machine) but already have it installed on another machine you can copy and paste both DLLs (`dbghelp.dll` and +`symsrv.dll`) from the *Debugging Tools For Windows* onto the target machine, preferably in their own (writable) +directory but **not** a system one (never erase the default one in `%WINDIR%\System32`). Both DLLs must reside alongside each other. + +In case you would want to provide a different path for the DLLs, you can use the `dll_dir_path` parameter of the +`SymbolResolver` class: + +```python +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import pathlib +import sys + +from procmon_parser import ProcmonLogsReader, SymbolResolver + +def main(): + log_file = pathlib.Path(r"c:\temp\Logfile.PML") + dll_dir_path = r"c:\tmp\my_debug_dll_dir" + + with log_file.open("rb") as f: + procmon_reader = ProcmonLogsReader(f) + # disable automatic retrieval of dbghelp.dll and symsrv.dll, and use the provided path instead. + # It must contain at least both DLLs: + # - from the same provider (e.g. Debugging tools for Windows) + # - and the same architecture (e.g. Debugging tools for Windows '\x64' directory). + symbol_resolver = SymbolResolver(procmon_reader, dll_dir_path=dll_dir_path) + # ... + +if __name__ == "__main__": + sys.exit(main()) +``` + +### Skipping `symsrv.dll` Check + +The `SymbolResolver` class in `procmon-parser` checks if both `dbghelp` and `symsrv` DLLs are present in the provided +directory (if you pass it through the `dll_dir_path` parameter as explained above). + +If you have offline symbols already available (for example, by having previously used the `symchk` +([documentation](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/using-symchk)) tool from the +*Debugging Tools For Windows*), and do not want to connect your machine to the Internet, you can skip the `symsrv.dll` +automatic check by using the `skip_symsrv` parameter of the `SymbolResolver` class: + +```python +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import pathlib +import sys + +from procmon_parser import ProcmonLogsReader, SymbolResolver + +def main(): + log_file = pathlib.Path(r"c:\temp\Logfile.PML") + # does not contain symsrv.dll + dll_dir_path = r"c:\tmp\my_debug_dll_dir" + + with log_file.open("rb") as f: + procmon_reader = ProcmonLogsReader(f) + # disable automatic retrieval of dbghelp.dll and symsrv.dll, and use the provided path instead. + # skip entirely the check for symsrv.dll. + # Use **only** if you know that you already have the necessary symbols! + symbol_resolver = SymbolResolver(procmon_reader, dll_dir_path=dll_dir_path, skip_symsrv=True) + # ... + +if __name__ == "__main__": + sys.exit(main()) +``` diff --git a/docs/pictures/event.png b/docs/pictures/event.png new file mode 100644 index 0000000000000000000000000000000000000000..55e9f5c8285f579eecfd02d1d775bc2d3b7001ec GIT binary patch literal 13488 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 literal 0 HcmV?d00001 diff --git a/docs/pictures/stack_trace_no_symbols.png b/docs/pictures/stack_trace_no_symbols.png new file mode 100644 index 0000000000000000000000000000000000000000..b6232cf83a1f66c421d170657638683281d6cf7f GIT binary patch literal 72711 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 Tqi=5$FfhIYwEg@+$#e;`1e8+|M-~s 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 SuC_c)E*z?oDQ;EJoCGIZ+hd{TZga9vNeQH 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%0AZ|@z~WY(<>+tERV84IHb(NRG~Kt(`W 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 Ba5?}0 literal 0 HcmV?d00001 From 849dbb1febd688df6c555ebe3f62c521a3a71e43 Mon Sep 17 00:00:00 2001 From: neitsa Date: Thu, 16 Feb 2023 19:08:21 +0100 Subject: [PATCH 16/26] Add debug callback to debug symbol resolution problems. --- docs/StackTraceSymbolicResolution.md | 40 +++++++ procmon_parser/__init__.py | 4 +- .../symbol_resolver/symbol_resolver.py | 44 +++++++- .../symbol_resolver/win/__init__.py | 1 + procmon_parser/symbol_resolver/win/dbghelp.py | 105 +++++++++++++++++- .../symbol_resolver/win/win_types.py | 1 + 6 files changed, 181 insertions(+), 14 deletions(-) diff --git a/docs/StackTraceSymbolicResolution.md b/docs/StackTraceSymbolicResolution.md index bdaab9a..1583ad6 100644 --- a/docs/StackTraceSymbolicResolution.md +++ b/docs/StackTraceSymbolicResolution.md @@ -241,3 +241,43 @@ def main(): if __name__ == "__main__": sys.exit(main()) ``` + +## Debugging Symbol Resolution Problem + +If symbol resolution is not working as expected, you can pass a callback function - using the `debug_callback` named +parameter - to the `SymbolResolver` constructor, as follows: + +```python +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +import pathlib +import sys + +from procmon_parser import ProcmonLogsReader, SymbolResolver, CBA + +def symbol_debug_callback(handle: int, action_code: CBA | int, callback_data: str, user_context: int): + if action_code == CBA.CBA_DEBUG_INFO: + print(f"[DEBUG MESSAGE DBGHELP: CBA_DEBUG_INFO] {callback_data}") + return 1 + return 0 + +def main(): + log_file = pathlib.Path(r"c:\temp\Logfile.PML") + + with log_file.open("rb") as f: + procmon_reader = ProcmonLogsReader(f) + symbol_resolver = SymbolResolver(procmon_reader, debug_callback=symbol_debug_callback) + # ... + +if __name__ == "__main__": + sys.exit(main()) +``` + +The callback function mimics the [PSYMBOL_REGISTERED_CALLBACK64 ](https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nc-dbghelp-psymbol_registered_callback64) +Windows' API callback function. As of now only the `CBA.CBA_DEBUG_INFO` action code is handled internally. + +To indicate success handling the `CBA` code, the function **must** return 1. To indicate failure handling the code, +return 0. If your code does not handle a particular code, you should also return 0. (Returning 1 in this case may have +unintended consequences.) + +This will print a lot of information that is helpful debugging symbol retrieval problems. diff --git a/procmon_parser/__init__.py b/procmon_parser/__init__.py index 3322365..a27f854 100644 --- a/procmon_parser/__init__.py +++ b/procmon_parser/__init__.py @@ -15,9 +15,9 @@ if sys.platform == "win32" and sys.version_info >= (3, 5, 0): from procmon_parser.symbol_resolver.symbol_resolver import ( - SymbolResolver, StackTraceFrameInformation, StackTraceInformation) + SymbolResolver, StackTraceFrameInformation, StackTraceInformation, CBA) - __all__.extend(['SymbolResolver', 'StackTraceFrameInformation', 'StackTraceInformation']) + __all__.extend(['SymbolResolver', 'StackTraceFrameInformation', 'StackTraceInformation', 'CBA']) class ProcmonLogsReader(object): diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 7596a1b..ae69f1b 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -21,8 +21,9 @@ import pathlib # TODO: to be converted to py 2.7 equivalent. from procmon_parser.symbol_resolver.win.dbghelp import ( - DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT) -from procmon_parser.symbol_resolver.win.win_types import PVOID, HANDLE, DWORD64, DWORD + DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT, PSYMBOL_REGISTERED_CALLBACK64, + CBA, PIMAGEHLP_CBA_EVENTW) +from procmon_parser.symbol_resolver.win.win_types import PVOID, HANDLE, DWORD64, DWORD, ULONG, ULONG64, BOOL logger = logging.getLogger(__name__) @@ -176,8 +177,9 @@ def __init__(self, procmon_logs_reader, dll_dir_path=None, skip_symsrv=False, - symbol_path=None): - # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool, str) -> None + symbol_path=None, + debug_callback=None): + # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool, str, typing.Callable) -> None """Class Initialisation. Args: @@ -188,6 +190,8 @@ def __init__(self, symbol_path: Replace the `_NT_SYMBOL_PATH` environment variable if it exists, or prevent using %TEMP% as the download location of the symbol files. This must be a string compatible with the `_NT_SYMBOL_PATH` syntax. + debug_mode: If set to True, set the dbghelp functionalities in debug mode (can be used to understand and + debug problems with symbol downloading and resolution). Notes: If `dll_dir_path` is None, then the code does its best to find matching installations of the Debugging Tools @@ -234,10 +238,17 @@ def __init__(self, # DbgHelp wrapper instance initialisation and symbolic option setting. self._dbghelp = DbgHelp(self.dll_dir_path / "dbghelp.dll") - self._dbghelp.SymSetOptions( + + self._debug_callback = debug_callback + dbghelp_options = [ SYMOPT.SYMOPT_CASE_INSENSITIVE | SYMOPT.SYMOPT_UNDNAME | SYMOPT.SYMOPT_DEFERRED_LOADS | SYMOPT.SYMOPT_LOAD_LINES | SYMOPT.SYMOPT_OMAP_FIND_NEAREST | SYMOPT.SYMOPT_FAIL_CRITICAL_ERRORS | - SYMOPT.SYMOPT_INCLUDE_32BIT_MODULES | SYMOPT.SYMOPT_AUTO_PUBLICS) # 0x12237. + SYMOPT.SYMOPT_INCLUDE_32BIT_MODULES | SYMOPT.SYMOPT_AUTO_PUBLICS] + + if self._debug_callback is not None: + dbghelp_options.append(SYMOPT.SYMOPT_DEBUG) + + self._dbghelp.SymSetOptions(sum(dbghelp_options)) # 0x12237 (if not SYMOPT_DEBUG). # maximum user-address, used to discern between user and kernel modules (which don't change between processes). self._max_user_address: int = procmon_logs_reader.maximum_application_address @@ -332,6 +343,11 @@ def resolve_stack_trace(self, event): pid = event.process.pid self._dbghelp.SymInitialize(pid, None, False) + # set up callback if we are in debug mode + if self._debug_callback: + callback = PSYMBOL_REGISTERED_CALLBACK64(self._symbol_registered_callback) + self._dbghelp.SymRegisterCallbackW64(pid, callback, PVOID(pid)) + logger.debug(f"# Stack Trace frames: {len(event.stacktrace)}") logger.debug(f"PID: {pid:#08x}") @@ -511,6 +527,22 @@ def resolve_stack_trace(self, event): # dbghelp symbol cleanup self._dbghelp.SymCleanup(pid) + def _symbol_registered_callback(self, handle, action_code, callback_data, user_context): + # type: (HANDLE, ULONG, ULONG64, ULONG64) -> BOOL + param_callback_data = callback_data + try: + param_action_code = CBA(action_code) + if param_action_code == CBA.CBA_DEBUG_INFO: + param_callback_data = ctypes.cast(callback_data, ctypes.c_wchar_p).value + elif param_action_code == CBA.CBA_EVENT: + param_callback_data = ctypes.cast(callback_data, PIMAGEHLP_CBA_EVENTW).contents + except ValueError: + # can't convert from int to CBA. this happens for internal messages that surfaces. + param_action_code = action_code + + ret = self._debug_callback(handle, param_action_code, param_callback_data, user_context) + return ret + class DbgHelpUtils(object): """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg diff --git a/procmon_parser/symbol_resolver/win/__init__.py b/procmon_parser/symbol_resolver/win/__init__.py index e69de29..8781c8a 100644 --- a/procmon_parser/symbol_resolver/win/__init__.py +++ b/procmon_parser/symbol_resolver/win/__init__.py @@ -0,0 +1 @@ +from .dbghelp import CBA diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 6c9ee04..3d8b31c 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -11,22 +11,31 @@ import enum from procmon_parser.symbol_resolver.win.win_types import ( - HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD) + HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD, BOOLEAN) if sys.version_info >= (3, 5, 0): import typing + if typing.TYPE_CHECKING: import _ctypes # only used for typing as ctypes doesn't export inner types. logger = logging.getLogger(__name__) +# +MAX_PATH = 260 + # # Callback Functions needed by some DbgHelp APIs. # # PFINDFILEINPATHCALLBACK; used with the SymFindFileInPath function. # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nc-dbghelp-pfindfileinpathcallback -PFINDFILEINPATHCALLBACK = ctypes.WINFUNCTYPE(BOOL, PCSTR, PVOID, use_last_error=False) +PFINDFILEINPATHCALLBACK = ctypes.WINFUNCTYPE(BOOL, PCSTR, PVOID) + +# PSYMBOL_REGISTERED_CALLBACK64 ; passed to SymRegisterCallback64 +# Used if SymSetOptions is passed the 'SYMOPT_DEBUG' flag. +# https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nc-dbghelp-psymbol_registered_callback64 +PSYMBOL_REGISTERED_CALLBACK64 = ctypes.WINFUNCTYPE(BOOL, HANDLE, ULONG, ULONG64, ULONG64) # @@ -37,7 +46,8 @@ class MODLOAD_DATA(ctypes.Structure): # noqa """Contains module data. Used by SymLoadModuleExW. - See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-modload_data + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-modload_data """ _fields_ = ( ("ssize", DWORD), @@ -54,7 +64,8 @@ class MODLOAD_DATA(ctypes.Structure): # noqa class SYMBOL_INFOW(ctypes.Structure): # noqa """Contains symbol information. Used by SymFromAddrW. - See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_infow + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_infow """ BUFFER_NUM_ELEMENTS = 468 @@ -83,7 +94,8 @@ class SYMBOL_INFOW(ctypes.Structure): # noqa class IMAGEHLP_LINEW64(ctypes.Structure): # noqa """Represents a source file line. Used by SymGetLineFromAddrW64. - See: https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_linew64 + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_linew64 """ _fields_ = ( ("SizeOfStruct", DWORD), @@ -97,10 +109,61 @@ class IMAGEHLP_LINEW64(ctypes.Structure): # noqa PIMAGEHLP_LINEW64 = ctypes.POINTER(IMAGEHLP_LINEW64) +class IMAGEHLP_DEFERRED_SYMBOL_LOADW64(ctypes.Structure): # noqa + """Contains information about a deferred symbol load. + + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_deferred_symbol_loadw64 + """ + _fields_ = ( + ("SizeOfStruct", DWORD), + ("BaseOfImage", DWORD64), + ("Checksum", DWORD), + ("TimeDateStamp", DWORD), + ("FileName", WCHAR * (MAX_PATH + 1)), + ("Reparse", BOOLEAN), + ("hFile", HANDLE), + ("Flags", DWORD) + ) + + +PIMAGEHLP_DEFERRED_SYMBOL_LOADW64 = ctypes.POINTER(IMAGEHLP_DEFERRED_SYMBOL_LOADW64) + + +class IMAGEHLP_DUPLICATE_SYMBOL64(ctypes.Structure): # noqa + """Contains duplicate symbol information. + + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_duplicate_symbol64 + + """ + _fields_ = ( + ("SizeOfStruct", DWORD), + ("NumberOfDups", DWORD), + ("Symbol", PVOID), # should be POINTER(IMAGEHLP_SYMBOLW64) + ("SelectedSymbol", DWORD) + ) + + +PIMAGEHLP_DUPLICATE_SYMBOL64 = ctypes.POINTER(IMAGEHLP_DUPLICATE_SYMBOL64) + + +class IMAGEHLP_CBA_EVENTW(ctypes.Structure): # noqa + _fields_ = ( + ("severity", DWORD), + ("code", DWORD), + ("desc", PCWSTR), + ("object", PVOID) + ) + + +PIMAGEHLP_CBA_EVENTW = ctypes.POINTER(IMAGEHLP_CBA_EVENTW) + # # Functions descriptors # + class _FunctionDescriptor(object): __slots__ = ["name", "parameter_types", "return_type", "aliases"] @@ -177,6 +240,12 @@ def __init__(self, name, parameter_types=None, return_type=None, aliases=None): BOOL, ["SymGetSourceFile"] ), + + # SymRegisterCallback + # https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symregistercallback + _FunctionDescriptor("SymRegisterCallbackW64", + (HANDLE, PSYMBOL_REGISTERED_CALLBACK64, PVOID), + BOOL), ] @@ -264,6 +333,30 @@ class SSRVOPT(enum.IntFlag): SSRVOPT_ENABLE_COMM_MSG = 0x10000000 +class CBA(enum.IntEnum): + """Values passed to various callbacks used by dbghelp. + + Only one value can be used at a time. + """ + CBA_DEFERRED_SYMBOL_LOAD_START = 0x00000001 + CBA_DEFERRED_SYMBOL_LOAD_COMPLETE = 0x00000002 + CBA_DEFERRED_SYMBOL_LOAD_FAILURE = 0x00000003 + CBA_SYMBOLS_UNLOADED = 0x00000004 + CBA_DUPLICATE_SYMBOL = 0x00000005 + CBA_READ_MEMORY = 0x00000006 + CBA_DEFERRED_SYMBOL_LOAD_CANCEL = 0x00000007 + CBA_SET_OPTIONS = 0x00000008 + CBA_EVENT = 0x00000010 + CBA_DEFERRED_SYMBOL_LOAD_PARTIAL = 0x00000020 + CBA_DEBUG_INFO = 0x10000000 + CBA_SRCSRV_INFO = 0x20000000 + CBA_SRCSRV_EVENT = 0x40000000 + CBA_UPDATE_STATUS_BAR = 0x50000000 + CBA_ENGINE_PRESENT = 0x60000000 + CBA_CHECK_ENGOPT_DISALLOW_NETWORK_PATHS = 0x70000000 + CBA_CHECK_ARM_MACHINE_THUMB_TYPE_OVERRIDE = 0x80000000 + + class DbgHelp: """Main wrapper around DbgHelp.dll library functions. @@ -314,7 +407,7 @@ def _resolve_functions(self, function_descriptors): for function_descriptor in function_descriptors: self._register_function(function_descriptor) - def _register_function(self, function_descriptor) -> None: + def _register_function(self, function_descriptor): # type: (_FunctionDescriptor) -> None """[internal] Build a function ctypes wrapping from its function descriptor. diff --git a/procmon_parser/symbol_resolver/win/win_types.py b/procmon_parser/symbol_resolver/win/win_types.py index f18ba52..c74772b 100644 --- a/procmon_parser/symbol_resolver/win/win_types.py +++ b/procmon_parser/symbol_resolver/win/win_types.py @@ -4,6 +4,7 @@ """ import ctypes +BYTE = BOOLEAN = ctypes.c_uint8 HANDLE = ctypes.c_void_p BOOL = ctypes.c_long PCSTR = PCWSTR = PSTR = PWSTR = LPWSTR = ctypes.c_wchar_p From 31d245000c4a2c4d0aaca8a086ed858e4d9a1822 Mon Sep 17 00:00:00 2001 From: neitsa Date: Fri, 17 Feb 2023 12:05:24 +0100 Subject: [PATCH 17/26] Fixes and Python 2.7 compat: * Remove most of f-strings format. * Add a few missing comments. * Rewrite some typing comments. --- .../symbol_resolver/symbol_resolver.py | 110 +++++++++++------- procmon_parser/symbol_resolver/win/dbghelp.py | 18 ++- 2 files changed, 80 insertions(+), 48 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index ae69f1b..4ba5863 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -91,7 +91,8 @@ def frame(self): # type: () -> str """Return a string representation of a frame (its `FrameType` and frame number). """ - return f"{self.frame_type.name[0]} {self.frame_number}" + return "{frame_type.name[0]} {frame_number}".format( + frame_type=self.frame_type, frame_number=self.frame_number) @property def location(self): @@ -99,18 +100,21 @@ def location(self): """Return a string representation of the symbolic location at which the frame happens. """ if self.symbol_info is None: - return f"{self.address:#x}" + return "{address:#x}".format(address=self.address) # symbolic information (symbol + asm offset) - sym_str = f"{self.symbol_info.Name} + {self.displacement:#x}" + sym_str = "{symbol_info.Name} + {displacement:#x}".format( + symbol_info=self.symbol_info, displacement=self.displacement) if self.line_info is None: return sym_str # line information path = self.source_file_path if self.source_file_path else self.line_info.FileName - line_str = f"{path} ({self.line_info.LineNumber}; col: {self.line_displacement})" + line_str = "{path} ({line_info.LineNumber}; col: {line_displacement})".format( + path=path, line_info=self.line_info, line_displacement=self.line_displacement + ) - return f"{sym_str}, {line_str}" + return "{sym_str}, {line_str}".format(sym_str=sym_str, line_str=line_str) @property def module_name(self): @@ -134,7 +138,9 @@ def module_path(self): def __repr__(self): # type: () -> str - return f"{self.frame} {self.module_name} {self.location} {self.address:#x} {self.module_path}" + return "{frame} {module_name} {location} {address:#x} {module_path}".format( + frame=self.frame, module_name=self.module_name, location=self.location, address=self.address, + module_path=self.module_path) class StackTraceInformation(object): @@ -159,7 +165,7 @@ def prettify(resolved_stack_trace): max_frame = max(len(stfi.frame) for stfi in resolved_stack_trace) max_module = max(len(stfi.module_name) for stfi in resolved_stack_trace) max_location = max(len(stfi.location) for stfi in resolved_stack_trace) - max_address = max(len(f"{stfi.address:#x}") for stfi in resolved_stack_trace) + max_address = max(len("{stfi.address:#x}".format(stfi=stfi)) for stfi in resolved_stack_trace) output = list() for stfi in resolved_stack_trace: @@ -174,12 +180,13 @@ class SymbolResolver(object): """ def __init__(self, - procmon_logs_reader, - dll_dir_path=None, - skip_symsrv=False, - symbol_path=None, - debug_callback=None): - # type: (procmon_parser.ProcmonLogsReader, str | pathlib.Path | None, bool, str, typing.Callable) -> None + procmon_logs_reader, # type: procmon_parser.ProcmonLogsReader + dll_dir_path=None, # type: str | pathlib.Path | None + skip_symsrv=False, # type: bool + symbol_path=None, # type: str + debug_callback=None # type: typing.Callable[[int, CBA | int, str, int], int] + ): + # type: (...) -> None """Class Initialisation. Args: @@ -190,7 +197,7 @@ def __init__(self, symbol_path: Replace the `_NT_SYMBOL_PATH` environment variable if it exists, or prevent using %TEMP% as the download location of the symbol files. This must be a string compatible with the `_NT_SYMBOL_PATH` syntax. - debug_mode: If set to True, set the dbghelp functionalities in debug mode (can be used to understand and + debug_callback: If set to True, set the dbghelp functionalities in debug mode (can be used to understand and debug problems with symbol downloading and resolution). Notes: @@ -218,12 +225,14 @@ def __init__(self, else: # just check that the given dir contains dbghelp and symsrv. if not dll_dir_path.is_dir(): - raise ValueError(f"The given path '{dll_dir_path}' is not a directory.") + raise ValueError("The given path '{dll_dir_path}' is not a directory.".format( + dll_dir_path=dll_dir_path)) files_to_check = ["dbghelp.dll"] if not skip_symsrv: files_to_check.append("symsrv.dll") if not all((dll_dir_path / file_name).is_file() for file_name in files_to_check): - raise ValueError(f"The given path must be a path to a directory containing: {files_to_check!r}.") + raise ValueError("The given path must be a path to a directory containing: {files_to_check!r}.".format( + files_to_check=files_to_check)) self.dll_dir_path = dll_dir_path # _NT_SYMBOL_PATH is needed to store symbols locally. If it's not set, we need to set it. @@ -231,10 +240,12 @@ def __init__(self, if nt_symbol_path is None: if symbol_path is None: # resolve TEMP folder and set it at the symbol path. - symbol_path = f"srv*{os.environ['TEMP']}*https://msdl.microsoft.com/download/symbols" + symbol_path = "srv*{environ_tmp}*https://msdl.microsoft.com/download/symbols".format( + environ_tmp=os.environ['TEMP']) # set symbol path os.environ["_NT_SYMBOL_PATH"] = symbol_path - logger.debug(f"NT_SYMBOL_PATH: {os.environ['_NT_SYMBOL_PATH']}") + logger.debug("NT_SYMBOL_PATH: {environ_nt_symbol_path}".format( + environ_nt_symbol_path=os.environ['_NT_SYMBOL_PATH'])) # DbgHelp wrapper instance initialisation and symbolic option setting. self._dbghelp = DbgHelp(self.dll_dir_path / "dbghelp.dll") @@ -266,9 +277,9 @@ def __init__(self, sys_pid = next((p for p in procmon_logs_reader.processes() if p.pid == 4), None) sys_name = next((p for p in procmon_logs_reader.processes() if p.process_name.lower() == "system"), None) if sys_pid is not None: - logger.debug(f"Process w/ PID = 4: {sys_pid!r}") + logger.debug("Process w/ PID = 4: {sys_pid!r}".format(sys_pid=sys_pid)) if sys_name is not None: - logger.debug(f"Process w/ Name = 'System': {sys_name!r}") + logger.debug("Process w/ Name = 'System': {sys_name!r}".format(sys_name=sys_name)) raise RuntimeError("Could not get system modules.") def find_module(self, event, address): @@ -348,22 +359,24 @@ def resolve_stack_trace(self, event): callback = PSYMBOL_REGISTERED_CALLBACK64(self._symbol_registered_callback) self._dbghelp.SymRegisterCallbackW64(pid, callback, PVOID(pid)) - logger.debug(f"# Stack Trace frames: {len(event.stacktrace)}") - logger.debug(f"PID: {pid:#08x}") + logger.debug("# Stack Trace frames: {len_event_stack_trace}".format( + len_event_stack_trace=len(event.stacktrace))) + logger.debug("PID: {pid:#08x}".format(pid=pid)) # Resolve each of the addresses in the stack trace, frame by frame. for frame_number, address in enumerate(event.stacktrace): frame_type = FrameType.from_address(address, self._max_user_address) - logger.debug(f"{'-' * 79}\nStack Frame: {frame_number:04} type: {frame_type}") + logger.debug("{sep}\nStack Frame: {frame_number:04} type: {frame_type}".format( + sep='-' * 79, frame_number=frame_number, frame_type=frame_type)) # find the module that contains the given address. It might not be found. - logger.debug(f"Address: {address:#016x}") + logger.debug("Address: {address:#016x}".format(address=address)) module = self.find_module(event, address) if not module: yield StackTraceFrameInformation(frame_type, frame_number, address) continue - logger.debug(f"Address: {address:#016x} --> Module: {module!r}") + logger.debug("Address: {address:#016x} --> Module: {module!r}".format(address=address, module=module)) # We have the address and the module name. Get the corresponding file from the Symbol store! # Once we have the file, we'll be able to query the symbol for the address. @@ -390,7 +403,8 @@ def resolve_stack_trace(self, event): ) if not ret_val: last_err = ctypes.get_last_error() - logger.debug(f"SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).") + logger.debug("SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).".format( + j=j, last_err=last_err)) if j == 0 and last_err == 2: # ERROR_FILE_NOT_FOUND # 1st try and file was not found: check if the directory exists. If it is, give it another try. dir_path = pathlib.Path(module.path).parent @@ -402,7 +416,8 @@ def resolve_stack_trace(self, event): break else: # no more tries left or unknown error. - logger.error(f"SymFindFileInPathW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + logger.error("SymFindFileInPathW: ({last_err:#08x}) {formatted_last_err}".format( + last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) break else: # no error. @@ -412,7 +427,7 @@ def resolve_stack_trace(self, event): yield StackTraceFrameInformation(frame_type, frame_number, address, module) continue - logger.debug(f"Found file: {found_file.value}") + logger.debug("Found file: {found_file.value}".format(found_file=found_file)) # We have the file from the symbol store, we now 'load' the symbolic module (it does not load it inside # the process address space) to be able to query the symbol right after that. @@ -431,11 +446,12 @@ def resolve_stack_trace(self, event): # module was already loaded. This is not an error in this case. last_err = ctypes.get_last_error() if last_err != 0: # if it's not 0, then it's really an error. - logger.error(f"SymLoadModuleExW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + logger.error("SymLoadModuleExW: ({last_err:#08x}) {formatted_last_err}".format( + last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) yield StackTraceFrameInformation(frame_type, frame_number, address, module) continue - logger.debug(f"Module Base: {module_base:#x}") + logger.debug("Module Base: {module_base:#x}".format(module_base=module_base)) # Now that we have loaded the symbolic module, we query it with the address (lying inside it) to get the # name of the symbol and the displacement from the symbol (if any). @@ -451,11 +467,13 @@ def resolve_stack_trace(self, event): ) if ret_val == 0: last_err = ctypes.get_last_error() - logger.error(f"SymFromAddr: ({last_err:#08x}) {ctypes.FormatError(last_err)}") + logger.error("SymFromAddr: ({last_err:#08x}) {formatted_last_err}".format( + last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) yield StackTraceFrameInformation(frame_type, frame_number, address, module) continue - logger.debug(f"Symbol Name: {symbol_info.Name}; Displacement: {displacement.value:#08x}") + logger.debug("Symbol Name: {symbol_info.Name}; Displacement: {displacement.value:#08x}".format( + symbol_info=symbol_info, displacement=displacement)) # In case we have source information, we need to continue to query the symbol to get source information such # as the source file name and the line number. This obviously fails if there are no symbolic source code @@ -472,8 +490,9 @@ def resolve_stack_trace(self, event): # The above call fails if there are no source code information. This is the default for Windows binaries. if ret_val == 0: last_err = ctypes.get_last_error() - logger.debug(f"SymGetLineFromAddrW64 [no source line]: ({last_err:#08x}) " - f"{ctypes.FormatError(last_err)}") + logger.debug( + "SymGetLineFromAddrW64 [no source line]: ({last_err:#08x}) {formatted_last_err}".format( + last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value) continue @@ -487,17 +506,17 @@ def resolve_stack_trace(self, event): file_name = ctypes.create_unicode_buffer(line.FileName) # noqa line.FileName = ctypes.cast(file_name, ctypes.c_wchar_p) - logger.debug(f"File Name: '{line.FileName}'; Line Number: {line.LineNumber}; " - f"Line Displacement (col): {line_displacement.value}") + logger.debug("File Name: '{line.FileName}'; Line Number: {line.LineNumber}; " + "Line Displacement (col): {line_displacement.value}".format( + line=line, line_displacement=line_displacement)) # noqa # It's possible that the returned line.Filename is already a fully qualified path, in which case there's no # need to call SymGetSourceFileW, as the latter would be only used to retrieve the fully qualified path. # We just check that we already have fully qualified path. If it is, then we bail out, otherwise we call # SymGetSourceFileW. - fully_qualified_source_path = None if pathlib.Path(line.FileName).is_absolute(): # noqa # we have a fully qualified source file path. - logger.debug(f"source file path [from line.Filename]: {line.FileName}") + logger.debug("source file path [from line.Filename]: {line.FileName}".format(line=line)) fully_qualified_source_path = line.FileName else: # we don't have a fully qualified source file path. @@ -513,12 +532,15 @@ def resolve_stack_trace(self, event): ) if ret_val == 0: last_err = ctypes.get_last_error() - logger.debug(f"SymGetSourceFileW: ({last_err:#08x}) {ctypes.FormatError(last_err)}") - logger.debug(f"SymGetSourceFileW failed: using '{line.FileName}' as fallback.") + logger.debug("SymGetSourceFileW: ({last_err:#08x}) {formatted_last_err}".format( + last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) + logger.debug("SymGetSourceFileW failed: using '{line.FileName}' as fallback.".format( + line=line)) # use line.FileName as fallback fully_qualified_source_path = line.FileName else: - logger.debug(f"source file path [from SymGetSourceFileW]: {source_file_path.value}") + logger.debug("source file path [from SymGetSourceFileW]: {source_file_path.value}".format( + source_file_path=source_file_path)) fully_qualified_source_path = source_file_path.value yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value, @@ -597,7 +619,8 @@ def find_debugging_tools(): debugger_path: pathlib.Path | None = None try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sdk_key) as top_key: - value, value_type = winreg.QueryValueEx(top_key, f"WindowsDebuggersRoot{max_ver_str}") + value, value_type = winreg.QueryValueEx(top_key, "WindowsDebuggersRoot{max_ver_str}".format( + max_ver_str=max_ver_str)) if value_type == winreg.REG_SZ: debugger_path = pathlib.Path(value) except OSError: @@ -637,7 +660,8 @@ def find_windbg_preview(): key_name = winreg.EnumKey(top_key, i) if "microsoft.windbg" in key_name.lower(): # found Windbg Preview. Get its installation location. - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, f"{package_key}\\{key_name}") as windbg_key: + install_key = "{package_key}\\{key_name}".format(package_key=package_key, key_name=key_name) + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, install_key) as windbg_key: install_location, key_type = winreg.QueryValueEx(windbg_key, "PackageRootFolder") if key_type == winreg.REG_SZ: windbg_location = pathlib.Path(install_location) diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 3d8b31c..fa91ced 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -149,6 +149,11 @@ class IMAGEHLP_DUPLICATE_SYMBOL64(ctypes.Structure): # noqa class IMAGEHLP_CBA_EVENTW(ctypes.Structure): # noqa + """Contains information about a debugging event. + + See Also: + https://learn.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-imagehlp_cba_eventw + """ _fields_ = ( ("severity", DWORD), ("code", DWORD), @@ -378,12 +383,12 @@ def __init__(self, dbghelp_path): dbghelp_path: Path to the dbghelp.dll library. """ if not dbghelp_path.is_file(): - raise ValueError(f"The given path '{dbghelp_path}' is not a file.") + raise ValueError("The given path '{dbghelp_path}' is not a file.".format(dbghelp_path=dbghelp_path)) self._dll_path = dbghelp_path # Dictionary of functions; key is str (function name), value is ctypes function pointer. - self._functions: dict[str: _ctypes.CFuncPtr] = dict() + self._functions = dict() # type: dict[str: _ctypes.CFuncPtr] # DLL instance self._dbghelp = ctypes.WinDLL(str(dbghelp_path), use_last_error=True) @@ -391,10 +396,12 @@ def __init__(self, dbghelp_path): # resolve all needed functions. self._resolve_functions(_functions_descriptors) - def __getitem__(self, item: str): + def __getitem__(self, item): + # type: (str) -> _ctypes.CFuncPtr return self._functions[item] - def __getattr__(self, item: str): + def __getattr__(self, item): + # type: (str) -> _ctypes.CFuncPtr return self[item] def _resolve_functions(self, function_descriptors): @@ -423,7 +430,8 @@ def _register_function(self, function_descriptor): # We land here if the function can't be found in the given DLL. # note: it raises from quite deep inside ctypes if the function can't be resolved, which might be confusing. # Log it now and re-raise. - logger.error(f"The function {function_descriptor.name} was not found in the DLL: '{self._dll_path!r}'.") + logger.error("The function {function_descriptor.name} was not found in the DLL: " + "'{dll_path!r}'.".format(function_descriptor=function_descriptor, dll_path=self.dll_path)) raise if function_descriptor.parameter_types: function_pointer.argtypes = function_descriptor.parameter_types From 90eeff6b3df61126adf14977b1253d98023db16b Mon Sep 17 00:00:00 2001 From: neitsa Date: Sat, 18 Feb 2023 10:38:41 +0100 Subject: [PATCH 18/26] rm kernel32.py since it's not used. --- procmon_parser/symbol_resolver/win/kernel32.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 procmon_parser/symbol_resolver/win/kernel32.py diff --git a/procmon_parser/symbol_resolver/win/kernel32.py b/procmon_parser/symbol_resolver/win/kernel32.py deleted file mode 100644 index 57910ba..0000000 --- a/procmon_parser/symbol_resolver/win/kernel32.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- -import ctypes -from .win_types import BOOL, LPWSTR - -_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) - -# SetEnvironmentVariableW -# https://learn.microsoft.com/en-us/windows/win32/api/processenv/nf-processenv-setenvironmentvariablew -SetEnvironmentVariableW = _kernel32.SetEnvironmentVariableW -SetEnvironmentVariableW.argtypes = (LPWSTR, LPWSTR) -SetEnvironmentVariableW.restype = BOOL -SetEnvironmentVariable = SetEnvironmentVariableW From bec1c22c694300d0327d5d97e5cec96400cfb57a Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 22 Feb 2023 14:14:12 +0100 Subject: [PATCH 19/26] Fixes following review: * Fix callback documentation in constructor. * Add a more meaningful error message if DLLs can't be found. --- procmon_parser/symbol_resolver/symbol_resolver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 4ba5863..6ab5c94 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -197,8 +197,8 @@ def __init__(self, symbol_path: Replace the `_NT_SYMBOL_PATH` environment variable if it exists, or prevent using %TEMP% as the download location of the symbol files. This must be a string compatible with the `_NT_SYMBOL_PATH` syntax. - debug_callback: If set to True, set the dbghelp functionalities in debug mode (can be used to understand and - debug problems with symbol downloading and resolution). + debug_callback: A callback which can be used to understand and debug problems with symbol downloading and + resolution. Notes: If `dll_dir_path` is None, then the code does its best to find matching installations of the Debugging Tools @@ -221,7 +221,8 @@ def __init__(self, (v for v in [DbgHelpUtils.find_debugging_tools(), DbgHelpUtils.find_windbg_preview()] if v is not None), None) if not dll_dir_path: - raise ValueError("You need to provide a valid path to 'dbghelp.dll' and 'symsrv.dll'.") + raise ValueError("You need to provide a valid path to 'dbghelp.dll' and 'symsrv.dll' or install either " + "debugging tools or windbg preview.") else: # just check that the given dir contains dbghelp and symsrv. if not dll_dir_path.is_dir(): From 704ccdb9fad2e33250a189bef3bca6dbc9e53826 Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 22 Feb 2023 14:17:52 +0100 Subject: [PATCH 20/26] Fix following review: use for/else construct for system modules. --- procmon_parser/symbol_resolver/symbol_resolver.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 6ab5c94..3d60d12 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -272,9 +272,8 @@ def __init__(self, if process.process_name in ["System"] and process.user.lower() == "nt authority\\system": self.system_modules = process.modules break - - # Defensive check. - if not self.system_modules: + else: + # Couldn't find system modules. Log possible candidates. sys_pid = next((p for p in procmon_logs_reader.processes() if p.pid == 4), None) sys_name = next((p for p in procmon_logs_reader.processes() if p.process_name.lower() == "system"), None) if sys_pid is not None: From 8359bef7f75e3b4b37eb854c0640644f19fc54d3 Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 22 Feb 2023 14:22:51 +0100 Subject: [PATCH 21/26] Fix following review: use MAX_PATH constant rather than hardcoded numbers. --- procmon_parser/symbol_resolver/symbol_resolver.py | 7 +++++-- procmon_parser/symbol_resolver/win/dbghelp.py | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 3d60d12..d663cdb 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -27,6 +27,9 @@ logger = logging.getLogger(__name__) +# Maximum (default) Windows path length. +MAX_PATH = 260 + @enum.unique class FrameType(enum.Enum): @@ -380,7 +383,7 @@ def resolve_stack_trace(self, event): # We have the address and the module name. Get the corresponding file from the Symbol store! # Once we have the file, we'll be able to query the symbol for the address. - found_file = ctypes.create_unicode_buffer(260 * 2) + found_file = ctypes.create_unicode_buffer(MAX_PATH * ctypes.sizeof(ctypes.c_wchar)) module_id = PVOID(module.timestamp) search_path = None # use the default search path provided to SymInitialize. @@ -520,7 +523,7 @@ def resolve_stack_trace(self, event): fully_qualified_source_path = line.FileName else: # we don't have a fully qualified source file path. - source_file_path_size = DWORD(260) + source_file_path_size = DWORD(MAX_PATH) source_file_path = ctypes.create_unicode_buffer(source_file_path_size.value) ret_val = self._dbghelp.SymGetSourceFileW( pid, # hProcess diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index fa91ced..36ac48b 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -21,9 +21,6 @@ logger = logging.getLogger(__name__) -# -MAX_PATH = 260 - # # Callback Functions needed by some DbgHelp APIs. # From 55dc790cf105aa66a774a5d086c2b81df662c55e Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 22 Feb 2023 14:26:24 +0100 Subject: [PATCH 22/26] Fix following review: Add ERROR_FILE_NOT_FOUND constant. --- procmon_parser/symbol_resolver/symbol_resolver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index d663cdb..074c281 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -29,6 +29,8 @@ # Maximum (default) Windows path length. MAX_PATH = 260 +# Windows standard error code. +ERROR_FILE_NOT_FOUND = 2 @enum.unique @@ -408,7 +410,7 @@ def resolve_stack_trace(self, event): last_err = ctypes.get_last_error() logger.debug("SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).".format( j=j, last_err=last_err)) - if j == 0 and last_err == 2: # ERROR_FILE_NOT_FOUND + if j == 0 and last_err == ERROR_FILE_NOT_FOUND: # 1st try and file was not found: check if the directory exists. If it is, give it another try. dir_path = pathlib.Path(module.path).parent if dir_path.is_dir(): From 928d261477b0a352caadd7f73eaf674a3eb58779 Mon Sep 17 00:00:00 2001 From: neitsa Date: Wed, 22 Feb 2023 15:15:09 +0100 Subject: [PATCH 23/26] Fix following review: rework symbol file retrieval. --- .../symbol_resolver/symbol_resolver.py | 99 +++++++++++-------- procmon_parser/symbol_resolver/win/dbghelp.py | 2 + .../symbol_resolver/win/win_consts.py | 7 ++ 3 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 procmon_parser/symbol_resolver/win/win_consts.py diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 074c281..825ca1c 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -24,14 +24,10 @@ DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT, PSYMBOL_REGISTERED_CALLBACK64, CBA, PIMAGEHLP_CBA_EVENTW) from procmon_parser.symbol_resolver.win.win_types import PVOID, HANDLE, DWORD64, DWORD, ULONG, ULONG64, BOOL +from procmon_parser.symbol_resolver.win.win_consts import MAX_PATH, ERROR_FILE_NOT_FOUND logger = logging.getLogger(__name__) -# Maximum (default) Windows path length. -MAX_PATH = 260 -# Windows standard error code. -ERROR_FILE_NOT_FOUND = 2 - @enum.unique class FrameType(enum.Enum): @@ -385,7 +381,6 @@ def resolve_stack_trace(self, event): # We have the address and the module name. Get the corresponding file from the Symbol store! # Once we have the file, we'll be able to query the symbol for the address. - found_file = ctypes.create_unicode_buffer(MAX_PATH * ctypes.sizeof(ctypes.c_wchar)) module_id = PVOID(module.timestamp) search_path = None # use the default search path provided to SymInitialize. @@ -393,44 +388,21 @@ def resolve_stack_trace(self, event): # 1. The module is an MS module, in which case it's going to be resolved pretty much automatically. # 1.a if it's not an MS module it's going to fail. # 2. If it's not an MS module, then we indicate to SymFindFileInPath where to find the binary in SearchPath. - for j in range(2): - ret_val = self._dbghelp.SymFindFileInPathW( - HANDLE(pid), # hProcess - search_path, # SearchPath - module.path, # FileName (PCWSTR: it's fine to pass a python string) - ctypes.byref(module_id), # id - module.size, # two - 0, # three - SSRVOPT.SSRVOPT_GUIDPTR, # flags: ProcMon uses 'SSRVOPT_GUIDPTR' but it's not a GUID??? still works - found_file, # [out] FoundFile - PFINDFILEINPATHCALLBACK(0), # callback (nullptr) - None # context - ) - if not ret_val: - last_err = ctypes.get_last_error() - logger.debug("SymFindFileInPathW failed at attempt {j} (error: {last_err:#08x}).".format( - j=j, last_err=last_err)) - if j == 0 and last_err == ERROR_FILE_NOT_FOUND: - # 1st try and file was not found: check if the directory exists. If it is, give it another try. - dir_path = pathlib.Path(module.path).parent - if dir_path.is_dir(): - # directory exists; loop and try again. - search_path = str(dir_path) - else: - # the directory doesn't contain the required file on the local computer; just get out. - break - else: - # no more tries left or unknown error. + ret_val, found_file = self._find_file(pid, search_path, module, module_id) + if not ret_val: + last_err = ctypes.get_last_error() + logger.debug("SymFindFileInPathW failed at attempt 0 (error: {last_err:#08x}).".format( + last_err=last_err)) + if last_err == ERROR_FILE_NOT_FOUND: + # check if the directory exists. If it is, use it as the search path. + dir_path = pathlib.Path(module.path).parent + search_path = str(dir_path) if dir_path.is_dir() else search_path + ret_val, found_file = self._find_file(pid, search_path, module, module_id) + if ret_val == 0: logger.error("SymFindFileInPathW: ({last_err:#08x}) {formatted_last_err}".format( last_err=last_err, formatted_last_err=ctypes.FormatError(last_err))) - break - else: - # no error. - break - - if not found_file.value: - yield StackTraceFrameInformation(frame_type, frame_number, address, module) - continue + yield StackTraceFrameInformation(frame_type, frame_number, address, module) + continue logger.debug("Found file: {found_file.value}".format(found_file=found_file)) @@ -554,8 +526,51 @@ def resolve_stack_trace(self, event): # dbghelp symbol cleanup self._dbghelp.SymCleanup(pid) + def _find_file(self, pid, search_path, module, module_id): + # type: (int, str | None, procmon_parser.Module, ctypes.c_void_p[int]) -> tuple[int, ctypes.Array[ctypes.c_wchar]] + """[Internal] Locates a symbol file or executable image. + + Args: + pid: process ID; must be the same as the one passed to SymInitialize. + search_path: Either None (use the default search path) or a specific search path. + module: The module for which we're trying to find the symbol information. + module_id: pointer to id, see SymFindFileInPathW documentation for more information. + + Returns: + A tuple: the return code of the SymFindFileInPathW function and the path to the symbolic file on success. + """ + found_file = ctypes.create_unicode_buffer(MAX_PATH * ctypes.sizeof(ctypes.c_wchar)) + ret_val = self._dbghelp.SymFindFileInPathW( + HANDLE(pid), # hProcess + search_path, # SearchPath + module.path, # FileName (PCWSTR: it's fine to pass a python string) + ctypes.byref(module_id), # id + module.size, # two + 0, # three + SSRVOPT.SSRVOPT_GUIDPTR, # flags: ProcMon uses 'SSRVOPT_GUIDPTR' but it's not a GUID??? still works + found_file, # [out] FoundFile + PFINDFILEINPATHCALLBACK(0), # callback (nullptr) + None # context + ) + return ret_val, found_file + def _symbol_registered_callback(self, handle, action_code, callback_data, user_context): # type: (HANDLE, ULONG, ULONG64, ULONG64) -> BOOL + """[Internal] Callback passed to SymRegisterCallbackW64. Translates arguments to python types before calling the + user-defined callback. + + Notes: + Only called if a callback is passed to SymbolResolver constructor. + + Args: + handle: handle passed initially to SymInitialize (actually the pid of the process). + action_code: One of the constant defined in the `CBA` enumeration. + callback_data: data passed from the symbol engine. Interpretation of the data depends on the action code. + user_context: user defined data; The pid of the process. + + Returns: + The value returned by the user-defined callback. + """ param_callback_data = callback_data try: param_action_code = CBA(action_code) diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 36ac48b..77ac410 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -12,6 +12,7 @@ from procmon_parser.symbol_resolver.win.win_types import ( HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD, BOOLEAN) +from procmon_parser.symbol_resolver.win.win_consts import MAX_PATH if sys.version_info >= (3, 5, 0): import typing @@ -21,6 +22,7 @@ logger = logging.getLogger(__name__) + # # Callback Functions needed by some DbgHelp APIs. # diff --git a/procmon_parser/symbol_resolver/win/win_consts.py b/procmon_parser/symbol_resolver/win/win_consts.py new file mode 100644 index 0000000..9a064b1 --- /dev/null +++ b/procmon_parser/symbol_resolver/win/win_consts.py @@ -0,0 +1,7 @@ +"""Windows Constants +""" +# Windows (default) max path length. +MAX_PATH = 260 + +# Windows standard error codes. +ERROR_FILE_NOT_FOUND = 2 From 161de347cbd088a0691944b2ddc1951967a4d8eb Mon Sep 17 00:00:00 2001 From: neitsa Date: Thu, 23 Feb 2023 16:33:25 +0100 Subject: [PATCH 24/26] Fix following review: raise if symbol path is not set and skip_symsrv is True. --- procmon_parser/symbol_resolver/symbol_resolver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index 825ca1c..d2d4b62 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -209,7 +209,8 @@ def __init__(self, Raises: ValueError: The provided DLL path is not a valid directory, does not contain the required DLL(s) or the automatic - finder could not find the required DLL. + finder could not find the required DLL. Also raises if `skip_symsrv` is True but _NT_SYMBOL_PATH env. + var. is not set. RuntimeError: The initialisation couldn't get the system modules. """ @@ -240,6 +241,8 @@ def __init__(self, # _NT_SYMBOL_PATH is needed to store symbols locally. If it's not set, we need to set it. nt_symbol_path = os.environ.get("_NT_SYMBOL_PATH", None) if nt_symbol_path is None: + if skip_symsrv: + raise ValueError("_NT_SYMBOL_PATH env. var. is not set: you can't skip the symsrv.dll check.") if symbol_path is None: # resolve TEMP folder and set it at the symbol path. symbol_path = "srv*{environ_tmp}*https://msdl.microsoft.com/download/symbols".format( From cf82b9dbafd27aee38b23f9c970128177856469c Mon Sep 17 00:00:00 2001 From: neitsa Date: Fri, 24 Feb 2023 17:26:07 +0100 Subject: [PATCH 25/26] Fix following review: use a context manager to handle symbol engine init and clean up. --- .../symbol_resolver/symbol_resolver.py | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index d2d4b62..c186c83 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -2,6 +2,7 @@ # -*- coding:utf-8 -*- """Module used to resolve symbolic information for a given a stack trace. """ +import contextlib import ctypes import enum import logging @@ -332,8 +333,10 @@ def resolve_stack_trace(self, event): The `ProcmonLogsReader` instance must be instantiated with `should_get_stacktrace` set to True (default). Raises: - RuntimeError: the given event des not contain any stack trace information. Be sure to call - `ProcmonLogsReader` with the `should_get_stacktrace` parameter set to True. + RuntimeError: + - The given event des not contain any stack trace information (be sure to call `ProcmonLogsReader` with + the `should_get_stacktrace` parameter set to True). + - The symbol engine could not be correctly initialized. Examples: ```python @@ -351,13 +354,15 @@ def resolve_stack_trace(self, event): Yields: An instance of `StackTraceFrameInformation` for each of the frame in the stack trace. """ + with self._dbghelp_init(event) as sym_pid: + yield from self._resolve_stack_trace(event, sym_pid) + + def _resolve_stack_trace(self, event, pid): + # type: (procmon_parser.Event, int) -> typing.Iterator[StackTraceFrameInformation] + if not event.stacktrace or event.stacktrace is None: raise RuntimeError("Trying to resolve a stack trace while there is no stack trace.") - # Initialize dbghelp symbolic information. - pid = event.process.pid - self._dbghelp.SymInitialize(pid, None, False) - # set up callback if we are in debug mode if self._debug_callback: callback = PSYMBOL_REGISTERED_CALLBACK64(self._symbol_registered_callback) @@ -526,9 +531,6 @@ def resolve_stack_trace(self, event): yield StackTraceFrameInformation(frame_type, frame_number, address, module, symbol_info, displacement.value, line, line_displacement.value, fully_qualified_source_path) - # dbghelp symbol cleanup - self._dbghelp.SymCleanup(pid) - def _find_file(self, pid, search_path, module, module_id): # type: (int, str | None, procmon_parser.Module, ctypes.c_void_p[int]) -> tuple[int, ctypes.Array[ctypes.c_wchar]] """[Internal] Locates a symbol file or executable image. @@ -588,6 +590,36 @@ def _symbol_registered_callback(self, handle, action_code, callback_data, user_c ret = self._debug_callback(handle, param_action_code, param_callback_data, user_context) return ret + @contextlib.contextmanager + def _dbghelp_init(self, event): + # type: (procmon_parser.Event) -> typing.Generator[int, None, None] + """[internal] context manager used to make sure we clean up symbol information on exit from the + `resolve_stack_trace` function. Takes care of initializing and performing cleanup of the symbol engine. + + Args: + event: The event for which the stack trace is to be resolved. + + Returns: + The process pid used by the symbolic functions. + """ + pid = event.process.pid + if self._dbghelp.SymInitialize(pid, None, False) == 0: + last_err = ctypes.get_last_error() + raise RuntimeError("SymInitialize failed: {last_err:#08x}; msg: {err_msg}".format( + last_err=last_err, err_msg=ctypes.FormatError(last_err))) + else: + logger.debug("SymInitialize OK.") + try: + yield pid + finally: + if self._dbghelp.SymCleanup(pid) == 0: + last_err = ctypes.get_last_error() + raise RuntimeError("SymCleanup failed: {last_err:#08x}; msg: {err_msg}".format( + last_err=last_err, err_msg=ctypes.FormatError(last_err))) + else: + logger.debug("SymCleanup OK.") + + class DbgHelpUtils(object): """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg From e57aaf922b9e0a0c2387fdcf199ace1fba7f547d Mon Sep 17 00:00:00 2001 From: neitsa Date: Sat, 4 Mar 2023 14:37:44 +0100 Subject: [PATCH 26/26] More fixes for python 2.7. --- procmon_parser/__init__.py | 2 +- .../symbol_resolver/symbol_resolver.py | 30 ++++++++++++------- procmon_parser/symbol_resolver/win/dbghelp.py | 12 +++++--- setup.py | 4 ++- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/procmon_parser/__init__.py b/procmon_parser/__init__.py index a27f854..8c1e90c 100644 --- a/procmon_parser/__init__.py +++ b/procmon_parser/__init__.py @@ -13,7 +13,7 @@ 'Rule', 'Column', 'RuleAction', 'RuleRelation', 'PMLError' ] -if sys.platform == "win32" and sys.version_info >= (3, 5, 0): +if sys.platform == "win32": from procmon_parser.symbol_resolver.symbol_resolver import ( SymbolResolver, StackTraceFrameInformation, StackTraceInformation, CBA) diff --git a/procmon_parser/symbol_resolver/symbol_resolver.py b/procmon_parser/symbol_resolver/symbol_resolver.py index c186c83..a913533 100644 --- a/procmon_parser/symbol_resolver/symbol_resolver.py +++ b/procmon_parser/symbol_resolver/symbol_resolver.py @@ -10,16 +10,20 @@ import platform import re import sys -import winreg import procmon_parser if sys.platform != "win32": raise RuntimeError("Symbol Resolver can only be used on Windows Operating Systems.") -if sys.version_info >= (3, 5, 0): +_ver = sys.version_info[:3] +if _ver >= (3, 5, 0): + import winreg import typing - import pathlib # TODO: to be converted to py 2.7 equivalent. + import pathlib +elif _ver <= (2, 7, 18): + import pathlib2 as pathlib + import _winreg as winreg from procmon_parser.symbol_resolver.win.dbghelp import ( DbgHelp, PFINDFILEINPATHCALLBACK, SYMBOL_INFOW, IMAGEHLP_LINEW64, SYMOPT, SSRVOPT, PSYMBOL_REGISTERED_CALLBACK64, @@ -171,8 +175,11 @@ def prettify(resolved_stack_trace): output = list() for stfi in resolved_stack_trace: - output.append(f"{stfi.frame:<{max_frame}} {stfi.module_name:<{max_module}} {stfi.location:<{max_location}} " - f"0x{stfi.address:<{max_address}x} {stfi.module_path}") + output.append("{stfi.frame:<{max_frame}} {stfi.module_name:<{max_module}} {stfi.location:<{max_location}} " + "0x{stfi.address:<{max_address}x} {stfi.module_path}".format( + stfi=stfi, max_frame=max_frame, max_module=max_module, max_location=max_location, + max_address=max_address) + ) return '\n'.join(output) @@ -227,6 +234,8 @@ def __init__(self, raise ValueError("You need to provide a valid path to 'dbghelp.dll' and 'symsrv.dll' or install either " "debugging tools or windbg preview.") else: + if isinstance(dll_dir_path, str): + dll_dir_path = pathlib.Path(dll_dir_path) # just check that the given dir contains dbghelp and symsrv. if not dll_dir_path.is_dir(): raise ValueError("The given path '{dll_dir_path}' is not a directory.".format( @@ -268,7 +277,7 @@ def __init__(self, self._dbghelp.SymSetOptions(sum(dbghelp_options)) # 0x12237 (if not SYMOPT_DEBUG). # maximum user-address, used to discern between user and kernel modules (which don't change between processes). - self._max_user_address: int = procmon_logs_reader.maximum_application_address + self._max_user_address = procmon_logs_reader.maximum_application_address # Keep track of all system modules. for process in procmon_logs_reader.processes(): @@ -355,7 +364,9 @@ def resolve_stack_trace(self, event): An instance of `StackTraceFrameInformation` for each of the frame in the stack trace. """ with self._dbghelp_init(event) as sym_pid: - yield from self._resolve_stack_trace(event, sym_pid) + # yield from self._resolve_stack_trace(event, sym_pid) + for sti in self._resolve_stack_trace(event, sym_pid): + return sti def _resolve_stack_trace(self, event, pid): # type: (procmon_parser.Event, int) -> typing.Iterator[StackTraceFrameInformation] @@ -620,7 +631,6 @@ def _dbghelp_init(self, event): logger.debug("SymCleanup OK.") - class DbgHelpUtils(object): """Utility functions to automatically find DbgHelp.dll and Symsrv.dll if Debugging Tools For Windows or Windbg preview are installed on the current system. @@ -671,7 +681,7 @@ def find_debugging_tools(): max_ver = max(versions.keys()) max_ver_str = versions[max_ver] - debugger_path: pathlib.Path | None = None + debugger_path = None # type: pathlib.Path | None try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, sdk_key) as top_key: value, value_type = winreg.QueryValueEx(top_key, "WindowsDebuggersRoot{max_ver_str}".format( @@ -707,7 +717,7 @@ def find_windbg_preview(): package_key = (r"SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel" r"\Repository\Packages") - windbg_location: pathlib.Path | None = None + windbg_location = None # type: pathlib.Path | None try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, package_key) as top_key: num_keys, _, _ = winreg.QueryInfoKey(top_key) diff --git a/procmon_parser/symbol_resolver/win/dbghelp.py b/procmon_parser/symbol_resolver/win/dbghelp.py index 77ac410..c9b29e4 100644 --- a/procmon_parser/symbol_resolver/win/dbghelp.py +++ b/procmon_parser/symbol_resolver/win/dbghelp.py @@ -6,19 +6,23 @@ """ import ctypes import logging -import pathlib import sys -import enum + from procmon_parser.symbol_resolver.win.win_types import ( HANDLE, PCSTR, BOOL, DWORD, PCWSTR, PVOID, PWSTR, DWORD64, ULONG, ULONG64, WCHAR, PDWORD64, PDWORD, BOOLEAN) from procmon_parser.symbol_resolver.win.win_consts import MAX_PATH -if sys.version_info >= (3, 5, 0): +_ver = sys.version_info[:3] +if _ver >= (3, 5, 0): + import enum + import pathlib import typing - if typing.TYPE_CHECKING: import _ctypes # only used for typing as ctypes doesn't export inner types. +elif _ver <= (2, 7, 18): + import aenum as enum + import pathlib2 as pathlib logger = logging.getLogger(__name__) diff --git a/setup.py b/setup.py index 68dd765..54a22df 100644 --- a/setup.py +++ b/setup.py @@ -15,10 +15,12 @@ download_url="https://github.com/eronnen/procmon-parser/archive/v0.3.0.tar.gz", packages=["procmon_parser"], install_requires=[ - "enum34;python_version<'3.4'", + # "enum34;python_version<'3.4'", + "aenum;python_version<'3.4'", "construct>=2.10.54", "six", "ipaddress;python_version<'3'", + "pathlib2;python_version<'3'", ], classifiers=[ "Intended Audience :: Developers",