diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index 533a4e217..fba68a289 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -2,23 +2,31 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import List, Tuple, Optional import logging -from volatility3.framework import interfaces -from volatility3.framework import renderers, symbols +from typing import Iterable, List, Tuple, Optional +from enum import IntEnum +from volatility3.framework import exceptions, interfaces, renderers, symbols from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints -from volatility3.plugins.linux import pslist +from volatility3.plugins.linux import pslist, proc vollog = logging.getLogger(__name__) +class MaliciousFlags(IntEnum): + RWX = 0 + RX = 1 + X_DIRTY = 2 + + class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 4) + _version = (1, 1, 0) + + MAX_DUMPSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -31,6 +39,9 @@ class Malfind(interfaces.plugins.PluginInterface): requirements.VersionRequirement( name="pslist", component=pslist.PsList, version=(4, 0, 0) ), + requirements.VersionRequirement( + name="proc", component=proc.Maps, version=(1, 0, 3) + ), requirements.ListRequirement( name="pid", description="Filter on specific process IDs", @@ -38,66 +49,130 @@ class Malfind(interfaces.plugins.PluginInterface): optional=True, ), requirements.IntRequirement( - name="dump-size", - description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", + name="hexdump-size", + description="Amount of bytes to show for each region/page found - Default 64 bytes", optional=True, default=64, ), requirements.BooleanRequirement( - name="dump-page", - description="Dump each dirty page and content - Default off", + name="show-all-dirty-pages", + description="Show all dirty pages in a VMA if at least one dirty page is found - Default off", optional=True, default=False, ), + requirements.BooleanRequirement( + name="dump-regions", + description="Dump each suspicious memory region in output. All dirty pages will be dumped if --show-all-dirty-pages is enabled.", + optional=True, + default=False, + ), + requirements.IntRequirement( + name="dump-maxsize", + description="Maximum size for dumped memory regions " + "(all the bigger regions will be ignored) - Default 1 GB", + default=cls.MAX_DUMPSIZE_DEFAULT, + optional=True, + ), ] + def _get_dirty_pages(self, proc_layer, vma) -> Iterable[Tuple[int, int]]: + """Get dirty pages inside the specified VMA. + + Yields: + page address and page size + """ + page_addr = vma.vm_start + while page_addr < vma.vm_end: + try: + # We don't want to use the layer's page size by default to handle + # large pages (PUD, PMD...) correctly. + _, page_size, _ = proc_layer._translate(page_addr) + if proc_layer.is_dirty(page_addr): + yield page_addr, page_size + except ( + AttributeError, + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ): + page_size = proc_layer.page_size + + page_addr += page_size + + def _is_suspicious(self, proc_layer, vma) -> Optional[Tuple[int, MaliciousFlags]]: + """Determine if a VMA is suspicious based on any of the following criterias: + - RWX + - RX + - X + DIRTY + + Returns: + (suspicious page address, suspicious page size, malicious flag) or None + """ + flags_str = vma.get_protection() + + if flags_str == "rwx": + return vma.vm_start, vma.vm_end - vma.vm_start, MaliciousFlags.RWX + elif flags_str == "r-x" and vma.vm_file.dereference().vol.offset == 0: + return vma.vm_start, vma.vm_end - vma.vm_start, MaliciousFlags.RX + elif "x" in flags_str: + for page_addr, page_size in self._get_dirty_pages(proc_layer, vma): + vollog.warning( + f"Found dirty page at {page_addr:#x} inside executable region {vma.vm_start:#x}-{vma.vm_end:#x}!" + ) + # We do not attempt to find other dirty+exec pages once we have found one + return page_addr, page_size, MaliciousFlags.X_DIRTY + + return None + def _list_injections( self, task - ) -> Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes]: + ) -> Iterable[ + Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes, int, int] + ]: """Generate memory regions for a process that may contain injected code.""" - proc_layer_name = task.add_process_layer() if not proc_layer_name: - return None + return proc_layer = self.context.layers[proc_layer_name] - - dump_size = self.config["dump-size"] - + data_size = self.config["hexdump-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 - - dump_page = self.config["dump-page"] - 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 vma_name == "[vdso]": + continue - # 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 + suspicious_result = self._is_suspicious(proc_layer, vma) + if suspicious_result is None: + continue - 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: {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, offset + region_start, region_size, suspicious_flag = suspicious_result + # If _is_suspicious returns MaliciousFlags.X_DIRTY, this means at least one page + # in the region is dirty. If --show-all-dirty-pages is set, then we show + # all the dirty pages. + if ( + suspicious_flag == MaliciousFlags.X_DIRTY + and self.config["show-all-dirty-pages"] + ): + # Dump each dirty page + for dirty_page_addr, dirty_page_size in self._get_dirty_pages( + proc_layer, vma + ): + name = f"{vma_name}, dirty page address: {dirty_page_addr:#x}" + data = proc_layer.read(dirty_page_addr, data_size, pad=True) + yield vma, name, data, dirty_page_addr, dirty_page_size + continue + + name = vma_name + if suspicious_flag == MaliciousFlags.X_DIRTY: + name = f"{vma_name}, dirty page address: {region_start:#x}" + + data = proc_layer.read(vma.vm_start, data_size, pad=True) + yield vma, name, data, region_start, region_size def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel @@ -109,15 +184,30 @@ class Malfind(interfaces.plugins.PluginInterface): for task in tasks: process_name = utility.array_to_string(task.comm) - for vma, vma_name, data, offset in self._list_injections(task): + for vma, vma_name, data, region_start, region_size in self._list_injections( + task + ): if is_32bit_arch: architecture = "intel" else: architecture = "intel64" - disasm = renderers.Disassembly( - data, vma.vm_start + offset, architecture - ) + disasm = renderers.Disassembly(data, region_start, architecture) + + file_output = "Disabled" + if self.config["dump-regions"]: + file_handle = proc.Maps.vma_dump( + self.context, + task, + region_start, + region_start + region_size, + self.open, + self.config["dump-maxsize"], + ) + + if file_handle: + file_handle.close() + file_output = file_handle.preferred_filename yield ( 0, @@ -130,6 +220,7 @@ class Malfind(interfaces.plugins.PluginInterface): vma.get_protection(), format_hints.HexBytes(data), disasm, + file_output, ), ) @@ -146,6 +237,7 @@ class Malfind(interfaces.plugins.PluginInterface): ("Protection", str), ("Hexdump", format_hints.HexBytes), ("Disasm", renderers.Disassembly), + ("File output", str), ], self._generator( pslist.PsList.list_tasks( diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 0a6e05951..836810a6d 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1360,7 +1360,7 @@ class vm_area_struct(objects.StructType): break return malicious_pages - # used by malfind + # previously used by malfind def is_suspicious(self, proclayer=None): ret = False