From 7e77ee0e24cf5d95845a4f3db6a8c6854a9f6a36 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 10:24:04 +0200 Subject: [PATCH 01/11] Adding dump dirty page feature --- .../plugins/linux/malware/malfind.py | 22 ++++++++++++-- .../symbols/linux/extensions/__init__.py | 29 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index c7e141c02..a8104fbaa 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -37,6 +37,16 @@ class Malfind(interfaces.plugins.PluginInterface): element_type=int, optional=True, ), + requirements.IntRequirement( + name="dumpsize", + description="Dump X bytes of each malicious region found", + optional=True, + ), + requirements.BooleanRequirement( + name="dumppage", + description="Dump dirty page content (for each dirty page)", + optional=True, + ), ] def _list_injections( @@ -50,6 +60,8 @@ class Malfind(interfaces.plugins.PluginInterface): return None proc_layer = self.context.layers[proc_layer_name] + dumpsize = self.config.get("dumpsize") if self.config.get("dumpsize") is not None else 64 + dumppage = self.config.get("dumppage") or False for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) @@ -57,8 +69,14 @@ class Malfind(interfaces.plugins.PluginInterface): 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_name != "[vdso]": - data = proc_layer.read(vma.vm_start, 64, pad=True) - yield vma, vma_name, data + malicious_pages = vma.get_malicious_pages(proc_layer) + if dumppage: + for page_addr in malicious_pages: + data = proc_layer.read(page_addr, dumpsize, pad=True) + yield vma, vma_name+f", page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + else: + data = proc_layer.read(vma.vm_start,dumpsize,pad=True) + yield vma, vma_name, data def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3b9a73e7c..27cb44989 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,6 +1273,32 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None + def get_malicious_pages(self,proclayer=None): + malicious_pages = [] + + flags_str = self.get_protection() + + if flags_str == "rwx": + ret = True + elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: + ret = True + elif proclayer and "x" in flags_str: + for i in range(self.vm_start, self.vm_end, proclayer.page_size): + try: + if proclayer.is_dirty(i): + vollog.debug( + f"Found malicious (dirty+exec) page at {hex(i)} !" + ) + malicious_pages.append(i) + 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 + break + return malicious_pages + # used by malfind def is_suspicious(self, proclayer=None): ret = False @@ -1288,7 +1314,7 @@ class vm_area_struct(objects.StructType): try: if proclayer.is_dirty(i): vollog.warning( - f"Found malicious (dirty+exec) page at {hex(i)} !" + f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !" ) # We do not attempt to find other dirty+exec pages once we have found one ret = True @@ -2733,6 +2759,7 @@ class page(objects.StructType): for name, value in self.pageflags_enum.items(): if self.flags & (1 << value) != 0: flags.append(name) + print(name,value) return flags From 96fe242c9d0cbe35e0c653c18c2700e4bed441d2 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 11:00:18 +0200 Subject: [PATCH 02/11] Minor cleanup + comments --- .../plugins/linux/malware/malfind.py | 33 +++++++++++++------ .../symbols/linux/extensions/__init__.py | 12 +++---- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index a8104fbaa..39d885f68 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -38,13 +38,13 @@ class Malfind(interfaces.plugins.PluginInterface): optional=True, ), requirements.IntRequirement( - name="dumpsize", - description="Dump X bytes of each malicious region found", + name="dump_size", + description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, ), requirements.BooleanRequirement( - name="dumppage", - description="Dump dirty page content (for each dirty page)", + name="dump_page", + description="Dump each dirty page and content - Default off", optional=True, ), ] @@ -60,22 +60,35 @@ class Malfind(interfaces.plugins.PluginInterface): return None proc_layer = self.context.layers[proc_layer_name] - dumpsize = self.config.get("dumpsize") if self.config.get("dumpsize") is not None else 64 - dumppage = self.config.get("dumppage") or False + + # Allowing a dump_size of 0 (no dump) + dump_size = self.config.get("dump_size") if self.config.get("dump_size") is not None else 64 + + # Dumping page defaults to off, as in case a whole r-xp region is dirty + # this would likely dump 1000's of pages which might not always be wise nor necessary + + dump_page = self.config.get("dump_page") or False for vma in task.mm.get_vma_iter(): 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 is_suspicious returns true, this means at least one page + # in the region is dirty. If dump_page is true, then we dump + # all dirty pages + if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": malicious_pages = vma.get_malicious_pages(proc_layer) - if dumppage: + if dump_page: + # Dumping each dirty page for page_addr in malicious_pages: - data = proc_layer.read(page_addr, dumpsize, pad=True) - yield vma, vma_name+f", page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + data = proc_layer.read(page_addr, dump_size, pad=True) + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data else: - data = proc_layer.read(vma.vm_start,dumpsize,pad=True) + # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) + data = proc_layer.read(vma.vm_start,dump_size,pad=True) yield vma, vma_name, data def _generator(self, tasks): diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 27cb44989..236d28bb1 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1274,19 +1274,17 @@ class vm_area_struct(objects.StructType): return None def get_malicious_pages(self,proclayer=None): + """ + This function will return a list of all malicious pages inside a given dirty region + """ malicious_pages = [] - flags_str = self.get_protection() - if flags_str == "rwx": - ret = True - elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: - ret = True - elif proclayer and "x" in flags_str: + if proclayer and "r-x" in flags_str and self.vm_file.dereference().vol.offset !=0: for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.debug( + vollog.warning( f"Found malicious (dirty+exec) page at {hex(i)} !" ) malicious_pages.append(i) From ba9e13698d470e6c3502b12fec45f02dfc0e133f Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 11:33:44 +0200 Subject: [PATCH 03/11] Minor changes 2 --- volatility3/framework/plugins/linux/malware/malfind.py | 8 ++++---- .../framework/symbols/linux/extensions/__init__.py | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 39d885f68..577c6c1e8 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -38,12 +38,12 @@ class Malfind(interfaces.plugins.PluginInterface): optional=True, ), requirements.IntRequirement( - name="dump_size", + name="dump-size", description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, ), requirements.BooleanRequirement( - name="dump_page", + name="dump-page", description="Dump each dirty page and content - Default off", optional=True, ), @@ -62,12 +62,12 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] # Allowing a dump_size of 0 (no dump) - dump_size = self.config.get("dump_size") if self.config.get("dump_size") is not None else 64 + dump_size = self.config.get("dump-size") if self.config.get("dump-size") is not None else 64 # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary - dump_page = self.config.get("dump_page") or False + dump_page = self.config.get("dump-page") or False for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 236d28bb1..94dfb0576 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1284,7 +1284,7 @@ class vm_area_struct(objects.StructType): for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.warning( + vollog.debug( f"Found malicious (dirty+exec) page at {hex(i)} !" ) malicious_pages.append(i) @@ -2757,7 +2757,6 @@ class page(objects.StructType): for name, value in self.pageflags_enum.items(): if self.flags & (1 << value) != 0: flags.append(name) - print(name,value) return flags From 53b3bd7d47ca855a770be8be0119a6a86439a49b Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 12:51:59 +0200 Subject: [PATCH 04/11] black --- .../framework/plugins/linux/malware/malfind.py | 8 ++++++-- .../framework/symbols/linux/extensions/__init__.py | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 577c6c1e8..4aaaf9bf8 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -62,7 +62,11 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] # Allowing a dump_size of 0 (no dump) - dump_size = self.config.get("dump-size") if self.config.get("dump-size") is not None else 64 + dump_size = ( + self.config.get("dump-size") + if self.config.get("dump-size") is not None + else 64 + ) # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary @@ -88,7 +92,7 @@ class Malfind(interfaces.plugins.PluginInterface): yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data else: # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) - data = proc_layer.read(vma.vm_start,dump_size,pad=True) + data = proc_layer.read(vma.vm_start, dump_size, pad=True) yield vma, vma_name, data def _generator(self, tasks): diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 94dfb0576..99b2c989c 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,20 +1273,22 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None - def get_malicious_pages(self,proclayer=None): + def get_malicious_pages(self, proclayer=None): """ This function will return a list of all malicious pages inside a given dirty region """ malicious_pages = [] flags_str = self.get_protection() - if proclayer and "r-x" in flags_str and self.vm_file.dereference().vol.offset !=0: + if ( + proclayer + and "r-x" in flags_str + and self.vm_file.dereference().vol.offset != 0 + ): for i in range(self.vm_start, self.vm_end, proclayer.page_size): try: if proclayer.is_dirty(i): - vollog.debug( - f"Found malicious (dirty+exec) page at {hex(i)} !" - ) + vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !") malicious_pages.append(i) except ( exceptions.PagedInvalidAddressException, From e13b8f9bd0633496dda13df1446fcafa6c9207e7 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Tue, 17 Jun 2025 16:12:22 +0200 Subject: [PATCH 05/11] Fixing memory wrong memory offset in hex dump --- .../framework/plugins/linux/malware/malfind.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 4aaaf9bf8..98e9c4695 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -85,15 +85,17 @@ class Malfind(interfaces.plugins.PluginInterface): if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": malicious_pages = vma.get_malicious_pages(proc_layer) + offset = 0 if dump_page: # Dumping each dirty page for page_addr in malicious_pages: + offset = page_addr - vma.vm_start data = proc_layer.read(page_addr, dump_size, pad=True) - yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {page_addr-vma.vm_start:#x}", data + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset else: # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) data = proc_layer.read(vma.vm_start, dump_size, pad=True) - yield vma, vma_name, data + yield vma, vma_name, data, offset def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel @@ -105,13 +107,15 @@ class Malfind(interfaces.plugins.PluginInterface): for task in tasks: process_name = utility.array_to_string(task.comm) - for vma, vma_name, data in self._list_injections(task): + for vma, vma_name, data, offset in self._list_injections(task): if is_32bit_arch: architecture = "intel" else: architecture = "intel64" - disasm = renderers.Disassembly(data, vma.vm_start, architecture) + disasm = renderers.Disassembly( + data, vma.vm_start + offset, architecture + ) yield ( 0, From e4aa9af834bedc70f7e27d2a8599444b2a7af4ab Mon Sep 17 00:00:00 2001 From: tvanegro Date: Thu, 3 Jul 2025 13:31:03 +0200 Subject: [PATCH 06/11] version bump --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 98e9c4695..b9a03c616 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -18,7 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 3) + _version = (1, 0, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: From e7185b298f11345e8bbd9e41850e43813627ca03 Mon Sep 17 00:00:00 2001 From: tvanegro Date: Wed, 23 Jul 2025 11:06:20 +0200 Subject: [PATCH 07/11] PR comments --- .../framework/plugins/linux/malware/malfind.py | 7 +------ .../symbols/linux/extensions/__init__.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index b9a03c616..306bdc3b0 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -61,12 +61,7 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] - # Allowing a dump_size of 0 (no dump) - dump_size = ( - self.config.get("dump-size") - if self.config.get("dump-size") is not None - else 64 - ) + dump_size = self.config.get("dump-size", None) or 64 # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 99b2c989c..e48035102 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1273,10 +1273,20 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None - def get_malicious_pages(self, proclayer=None): - """ - This function will return a list of all malicious pages inside a given dirty region + def get_malicious_pages(self, proclayer) -> List[int]: + """Identifies and returns a list of potentially malicious memory pages. + + A page is considered malicious if it is: + - Executable (protection flags match 'r-x') + - Dirty (modified since process start, according to proclayer.is_dirty()) + + Args: + proclayer: The process's memory layer + + Returns: + List[int]: A list of virtual addresses for pages flagged as potentially malicious. """ + malicious_pages = [] flags_str = self.get_protection() From 438acddab635553b9820e8bdc6815a59ed99b222 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:26 +0100 Subject: [PATCH 08/11] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 306bdc3b0..721e1953d 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -41,6 +41,7 @@ class Malfind(interfaces.plugins.PluginInterface): name="dump-size", description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", optional=True, + default=64, ), requirements.BooleanRequirement( name="dump-page", From 16412537c0065ba284d83e5dc41d6b1d01a37a43 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:33 +0100 Subject: [PATCH 09/11] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 1 + 1 file changed, 1 insertion(+) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 721e1953d..09b64768f 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -47,6 +47,7 @@ class Malfind(interfaces.plugins.PluginInterface): name="dump-page", description="Dump each dirty page and content - Default off", optional=True, + default=False, ), ] From d41afc444e7590b83dd9f14702f76ed094371856 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:40 +0100 Subject: [PATCH 10/11] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 09b64768f..10fa71d36 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -63,7 +63,7 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer = self.context.layers[proc_layer_name] - dump_size = self.config.get("dump-size", None) or 64 + dump_size = self.config["dump-size"] # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary From f72b8ee21d72751907a16ea66cba692fe5c78790 Mon Sep 17 00:00:00 2001 From: ikelos Date: Sun, 28 Sep 2025 22:11:48 +0100 Subject: [PATCH 11/11] Update volatility3/framework/plugins/linux/malware/malfind.py --- volatility3/framework/plugins/linux/malware/malfind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 10fa71d36..cbd9f87c1 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -68,7 +68,7 @@ class Malfind(interfaces.plugins.PluginInterface): # Dumping page defaults to off, as in case a whole r-xp region is dirty # this would likely dump 1000's of pages which might not always be wise nor necessary - dump_page = self.config.get("dump-page") or False + dump_page = self.config["dump-page"] for vma in task.mm.get_vma_iter(): vma_name = vma.get_name(self.context, task)