From 7c82da4f5044a4bf35028c01924e659ccac5e828 Mon Sep 17 00:00:00 2001 From: xabrouck Date: Mon, 14 Aug 2023 11:53:14 +0200 Subject: [PATCH 1/2] check IoC of dirty bit in PTEs from executable VMAs. this can for example detect code injected using ptrace(). this can also detect injected code that was reset to the original code (malware uninstalled before memory dump happened). --- volatility3/framework/layers/intel.py | 9 +++++++++ volatility3/framework/plugins/linux/malfind.py | 2 +- .../framework/symbols/linux/extensions/__init__.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 478eb168f0..e2d89540d0 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -110,6 +110,11 @@ def _mask(value: int, high_bit: int, low_bit: int) -> int: def _page_is_valid(entry: int) -> bool: """Returns whether a particular page is valid based on its entry.""" return bool(entry & 1) + + @staticmethod + def _page_is_dirty(entry: int) -> bool: + """Returns whether a particular page is dirty based on its entry.""" + return bool(entry & (1<<6)) def canonicalize(self, addr: int) -> int: """Canonicalizes an address by performing an appropiate sign extension on the higher addresses""" @@ -259,6 +264,10 @@ def is_valid(self, offset: int, length: int = 1) -> bool: except exceptions.InvalidAddressException: return False + def is_dirty(self, offset: int) -> bool: + """Returns whether the page at offset is marked dirty""" + return self._page_is_dirty(self._translate_entry(offset)[0]) + def mapping( self, offset: int, length: int, ignore_errors: bool = False ) -> Iterable[Tuple[int, int, int, int, str]]: diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index 1fd005de86..332d5ede11 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -47,7 +47,7 @@ def _list_injections(self, task): proc_layer = self.context.layers[proc_layer_name] for vma in task.mm.get_vma_iter(): - if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]": + if vma.is_suspicious(proc_layer) and vma.get_name(self.context, task) != "[vdso]": data = proc_layer.read(vma.vm_start, 64, pad=True) yield vma, data diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 527785a690..d2e6197ef2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -578,7 +578,7 @@ def get_name(self, context, task): return fname # used by malfind - def is_suspicious(self): + def is_suspicious(self, proclayer): ret = False flags_str = self.get_protection() @@ -587,6 +587,15 @@ def is_suspicious(self): ret = True elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: ret = True + elif "x" in flags_str: + for i in range(self.vm_start,self.vm_end,constants.linux.PAGE_SHIFT): + try: + if proclayer.is_dirty(i): + vollog.warning(f"Found malicious (dirty+exec) page at {hex(i)} !") + ret = True + break + except (exceptions.PagedInvalidAddressException, exceptions.InvalidAddressException): + pass return ret From 6f7f1284adbbcfacd759ec0d931859e0789cfc8a Mon Sep 17 00:00:00 2001 From: xabrouck Date: Fri, 18 Aug 2023 11:37:04 +0200 Subject: [PATCH 2/2] Fix bug with PAGE_SHIFT that wasn't shifted, also greatly increases performance Use black Better logging --- volatility3/framework/layers/intel.py | 6 +++--- .../framework/plugins/linux/malfind.py | 13 ++++++++++-- .../symbols/linux/extensions/__init__.py | 21 +++++++++++++------ 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index e2d89540d0..046203fa68 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -110,11 +110,11 @@ def _mask(value: int, high_bit: int, low_bit: int) -> int: def _page_is_valid(entry: int) -> bool: """Returns whether a particular page is valid based on its entry.""" return bool(entry & 1) - + @staticmethod def _page_is_dirty(entry: int) -> bool: """Returns whether a particular page is dirty based on its entry.""" - return bool(entry & (1<<6)) + return bool(entry & (1 << 6)) def canonicalize(self, addr: int) -> int: """Canonicalizes an address by performing an appropiate sign extension on the higher addresses""" @@ -267,7 +267,7 @@ def is_valid(self, offset: int, length: int = 1) -> bool: def is_dirty(self, offset: int) -> bool: """Returns whether the page at offset is marked dirty""" return self._page_is_dirty(self._translate_entry(offset)[0]) - + def mapping( self, offset: int, length: int, ignore_errors: bool = False ) -> Iterable[Tuple[int, int, int, int, str]]: diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index 332d5ede11..8a21afc03f 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -3,7 +3,7 @@ # from typing import List - +import logging from volatility3.framework import constants, interfaces from volatility3.framework import renderers from volatility3.framework.configuration import requirements @@ -11,6 +11,8 @@ from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" @@ -47,7 +49,14 @@ def _list_injections(self, task): proc_layer = self.context.layers[proc_layer_name] for vma in task.mm.get_vma_iter(): - if vma.is_suspicious(proc_layer) and vma.get_name(self.context, task) != "[vdso]": + vma_name = vma.get_name(self.context, task) + vollog.debug( + f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" + ) + if ( + vma.is_suspicious(proc_layer) + and vma.get_name(self.context, task) != "[vdso]" + ): data = proc_layer.read(vma.vm_start, 64, pad=True) yield vma, data diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d2e6197ef2..616e54e703 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -578,7 +578,7 @@ def get_name(self, context, task): return fname # used by malfind - def is_suspicious(self, proclayer): + def is_suspicious(self, proclayer=None): ret = False flags_str = self.get_protection() @@ -587,15 +587,24 @@ def is_suspicious(self, proclayer): ret = True elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: ret = True - elif "x" in flags_str: - for i in range(self.vm_start,self.vm_end,constants.linux.PAGE_SHIFT): + elif proclayer and "x" in flags_str: + for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT): try: if proclayer.is_dirty(i): - vollog.warning(f"Found malicious (dirty+exec) page at {hex(i)} !") + vollog.warning( + f"Found malicious (dirty+exec) page at {hex(i)} !" + ) + # We do not attempt to find other dirty+exec pages once we have found one ret = True break - except (exceptions.PagedInvalidAddressException, exceptions.InvalidAddressException): - pass + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug(f"Unable to translate address {hex(i)} : {excp}") + # Abort as it is likely that other addresses in the same range will also fail + ret = False + break return ret