From e25e23d8cf509054bfbe6d5fe3a193b76caeb58f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:46:19 +0300 Subject: [PATCH 01/15] Create etwpatch.py --- .../framework/plugins/windows/etwpatch.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 volatility3/framework/plugins/windows/etwpatch.py diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py new file mode 100644 index 000000000..800f94623 --- /dev/null +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -0,0 +1,142 @@ +# etwpatch.py +# Plugin name: windows.etwpatch +# Volatility 3 plugin to detect ETW patching via EtwEventWrite prologue + +import contextlib +import logging + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows.extensions import pe +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + +class EtwPatch(interfaces.plugins.PluginInterface): + """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" + + # Plugin metadata for auto-discovery + _version = (1, 0, 0) + _required_framework_version = (2, 26, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name='kernel', + description='Windows kernel', + architectures=["Intel32", "Intel64"] + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name='pid', + description='Filter on specific process IDs', + element_type=int, + optional=True + ) + ] + + def _generator(self): + pid_filter = self.config.get('pid', None) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config['kernel']): + + # If the user passed --pid, only process those IDs + if pid_filter and proc.UniqueProcessId not in pid_filter: + continue + + pid = int(proc.UniqueProcessId) + proc_name = proc.ImageFileName.cast( + "string", + max_length = proc.ImageFileName.vol.count, + errors = 'replace' + ) + + # Build a per-process memory layer + try: + proc_layer_name = proc.add_process_layer() + except Exception: + continue + + proc_layer = self.context.layers[proc_layer_name] + + # Find ntdll.dll module + for module in proc.load_order_modules(): + BaseDllName = FullDllName = renderers.UnreadableValue() + with contextlib.suppress(exceptions.InvalidAddressException): + BaseDllName = module.BaseDllName.get_string() + FullDllName = module.FullDllName.get_string() + + if BaseDllName != 'ntdll.dll': + continue + + base = module.DllBase + size = module.SizeOfImage + + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + + pe_obj = pe_symbols.PESymbols.get_pefile_obj( + self.context, pe_table_name, proc_layer_name, base + ) + + try: + pe_obj.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + except Exception as e: + vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + continue + + if not hasattr(pe_obj, "DIRECTORY_ENTRY_EXPORT"): + return None + + for export in pe_obj.DIRECTORY_ENTRY_EXPORT.symbols: + if export.name not in [b"EtwEventWrite", b"EtwEventWriteFull", b"NtTraceEvent"]: + continue + + function_start = base + export.address + try: + with contextlib.suppress(exceptions.InvalidAddressException): + opcode = self.context.layers[proc_layer_name].read( + function_start, 1 + ).hex() + + # 0xC3 = RET, 0xE9 = JMP (common ETW patches) + if opcode in ('c3', 'e9'): + yield (0, ( + pid, + proc_name, + BaseDllName, + export.name.decode(), + format_hints.Hex(function_start), + opcode + )) + except Exception as e: + vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + continue + finally: + break + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("DLL", str), + ("Function", str), + ("Offset", format_hints.Hex), + ("Opcode", str) + ], + self._generator() + ) From 6ac76b27ff1dd4e402f1d26a3d174e3e436eee68 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:47:08 +0300 Subject: [PATCH 02/15] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 800f94623..76d769731 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,7 +1,6 @@ -# etwpatch.py -# Plugin name: windows.etwpatch -# Volatility 3 plugin to detect ETW patching via EtwEventWrite prologue - +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# import contextlib import logging From 31184afe858e37106dfd6364e7e622fe6088967e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:48:59 +0300 Subject: [PATCH 03/15] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 76d769731..656e8a8ac 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -3,6 +3,7 @@ # import contextlib import logging +import pefile from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements From a167ddc04dc5a6fd2bf77157bf41699218d3140e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Thu, 24 Apr 2025 20:51:02 +0300 Subject: [PATCH 04/15] Update etwpatch.py --- volatility3/framework/plugins/windows/etwpatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 656e8a8ac..281f85172 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -17,7 +17,6 @@ vollog = logging.getLogger(__name__) class EtwPatch(interfaces.plugins.PluginInterface): """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" - # Plugin metadata for auto-discovery _version = (1, 0, 0) _required_framework_version = (2, 26, 0) From 533f96ec03dd800f0c664b9c627999b2b07c2d74 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 12:53:46 +0000 Subject: [PATCH 05/15] Added extended usage of pe_symbols --- .../framework/plugins/windows/etwpatch.py | 114 ++++++++---------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 281f85172..36cb4b9a9 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -42,90 +42,80 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) ] + def _get_dll_vads(self, proc, dll_name): + """Retrieve VADs for a specific DLL in the process.""" + collected_modules = pe_symbols.PESymbols.get_proc_vads_with_file_paths(proc) + return [ + (vad_start, vad_size, proc.add_process_layer()) + for vad_start, vad_size, filepath in collected_modules + if pe_symbols.PESymbols.filename_for_path(filepath) == dll_name + ] + + def _find_symbols(self, dll_name, symbols, proc_layer_name, dll_vads): + """Find symbols for a specific DLL.""" + filter_module = {dll_name: {"names": symbols}} + process_modules = {dll_name: [(proc_layer_name, vad_start, vad_size) for vad_start, vad_size, _ in dll_vads]} + return pe_symbols.PESymbols.find_symbols(self.context, self.config_path, filter_module, process_modules) + + def _get_first_opcode(self, proc_layer_name, function_start): + """Check the first opcode of a function.""" + try: + return self.context.layers[proc_layer_name].read(function_start, 1).hex() + except exceptions.InvalidAddressException: + return None + def _generator(self): pid_filter = self.config.get('pid', None) for proc in pslist.PsList.list_processes( context=self.context, kernel_module_name=self.config['kernel']): - - # If the user passed --pid, only process those IDs + # Skip processes not in the PID filter if pid_filter and proc.UniqueProcessId not in pid_filter: continue pid = int(proc.UniqueProcessId) proc_name = proc.ImageFileName.cast( "string", - max_length = proc.ImageFileName.vol.count, - errors = 'replace' + max_length=proc.ImageFileName.vol.count, + errors='replace' ) - # Build a per-process memory layer try: proc_layer_name = proc.add_process_layer() - except Exception: + except exceptions.InvalidAddressException: continue - proc_layer = self.context.layers[proc_layer_name] + dlls_to_check = { + "ntdll.dll": [ + "EtwEventWrite", + "EtwEventWriteFull", + "NtTraceEvent" + ], + "advapi32.dll": [ + "EventWrite" + ] + } - # Find ntdll.dll module - for module in proc.load_order_modules(): - BaseDllName = FullDllName = renderers.UnreadableValue() - with contextlib.suppress(exceptions.InvalidAddressException): - BaseDllName = module.BaseDllName.get_string() - FullDllName = module.FullDllName.get_string() - - if BaseDllName != 'ntdll.dll': + for dll_name, symbols in dlls_to_check.items(): + dll_vads = self._get_dll_vads(proc, dll_name) + if not dll_vads: continue - base = module.DllBase - size = module.SizeOfImage - - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - pe_obj = pe_symbols.PESymbols.get_pefile_obj( - self.context, pe_table_name, proc_layer_name, base - ) - - try: - pe_obj.parse_data_directories( - directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] - ) - except Exception as e: - vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") + found_symbols, _ = self._find_symbols(dll_name, symbols, proc_layer_name, dll_vads) + if dll_name not in found_symbols: continue - - if not hasattr(pe_obj, "DIRECTORY_ENTRY_EXPORT"): - return None - - for export in pe_obj.DIRECTORY_ENTRY_EXPORT.symbols: - if export.name not in [b"EtwEventWrite", b"EtwEventWriteFull", b"NtTraceEvent"]: - continue - - function_start = base + export.address - try: - with contextlib.suppress(exceptions.InvalidAddressException): - opcode = self.context.layers[proc_layer_name].read( - function_start, 1 - ).hex() - - # 0xC3 = RET, 0xE9 = JMP (common ETW patches) - if opcode in ('c3', 'e9'): - yield (0, ( - pid, - proc_name, - BaseDllName, - export.name.decode(), - format_hints.Hex(function_start), - opcode - )) - except Exception as e: - vollog.debug(f"Error parsing IMAGE_DIRECTORY_ENTRY_EXPORT with {e}") - continue - finally: - break + + for symbol_name, function_start in found_symbols[dll_name]: + opcode = self._get_first_opcode(proc_layer_name, function_start) + if opcode in ('c3', 'e9'): # RET or JMP + yield (0, ( + pid, + proc_name, + dll_name, + symbol_name, + format_hints.Hex(function_start), opcode + )) def run(self): return renderers.TreeGrid( From 02634d0e30edd2cabfb966c54595b892f245194c Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 12:54:46 +0000 Subject: [PATCH 06/15] oops --- volatility3/framework/plugins/windows/etwpatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 36cb4b9a9..316af4dab 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -114,7 +114,8 @@ class EtwPatch(interfaces.plugins.PluginInterface): proc_name, dll_name, symbol_name, - format_hints.Hex(function_start), opcode + format_hints.Hex(function_start), + opcode )) def run(self): From d449104d8ba1e1459a58fdd6cdf4a6070703a723 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 13:26:14 +0000 Subject: [PATCH 07/15] removed unnecessary imports --- volatility3/framework/plugins/windows/etwpatch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 316af4dab..d6632617a 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,15 +1,11 @@ # This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import contextlib import logging -import pefile from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) From c9386782724311f2b1f8c373dc4af414dc4f875f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 14:21:24 +0000 Subject: [PATCH 08/15] Fixed according to atcuno comments --- .../framework/plugins/windows/etwpatch.py | 122 ++++++++---------- 1 file changed, 55 insertions(+), 67 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index d6632617a..79ddf2e1a 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -1,10 +1,11 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import pslist, pe_symbols @@ -16,6 +17,11 @@ class EtwPatch(interfaces.plugins.PluginInterface): _version = (1, 0, 0) _required_framework_version = (2, 26, 0) + etw_functions = { + "ntdll.dll": ["EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent"], + "advapi32.dll": ["EventWrite"], + } + @classmethod def get_requirements(cls): return [ @@ -38,81 +44,63 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) ] - def _get_dll_vads(self, proc, dll_name): - """Retrieve VADs for a specific DLL in the process.""" - collected_modules = pe_symbols.PESymbols.get_proc_vads_with_file_paths(proc) - return [ - (vad_start, vad_size, proc.add_process_layer()) - for vad_start, vad_size, filepath in collected_modules - if pe_symbols.PESymbols.filename_for_path(filepath) == dll_name - ] - - def _find_symbols(self, dll_name, symbols, proc_layer_name, dll_vads): - """Find symbols for a specific DLL.""" - filter_module = {dll_name: {"names": symbols}} - process_modules = {dll_name: [(proc_layer_name, vad_start, vad_size) for vad_start, vad_size, _ in dll_vads]} - return pe_symbols.PESymbols.find_symbols(self.context, self.config_path, filter_module, process_modules) - - def _get_first_opcode(self, proc_layer_name, function_start): - """Check the first opcode of a function.""" - try: - return self.context.layers[proc_layer_name].read(function_start, 1).hex() - except exceptions.InvalidAddressException: - return None - def _generator(self): - pid_filter = self.config.get('pid', None) + # Get all ETW function addresses before looping through processes + found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( + context=self.context, + config_path=self.config_path, + kernel_module_name=self.config["kernel"], + symbols=self.etw_functions, + ) + + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for proc in pslist.PsList.list_processes( context=self.context, - kernel_module_name=self.config['kernel']): - # Skip processes not in the PID filter - if pid_filter and proc.UniqueProcessId not in pid_filter: - continue - - pid = int(proc.UniqueProcessId) - proc_name = proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors='replace' - ) - + kernel_module_name=self.config['kernel'], + filter_func=filter_func, + ): + try: + proc_id = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException: + vollog.debug(f"Unable to create process layer for PID {proc_id}") continue - dlls_to_check = { - "ntdll.dll": [ - "EtwEventWrite", - "EtwEventWriteFull", - "NtTraceEvent" - ], - "advapi32.dll": [ - "EventWrite" - ] + # Map of opcodes to their instruction names + opcode_map = { + 'c3': 'RET', + 'e9': 'JMP', } - for dll_name, symbols in dlls_to_check.items(): - dll_vads = self._get_dll_vads(proc, dll_name) - if not dll_vads: - continue - - found_symbols, _ = self._find_symbols(dll_name, symbols, proc_layer_name, dll_vads) - if dll_name not in found_symbols: - continue - - for symbol_name, function_start in found_symbols[dll_name]: - opcode = self._get_first_opcode(proc_layer_name, function_start) - if opcode in ('c3', 'e9'): # RET or JMP - yield (0, ( - pid, - proc_name, - dll_name, - symbol_name, - format_hints.Hex(function_start), - opcode - )) + for dll_name, functions in found_symbols.items(): + for func_name, func_addr in functions: + try: + opcode = self.context.layers[proc_layer_name].read( + func_addr, 1 + ).hex() + if opcode in opcode_map: + instruction = opcode_map[opcode] + yield ( + 0, + ( + proc_id, + proc_name, + dll_name, + func_name, + format_hints.Hex(func_addr), + f"{opcode} ({instruction})" + ), + ) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") + continue + except KeyError: + # Layer may no longer exist + vollog.debug(f"Layer {proc_layer_name} no longer exists for process {proc_id}") + continue def run(self): return renderers.TreeGrid( @@ -122,7 +110,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): ("DLL", str), ("Function", str), ("Offset", format_hints.Hex), - ("Opcode", str) + ("Opcode", str), ], - self._generator() + self._generator(), ) From 817bd5ce7fb26984cfb0ab5860d00c24fd3bc3f8 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 15:07:03 +0000 Subject: [PATCH 09/15] additional fixes --- .../framework/plugins/windows/etwpatch.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 79ddf2e1a..1ecedea3f 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -18,8 +18,18 @@ class EtwPatch(interfaces.plugins.PluginInterface): _required_framework_version = (2, 26, 0) etw_functions = { - "ntdll.dll": ["EtwEventWrite", "EtwEventWriteFull", "NtTraceEvent"], - "advapi32.dll": ["EventWrite"], + "ntdll.dll": { + pe_symbols.wanted_names_identifier: [ + "EtwEventWrite", + "EtwEventWriteFull", + "NtTraceEvent" + ], + }, + "advapi32.dll": { + pe_symbols.wanted_names_identifier:[ + "EventWrite" + ], + }, } @classmethod @@ -96,11 +106,6 @@ class EtwPatch(interfaces.plugins.PluginInterface): ) except exceptions.InvalidAddressException: vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") - continue - except KeyError: - # Layer may no longer exist - vollog.debug(f"Layer {proc_layer_name} no longer exists for process {proc_id}") - continue def run(self): return renderers.TreeGrid( From c5a4b34bfa5f1466b61551768ea0ad61becbc200 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 15:18:53 +0000 Subject: [PATCH 10/15] Fixed plugin docs --- volatility3/framework/plugins/windows/etwpatch.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 1ecedea3f..3605dfccb 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -12,7 +12,13 @@ from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) class EtwPatch(interfaces.plugins.PluginInterface): - """Detects ETW patching by examining the first opcode of EtwEventWrite in ntdll.dll.""" + """Identifies ETW (Event Tracing for Windows) patching techniques used by malware to evade detection. + + This plugin examines the first opcode of key ETW functions in ntdll.dll and advapi32.dll + to detect common ETW bypass techniques such as return pointer manipulation (RET) or function + redirection (JMP). Attackers often patch these functions to prevent security tools from + receiving telemetry about process execution, API calls, and other system events. + """ _version = (1, 0, 0) _required_framework_version = (2, 26, 0) From 281a237e03296ff38c7d0353157fa068c32a8c39 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Mon, 28 Apr 2025 16:03:45 +0000 Subject: [PATCH 11/15] black and ruff fixes --- .../framework/plugins/windows/etwpatch.py | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 3605dfccb..e79735213 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -7,16 +7,17 @@ from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, pe_symbols +from volatility3.plugins.windows import pslist, pe_symbols vollog = logging.getLogger(__name__) + class EtwPatch(interfaces.plugins.PluginInterface): """Identifies ETW (Event Tracing for Windows) patching techniques used by malware to evade detection. - - This plugin examines the first opcode of key ETW functions in ntdll.dll and advapi32.dll - to detect common ETW bypass techniques such as return pointer manipulation (RET) or function - redirection (JMP). Attackers often patch these functions to prevent security tools from + + This plugin examines the first opcode of key ETW functions in ntdll.dll and advapi32.dll + to detect common ETW bypass techniques such as return pointer manipulation (RET) or function + redirection (JMP). Attackers often patch these functions to prevent security tools from receiving telemetry about process execution, API calls, and other system events. """ @@ -28,13 +29,11 @@ class EtwPatch(interfaces.plugins.PluginInterface): pe_symbols.wanted_names_identifier: [ "EtwEventWrite", "EtwEventWriteFull", - "NtTraceEvent" + "NtTraceEvent", ], }, "advapi32.dll": { - pe_symbols.wanted_names_identifier:[ - "EventWrite" - ], + pe_symbols.wanted_names_identifier: ["EventWrite"], }, } @@ -42,22 +41,22 @@ class EtwPatch(interfaces.plugins.PluginInterface): def get_requirements(cls): return [ requirements.ModuleRequirement( - name='kernel', - description='Windows kernel', - architectures=["Intel32", "Intel64"] + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], ), requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(3, 0, 0) ), requirements.VersionRequirement( - name="pe_symbols", component=pslist.PsList, version=(3, 0, 0) + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) ), requirements.ListRequirement( - name='pid', - description='Filter on specific process IDs', + name="pid", + description="Filter on specific process IDs", element_type=int, - optional=True - ) + optional=True, + ), ] def _generator(self): @@ -68,15 +67,15 @@ class EtwPatch(interfaces.plugins.PluginInterface): kernel_module_name=self.config["kernel"], symbols=self.etw_functions, ) - + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) for proc in pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config['kernel'], - filter_func=filter_func, - ): - + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ): + try: proc_id = proc.UniqueProcessId proc_name = utility.array_to_string(proc.ImageFileName) @@ -87,16 +86,18 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - 'c3': 'RET', - 'e9': 'JMP', + "c3": "RET", + "e9": "JMP", } for dll_name, functions in found_symbols.items(): for func_name, func_addr in functions: try: - opcode = self.context.layers[proc_layer_name].read( - func_addr, 1 - ).hex() + opcode = ( + self.context.layers[proc_layer_name] + .read(func_addr, 1) + .hex() + ) if opcode in opcode_map: instruction = opcode_map[opcode] yield ( @@ -107,11 +108,13 @@ class EtwPatch(interfaces.plugins.PluginInterface): dll_name, func_name, format_hints.Hex(func_addr), - f"{opcode} ({instruction})" + f"{opcode} ({instruction})", ), ) except exceptions.InvalidAddressException: - vollog.debug(f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}") + vollog.debug( + f"Invalid address when reading function {func_name} at {func_addr:#x} in process {proc_id}" + ) def run(self): return renderers.TreeGrid( From 39b35a3a8070854fa3a9238a1190e5978288df3f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:35 +0300 Subject: [PATCH 12/15] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index e79735213..7aee06e43 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -86,8 +86,8 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - "c3": "RET", - "e9": "JMP", + 0xc3: "RET", + 0xe9: "JMP", } for dll_name, functions in found_symbols.items(): From cc1df7b4617be694ec22aac3ca49d7c5175058d1 Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:47 +0300 Subject: [PATCH 13/15] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 7aee06e43..30e8b2947 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -95,8 +95,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): try: opcode = ( self.context.layers[proc_layer_name] - .read(func_addr, 1) - .hex() + .read(func_addr, 1)[0] ) if opcode in opcode_map: instruction = opcode_map[opcode] From 4738efefa3cdc4a4405df57551ffcc653bff9b9e Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 13:11:58 +0300 Subject: [PATCH 14/15] Update volatility3/framework/plugins/windows/etwpatch.py Co-authored-by: ikelos --- volatility3/framework/plugins/windows/etwpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index 30e8b2947..a21cab030 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -107,7 +107,7 @@ class EtwPatch(interfaces.plugins.PluginInterface): dll_name, func_name, format_hints.Hex(func_addr), - f"{opcode} ({instruction})", + f"{opcode:02x} ({instruction})", ), ) except exceptions.InvalidAddressException: From 19b094acf686f3a0a093a1ac6b62b2b0d1391c5f Mon Sep 17 00:00:00 2001 From: Elad Levi <99.elad.levi@gmail.com> Date: Tue, 29 Apr 2025 14:20:42 +0300 Subject: [PATCH 15/15] Fix lint errors --- volatility3/framework/plugins/windows/etwpatch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/windows/etwpatch.py b/volatility3/framework/plugins/windows/etwpatch.py index a21cab030..14fbacc28 100644 --- a/volatility3/framework/plugins/windows/etwpatch.py +++ b/volatility3/framework/plugins/windows/etwpatch.py @@ -86,17 +86,16 @@ class EtwPatch(interfaces.plugins.PluginInterface): # Map of opcodes to their instruction names opcode_map = { - 0xc3: "RET", - 0xe9: "JMP", + 0xC3: "RET", + 0xE9: "JMP", } for dll_name, functions in found_symbols.items(): for func_name, func_addr in functions: try: - opcode = ( - self.context.layers[proc_layer_name] - .read(func_addr, 1)[0] - ) + opcode = self.context.layers[proc_layer_name].read( + func_addr, 1 + )[0] if opcode in opcode_map: instruction = opcode_map[opcode] yield (