diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml deleted file mode 100644 index 3df690543..000000000 --- a/.github/workflows/black.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Black python formatter - -on: [push, pull_request] - -jobs: - lint: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: psf/black@stable - with: - options: "--check --diff --verbose" - src: "./volatility3" - # FIXME: Remove when Volatility3 minimum Python version is >3.8 - version: "24.8.0" diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 9b9cbed4d..fbc07cae9 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: host: [ ubuntu-latest, windows-latest ] - python-version: [ "3.8", "3.9", "3.10", "3.11" ] + python-version: [ "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml index 98a05a616..e2381dab2 100644 --- a/.github/workflows/ruff.yaml +++ b/.github/workflows/ruff.yaml @@ -4,7 +4,7 @@ name: Ruff on: [push, pull_request] jobs: - lint: + lint-and-format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -13,3 +13,5 @@ jobs: with: args: check src: "." + + - run: "ruff format --check --diff" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 266e8bcc3..104b1f9d8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,19 +26,19 @@ jobs: run: | mkdir test_images cd test_images - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux-sample-1.bin.gz" gunzip linux-sample-1.bin.gz - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-xp-laptop-2005-06-25.img.gz" gunzip win-xp-laptop-2005-06-25.img.gz - curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-10_19041-2025_03.dmp.gz" + curl -sLO "https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/win-10_19041-2025_03.dmp.gz" gunzip win-10_19041-2025_03.dmp.gz cd - - name: Download and Extract symbols run: | cd ./volatility3/symbols - curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip - curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/symbols_win-10_19041-2025_03.zip + curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip + curl -sLO https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/symbols_win-10_19041-2025_03.zip unzip linux.zip unzip symbols_win-10_19041-2025_03.zip cd - diff --git a/CODING_STYLE.md b/CODING_STYLE.md index a4e248ffe..ed69003c6 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -5,7 +5,7 @@ The coding standards for volatility are mostly by our linter and our code format All code submissions will be vetted automatically through tests from both and the submission will not be accepted if either of these fail. Code Linter: Ruff -Code Formatter: Black +Code Formatter: Ruff In addition, there are some coding practices that we employ to prevent specific failure cases and ensure consistency across the codebase. These are documented below along with the rationale for the decision. diff --git a/README.md b/README.md index 8c5332a90..e38975f91 100644 --- a/README.md +++ b/README.md @@ -65,19 +65,19 @@ pip install -e ".[dev]" Symbol table packs for the various operating systems are available for download at: - +[windows.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/windows.zip) - +[mac.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/mac.zip) - +[linux.zip](https://github.com/volatilityfoundation/volatility3-test-data/releases/download/v0.0.1/linux.zip) The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: - +[SHA256SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA256SUMS) - +[SHA1SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/SHA1SUMS) - +[MD5SUMS](https://raw.githubusercontent.com/volatilityfoundation/volatility3-test-data/refs/tags/v0.0.1/symbols/MD5SUMS) Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file). diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e20268566..e81277087 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -618,3 +618,37 @@ class TestLinuxPscallstack: rb"1\s+init\s+39\s+0x88001f999a40.*?0xffff81109039\s+do_select\s+T\s+kernel", out, ) + + +class TestLinuxSockscan: + def test_linux_sockscan(self, volatility, python): + # designed for linux-sample-1.dmp SHA1:1C3A4627EDCA94A7ADE3414592BEF0E62D7D3BB6 + image = LinuxSamples.LINUX_GENERIC.value.path + rc, out, err = test_volatility.runvol_plugin( + "linux.sockscan.Sockscan", image, volatility, python + ) + + # ensure that multiple unix paths for sockets have been found + assert ( + len( + re.findall( + rb"(/[ -~]+?){1,8}", + out, + ) + ) + >= 10 + ) + + # ensure that multiple IPv4 addresses have been found + assert ( + len( + re.findall( + rb"((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}", + out, + ) + ) + >= 10 + ) + + assert out.count(b"\n") >= 50 + assert rc == 0 diff --git a/vol.py b/vol.py index c49d5985d..8c9701bfd 100755 --- a/vol.py +++ b/vol.py @@ -5,7 +5,12 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import sys + import volatility3.cli if __name__ == "__main__": + # Ensure stdout/stderr use UTF-8 to avoid output encoding errors on Windows systems + sys.stderr.reconfigure(encoding="utf-8") + sys.stdout.reconfigure(encoding="utf-8") volatility3.cli.main() diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index d55201371..d400067af 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -10,8 +10,8 @@ import string import sys from functools import wraps from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union -from volatility3.cli import text_filter +from volatility3.cli import text_filter from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.renderers import format_hints @@ -464,9 +464,9 @@ class PrettyTextRenderer(CLIRenderer): accumulator.append((node.path_depth, line)) return accumulator - final_output: List[Tuple[int, Dict[interfaces.renderers.Column, list[str]]]] = ( - [] - ) + final_output: List[ + Tuple[int, Dict[interfaces.renderers.Column, list[str]]] + ] = [] if not grid.populated: grid.populate(visitor, final_output) else: @@ -598,7 +598,8 @@ class JsonRenderer(CLIRenderer): if self.filter and self.filter.filter(line): return accumulator - if node.parent: + # Only add if the parent hasn't been filtered out + if node.parent and node.parent.path in acc_map: acc_map[node.parent.path]["__children"].append(node_dict) else: final_tree.append(node_dict) diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index 95703aaf7..511b95731 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -165,9 +165,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface): "init_mm" ).address and init_task.tasks.next.cast( "long unsigned int" - ) == init_task.tasks.prev.cast( - "long unsigned int" - ): + ) == init_task.tasks.prev.cast("long unsigned int"): # The idle task steals `mm` from previously running task, i.e., # `init_mm` is only used as long as no CPU has ever been idle. # This catches cases where we found a fragment of the diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 7d56b01b3..e57bf6974 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -153,8 +153,7 @@ class DtbSelfRefPae(DtbSelfReferential): # Mask off the page bits of top level page map page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4 page_table = data[ - top_pae_page - - data_offset : top_pae_page + top_pae_page - data_offset : top_pae_page - data_offset + (4 * self.ptr_size) ] @@ -200,7 +199,7 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): ( "Detecting Self-referential pointer for recent windows", [DtbSelfRef64bit()], - [(0x150000, 0x150000), (0x650000, 0xA0000)], + [(0x150000, 0x150000), (0x550000, 0x1A0000), (0x900000, 0x100000)], ), ( "Older windows fixed location self-referential pointers", @@ -287,28 +286,53 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): """Key used to sort by tests""" return tests.index(x[0]), x[1] - def get_max_pointer(page_table, test, ptr_size: int): - """Determines a pointer from a page_table""" - max_ptr = 0 + def get_valid_page_table_pointers(page_table, ptr_size: int): + """Yields valid pointers from a page table""" for index in range(0, len(page_table), ptr_size): pointer = struct.unpack( test.ptr_struct, page_table[index : index + ptr_size] )[0] # Make sure the pointer is valid, ignore large pages which would require more calculation if pointer & 0x1 and not pointer & 0x80: - max_ptr = max( - max_ptr, - (pointer ^ (pointer & 0xFFF)) - % test.layer_type.maximum_address, - ) + yield pointer + + def get_max_pointer(page_table, test, ptr_size: int): + """Determines a pointer from a page_table""" + max_ptr = 0 + for pointer in get_valid_page_table_pointers(page_table, ptr_size): + max_ptr = max( + max_ptr, + (pointer ^ (pointer & 0xFFF)) % test.layer_type.maximum_address, + ) return max_ptr + def page_table_is_dummy(page_table, ptr_size: int): + """Verify that a page table has at least 12 valid pointers""" + valid_pointers = 0 + for _ in get_valid_page_table_pointers(page_table, ptr_size): + valid_pointers += 1 + # 10 is an arbitrary constant + if valid_pointers >= 10: + # Do not consume the entire generator to enhance performance + return False + vollog.debug(f"Found {valid_pointers} valid pointers") + return True + hits = sorted(list(hits), key=sort_by_tests) + vollog.debug(f"WindowsIntelStacker hits: {hits}") + for test, page_map_offset in hits: # Turn the page tables into integers and find the largest one page_table = base_layer.read(page_map_offset, 0x1000) ptr_size = struct.calcsize(test.ptr_struct) + # Modern windows can have a dummy page table with only about 2 entries, so sanity check + if page_table_is_dummy(page_table, ptr_size): + vollog.debug( + f"DTB {page_map_offset:x} contains less than 12 valid pointers, ignoring" + ) + continue + max_pointer = get_max_pointer(page_table, test, ptr_size) if max_pointer <= base_layer.maximum_address: diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 689ef122b..684ba7cfd 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -25,6 +25,18 @@ from volatility3.framework.constants._version import ( REQUIRED_PYTHON_VERSION = (3, 8, 0) +CACHE_PATH = os.path.join( + os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"), + "volatility3", +) +"""Default path to store cached data""" + +if sys.platform == "win32": + CACHE_PATH = os.path.realpath( + os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3") + ) +os.makedirs(CACHE_PATH, exist_ok=True) + PLUGINS_PATH = [ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")), os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")), @@ -34,6 +46,9 @@ PLUGINS_PATH = [ SYMBOL_BASEPATHS = [ os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "symbols")), os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "symbols")), + os.path.abspath( + os.path.join(CACHE_PATH, "symbols") + ), # User cache fallback for automatically downloaded temporary symbols ] """Default list of paths to load symbols from (volatility3/symbols and volatility3/framework/symbols)""" @@ -71,21 +86,9 @@ LOGLEVEL_VVVV = 6 """Logging level for four levels of detail: -vvvvvv""" -CACHE_PATH = os.path.join( - os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"), - "volatility3", -) -"""Default path to store cached data""" - SQLITE_CACHE_PERIOD = "-3 days" """SQLite time modifier for how long each item is valid in the cache for""" -if sys.platform == "win32": - CACHE_PATH = os.path.realpath( - os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3") - ) -os.makedirs(CACHE_PATH, exist_ok=True) - IDENTIFIERS_FILENAME = "identifier.cache" """Default location to record information about available identifiers""" diff --git a/volatility3/framework/constants/_version.py b/volatility3/framework/constants/_version.py index 7f71c277e..10dbf3cf1 100644 --- a/volatility3/framework/constants/_version.py +++ b/volatility3/framework/constants/_version.py @@ -1,6 +1,6 @@ # We use the SemVer 2.0.0 versioning scheme VERSION_MAJOR = 2 # Number of releases of the library with a breaking change -VERSION_MINOR = 27 # Number of changes that only add to the interface +VERSION_MINOR = 28 # Number of changes that only add to the interface VERSION_PATCH = 0 # Number of changes that do not change the interface VERSION_SUFFIX = "" diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 3f8c52b43..793ef11e8 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -9,6 +9,10 @@ Linux-specific values that aren't found in debug symbols import enum from dataclasses import dataclass +# Exec argument limits +# Ref: include/uapi/linux/binfmts.h (linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940) +MAX_ARG_STRLEN = 32 * 4096 + KERNEL_NAME = "__kernel__" """The value hard coded from the Linux Kernel (hence not extracted from the layer itself)""" @@ -432,6 +436,17 @@ VMCOREINFO_MAGIC = b"VMCOREINFO\x00" VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00" OSRELEASE_TAG = b"OSRELEASE=" +ATTRIBUTE_NAME_MAX_SIZE = 255 +""" +In 5.9-rc1+, the Linux kernel limits the READ size of a section bin_attribute name to MODULE_SECT_READ_SIZE: + +- https://elixir.bootlin.com/linux/v6.15-rc4/source/kernel/module/sysfs.c#L106 +- https://github.com/torvalds/linux/commit/11990a5bd7e558e9203c1070fc52fb6f0488e75b + +However, the raw section name loaded from the .ko ELF can in theory be thousands of characters, +and unless we do a NULL terminated search we can't set a perfect value. +""" + @dataclass class TaintFlag: diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index 7c052f70a..841d821cb 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -160,8 +160,7 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer): if frame_type == 0xFF: if ( data[ - offset - + frame_header_len : offset + offset + frame_header_len : offset + frame_header_len + frame_size ] diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 848580004..380d0e49f 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -10,7 +10,7 @@ import struct from typing import Any, Dict, Iterable, List, Optional, Tuple from volatility3 import classproperty -from volatility3.framework import exceptions, interfaces, constants +from volatility3.framework import constants, exceptions, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.layers import linear @@ -38,7 +38,7 @@ class Intel(linear.LinearlyMappedLayer): # NOTE: _maxphyaddr is MAXPHYADDR as defined in the Intel specs *NOT* the maximum physical address _maxphyaddr = 32 _maxvirtaddr = _maxphyaddr - _structure = [("page directory", 10, False), ("page table", 10, True)] + _structure = [("page directory", 10, True), ("page table", 10, False)] _direct_metadata = collections.ChainMap( {"architecture": "Intel32"}, {"mapped": True}, @@ -221,18 +221,6 @@ class Intel(linear.LinearlyMappedLayer): entry, "Page Fault at entry " + hex(entry) + " in table " + name, ) - # Check if we're a large page - if large_page and (entry & self._PAGE_PSE): - # Mask off the PAT bit - if entry & self._PAGE_PAT_LARGE: - entry -= self._PAGE_PAT_LARGE - # We're a large page, the rest is finished below - # If we want to implement PSE-36, it would need to be done here - break - # Figure out how much of the offset we should be using - start = position - position -= size - index = self._mask(page_address, start, position + 1) >> (position + 1) # Grab the base address of the table we'll be getting the next entry from base_address = self._mask( @@ -249,6 +237,11 @@ class Intel(linear.LinearlyMappedLayer): "Page Fault at entry " + hex(entry) + " in table " + name, ) + # Figure out how much of the offset we should be using + start = position + position -= size + index = self._mask(page_address, start, position + 1) >> (position + 1) + # Read the data for the next entry entry_data_start = index << self._index_shift entry_data = table[entry_data_start : entry_data_start + self._entry_size] @@ -262,16 +255,52 @@ class Intel(linear.LinearlyMappedLayer): # Read out the new entry from memory (entry,) = struct.unpack(self._entry_format, entry_data) + # Check if we're a large page + if large_page and (entry & self._PAGE_PSE): + # Mask off the PAT bit + if entry & self._PAGE_PAT_LARGE: + entry -= self._PAGE_PAT_LARGE + # We're a large page, the rest is finished below + # If we want to implement PSE-36, it would need to be done here + break + return entry, position @functools.lru_cache(maxsize=1025) def _get_valid_table(self, base_address: int) -> Optional[bytes]: """Extracts the table, validates it and returns it if it's valid.""" - table = self._context.layers.read( - self._base_layer, base_address, self.page_size - ) + try: + table = self._context.layers.read( + self._base_layer, base_address, self.page_size + ) + except exceptions.InvalidAddressException: + return None + #### # If the table is entirely duplicates, then mark the whole table as bad + # This is because Windows 10 onwards has a tendency to map unused pages as present + # This had the following consequences: + # - Used very litle physical memory + # - Exploded virtual memory + # - Causes *scan plugins to take multiple hours to complete even on small images + + # Previous versions of volatility would ignore a page during a scan when it matched + # the one directly preceding it in physical memory. + # This could trip if only two pages were identical and still required enumerating all + # the invalid pages (which itself was quite time consuming) + + # For this reason, volatility 3 shifted to looking at entire page tables (1,024 pages) + # and if all the pages mapped to the same place the table wouuld be skipped + # This could also be applied to the Directory level as well as the Table level, allowing + # Volatility to skip huge sections of virtual memory very efficiently, without missing + # any pages that were distinct within a particular page table (or directory). + + # In order to work at this level, the logic was moved out of the scanning component and + # directly into the layer logic itself. This does have the side effect of preventing + # entirely duplicated page tables from reporting as present, however, the trade off between + # Windows 10+ reduced scanning times (common amongst scan plugins) versus incorrectly reporting + # entire page tables of identically mapped repeating *valid* data (rare) was accepted in favour + # of the more common occurance. if table == table[: self._entry_size] * self._entry_number: return None return table @@ -371,12 +400,18 @@ class Intel(linear.LinearlyMappedLayer): yield offset, length, mapped_offset, length, layer_name return None while length > 0: + skip_mask = None try: chunk_offset, page_size, layer_name = self._translate(offset) - chunk_size = min(page_size - (chunk_offset % page_size), length) + # Page align the chunk size value + chunk_size = min(page_size - (offset % page_size), length) if not self._context.layers[layer_name].is_valid( chunk_offset, chunk_size ): + # Virtual -> physical is contiguous in the chunk_size range. + # If we fail, we can jump directly to the end as we know all bytes in between + # aren't mapped (virtually and) physically anyway. + skip_mask = chunk_size - 1 raise exceptions.InvalidAddressException( layer_name=layer_name, invalid_address=chunk_offset ) @@ -386,12 +421,13 @@ class Intel(linear.LinearlyMappedLayer): ) as excp: if not ignore_errors: raise - # We can jump more if we know where the page fault failed - if isinstance(excp, exceptions.PagedInvalidAddressException): - mask = (1 << excp.invalid_bits) - 1 - else: - mask = (1 << self._page_size_in_bits) - 1 - length_diff = mask + 1 - (offset & mask) + if skip_mask is None: + # We can jump more if we know where the page fault occured + if isinstance(excp, exceptions.PagedInvalidAddressException): + skip_mask = (1 << excp.invalid_bits) - 1 + else: + skip_mask = (1 << self._page_size_in_bits) - 1 + length_diff = skip_mask + 1 - (offset & skip_mask) length -= length_diff offset += length_diff else: @@ -429,7 +465,7 @@ class IntelPAE(Intel): _structure = [ ("page directory pointer", 2, False), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] _direct_metadata = collections.ChainMap({"pae": True}, Intel._direct_metadata) @@ -449,7 +485,7 @@ class Intel32e(Intel): ("page map layer 4", 9, False), ("page directory pointer", 9, True), ("page directory", 9, True), - ("page table", 9, True), + ("page table", 9, False), ] diff --git a/volatility3/framework/objects/__init__.py b/volatility3/framework/objects/__init__.py index 08d6cb31e..40eb532f1 100644 --- a/volatility3/framework/objects/__init__.py +++ b/volatility3/framework/objects/__init__.py @@ -948,9 +948,9 @@ class AggregateType(interfaces.objects.ObjectInterface): if isinstance(cls, agg_type): agg_name = agg_type.__name__ - assert isinstance( - members, collections.abc.Mapping - ), f"{agg_name} members parameter must be a mapping: {type(members)}" + assert isinstance(members, collections.abc.Mapping), ( + f"{agg_name} members parameter must be a mapping: {type(members)}" + ) assert all( (isinstance(member, tuple) and len(member) == 2) for member in members.values() diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 59ac0ee55..799639a67 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -3,11 +3,13 @@ # import re - +import logging from typing import Optional, Union from volatility3.framework import interfaces, objects, constants, exceptions +vollog = logging.getLogger(__name__) + def rol(value: int, count: int, max_bits: int = 64) -> int: """A rotate-left instruction in Python""" @@ -250,3 +252,63 @@ def array_of_pointers( ).clone() subtype_pointer.update_vol(subtype=subtype) return array.cast("array", count=count, subtype=subtype_pointer) + + +def dynamically_sized_array_of_pointers( + context: interfaces.context.ContextInterface, + array: interfaces.objects.ObjectInterface, + subtype: Union[str, interfaces.objects.Template], + iterator_guard_value: int, + stop_value: int = 0, + stop_on_invalid_pointers: bool = True, +) -> interfaces.objects.ObjectInterface: + """Iterates over a dynamically sized array of pointers (e.g. NULL-terminated). + Array iteration should always be performed with an arbitrary guard value as maximum size, + to prevent running forever in case something unexpected happens. + + Args: + context: The context on which to operate. + array: The object to cast to an array. + iterator_guard_value: Stop iterating when the iterator index is greater than this value. This is an extra-safety against smearing. + subtype: The subtype of the array's pointers. + stop_value: Stop value used to determine when to terminate iteration once it is encountered. Defaults to 0 (NULL-terminated arrays). + stop_on_invalid_pointers: Determines whether to stop iterating or not when an invalid pointer is encountered. This can be useful for arrays + that are known to have smeared entries before the end. + + Returns: + An array of pointer objects + """ + new_count = 0 + sym_table_name = array.get_symbol_table_name() + sym_table = context.symbol_space[sym_table_name] + ptr_size = sym_table.get_type("pointer").size + layer_name = array.vol.layer_name + + offset = array.vol.offset + entry = None + while entry != stop_value and new_count < iterator_guard_value: + try: + entry = context.object( + sym_table_name + constants.BANG + "pointer", + offset=offset, + layer_name=layer_name, + ) + except exceptions.InvalidAddressException: + break + + if not entry.is_readable() and stop_on_invalid_pointers: + break + + offset += ptr_size + new_count += 1 + else: + vollog.log( + constants.LOGLEVEL_V, + f"""Iterator guard value {iterator_guard_value} reached while iterating over array at offset {array.vol.offset:#x}.\ + This means that there is a bug (e.g. smearing) with this array, or that it may contain valid entries past the iterator guard value.""", + ) + + # Leverage the "Array" object instead of returning a Python list + return array_of_pointers( + array=array, count=new_count, subtype=subtype, context=context + ) diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index eea39206d..69a1ee2b7 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -4,10 +4,11 @@ import logging from typing import List -from volatility3.framework import interfaces, renderers, layers +from volatility3.framework import constants, interfaces, layers, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows import pdbutil vollog = logging.getLogger(__name__) @@ -16,6 +17,7 @@ class Banners(interfaces.plugins.PluginInterface): """Attempts to identify potential linux banners in an image""" _required_framework_version = (2, 0, 0) + _version = (1, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -28,6 +30,11 @@ class Banners(interfaces.plugins.PluginInterface): component=scanners.RegExScanner, version=(1, 0, 0), ), + requirements.VersionRequirement( + name="pdb_signature_scanner", + component=pdbutil.PdbSignatureScanner, + version=(1, 0, 0), + ), ] def _generator(self): @@ -42,6 +49,7 @@ class Banners(interfaces.plugins.PluginInterface): cls, context: interfaces.context.ContextInterface, layer_name: str ): """Identifies banners from a memory image""" + # Look for likely linux/mac banners layer = context.layers[layer_name] for offset in layer.scan( context=context, @@ -64,6 +72,25 @@ class Banners(interfaces.plugins.PluginInterface): format_hints.Hex(offset), str(data, encoding="latin-1", errors="?"), ) + yield from cls.locate_windows_banners(context, layer_name) + + @classmethod + def locate_windows_banners( + cls, context: interfaces.context.ContextInterface, layer_name: str + ): + layer = context.layers[layer_name] + kernel_pdb_names = [ + bytes(name + ".pdb", "utf-8") + for name in constants.windows.KERNEL_MODULE_NAMES + ] + for guid, age, pdb_name, offset in layer.scan( + context=context, + scanner=pdbutil.PdbSignatureScanner(kernel_pdb_names), + ): + yield ( + format_hints.Hex(offset), + f"{pdb_name.decode('latin-1')}|{guid}|{age}", + ) def run(self): return renderers.TreeGrid( diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py index cbd9f87c1..533a4e217 100644 --- a/volatility3/framework/plugins/linux/malware/malfind.py +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -88,7 +88,12 @@ class Malfind(interfaces.plugins.PluginInterface): 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 + 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) diff --git a/volatility3/framework/plugins/linux/malware/process_spoofing.py b/volatility3/framework/plugins/linux/malware/process_spoofing.py new file mode 100644 index 000000000..493a1de9a --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/process_spoofing.py @@ -0,0 +1,300 @@ +# 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 pathlib import PurePosixPath +from typing import Optional, Tuple, Iterator + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.constants import linux as linux_constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class ProcessSpoofing(plugins.PluginInterface): + """Detects process spoofing by comparing executable path to cmdline & comm fields. + + Examples of such behavior can be found here: https://github.com/SolitudePy/linux-mal + """ + + _required_framework_version = (2, 27, 0) + _version = (1, 1, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + @classmethod + def get_executable_path( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Optional[str]: + """ + Extract the executable path from task_struct.mm.exe_file + + Args: + context: The context to operate on + task: task_struct object of the process + + Returns: + Returns executable path or None if not available + """ + + try: + mm = task.mm + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to access mm for task at {task.vol.offset:#x}: {e}") + return None + + if not mm or not mm.is_readable(): + # Kernel threads don't have mm struct + return None + + try: + exe_file = mm.exe_file + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to access exe_file for task at {task.vol.offset:#x}: {e}" + ) + return None + + if not exe_file or not exe_file.is_readable(): + return None + + try: + exe_path = linux.LinuxUtilities.path_for_file(context, task, exe_file) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug( + f"Unable to read exe_file path for task at {task.vol.offset:#x}: {e}" + ) + return None + + return exe_path + + @classmethod + def get_cmdline_basename( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Optional[str]: + """ + Extract the command line arguments and return the basename of the first argument. + + Notes: + The read length is capped at ``MAX_ARG_STRLEN`` (32 * 4096) per the + kernel limit defined in ``include/uapi/linux/binfmts.h`` (see + linux.git commit f6031913338f1dad5bd8cb7286ff4e53644b6940). + + Args: + context: The context to operate on + task: task_struct object of the process + + Returns: + Basename of the first command line argument or None if not available + """ + mm = task.mm + if not mm or not mm.is_readable(): + return None + + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + return None + + start = task.mm.arg_start + size_to_read = task.mm.arg_end - task.mm.arg_start + + if size_to_read <= 0: + return None + + read_length = min(size_to_read, linux_constants.MAX_ARG_STRLEN) + + try: + cmdline = utility.address_to_string( + context=context, + layer_name=proc_layer_name, + address=start, + count=read_length, + errors="replace", + encoding="utf-8", + ) + except exceptions.InvalidAddressException as e: + vollog.debug( + f"Unable to read cmdline for task at {task.vol.offset:#x}: {e}" + ) + return None + + if not cmdline: + return None + + basename = PurePosixPath(cmdline).name + return basename if basename else None + + @classmethod + def get_comm(cls, task: interfaces.objects.ObjectInterface) -> Optional[str]: + """ + Extract the comm field from task_struct + + Args: + task: task_struct object of the process + + Returns: + Process name from comm field or None if not available + """ + try: + return utility.array_to_string(task.comm) + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.debug(f"Unable to read comm for task at {task.vol.offset:#x}: {e}") + return None + + @classmethod + def extract_process_names( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], bool]: + """ + Extract all process name sources for comparison + + Returns: + Tuple of (exe_path, exe_basename, cmdline_basename, comm) + """ + exe_path = cls.get_executable_path(context, task) + exe_basename = PurePosixPath(exe_path).name if exe_path else None + if exe_basename and exe_basename.endswith(" (deleted)"): + exe_basename = exe_basename[: -len(" (deleted)")] + cmdline_basename = cls.get_cmdline_basename(context, task) + comm = cls.get_comm(task) + + return exe_path, exe_basename, cmdline_basename, comm + + def _detect_spoofing( + self, + exe_basename: Optional[str], + cmdline_basename: Optional[str], + comm: Optional[str], + ) -> Tuple[bool, bool]: + """ + Analyze the three name sources to detect potential spoofing + + Args: + exe_basename: Basename from exe_file path + cmdline_basename: Basename from command line + comm: Name from comm field + + Returns: + Tuple of (cmdline_spoofed, comm_spoofed) boolean flags + """ + # Skip kernel threads - need at least 2 sources for comparison + available_sources = sum( + 1 for name in [exe_basename, cmdline_basename, comm] if name + ) + if available_sources < 2: + return False, False + + # Check for cmdline spoofing + cmdline_spoofed = False + if exe_basename and cmdline_basename: + cmdline_spoofed = exe_basename != cmdline_basename + + # Check for comm spoofing (comm is truncated to 15 characters) + comm_spoofed = False + if exe_basename and comm: + comm_spoofed = exe_basename[:15] != comm + + return cmdline_spoofed, comm_spoofed + + def _generator(self, tasks) -> Iterator[Tuple[int, Tuple]]: + """ + Generate process spoofing detection results + + Args: + tasks: Iterator of task_struct objects + + Yields: + Tuple containing process information and spoofing analysis + """ + for task in tasks: + try: + pid = task.pid + ppid = task.get_parent_pid() + + exe_path, exe_basename, cmdline_basename, comm = ( + self.extract_process_names(self.context, task) + ) + + cmdline_spoofed, comm_spoofed = self._detect_spoofing( + exe_basename, cmdline_basename, comm + ) + + is_deleted = exe_path.endswith(" (deleted)") if exe_path else False + + # Convert None values to strings for TreeGrid compatibility + exe_path_render = exe_path if exe_path else "N/A" + cmdline_render = cmdline_basename if cmdline_basename else "N/A" + comm_render = comm if comm else "N/A" + + yield ( + 0, + ( + pid, + ppid, + exe_path_render, + cmdline_render, + comm_render, + cmdline_spoofed, + comm_spoofed, + is_deleted, + ), + ) + + except (exceptions.InvalidAddressException, AttributeError) as e: + vollog.warning( + f"Unable to process task PID {getattr(task, 'pid', 'unknown')} at {task.vol.offset:#x}: {e}" + ) + continue + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("Exe_Path", str), + ("Cmdline_Basename", str), + ("Comm", str), + ("Cmdline_Spoofed", bool), + ("Comm_Spoofed", bool), + ("Exe_Deleted", bool), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index a8864281a..3b6b6f0e5 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -4,8 +4,8 @@ import logging from typing import List +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3 import framework -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints @@ -17,7 +17,7 @@ vollog = logging.getLogger(__name__) class ModuleExtract(interfaces.plugins.PluginInterface): """Recreates an ELF file from a specific address in the kernel""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) @@ -37,9 +37,9 @@ class ModuleExtract(interfaces.plugins.PluginInterface): optional=False, ), requirements.VersionRequirement( - name="linux_utilities_module_extract", - version=(1, 0, 0), - component=linux_utilities_module_extract.ModuleExtract, + name="linux_utilities_modules_module_extract", + version=(1, 0, 2), + component=linux_utilities_modules.ModuleExtract, ), ] @@ -58,7 +58,7 @@ class ModuleExtract(interfaces.plugins.PluginInterface): module = kernel.object(object_type="module", offset=base_address, absolute=True) - elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( + elf_data = linux_utilities_modules.ModuleExtract.extract_module( self.context, self.config["kernel"], module ) if not elf_data: diff --git a/volatility3/framework/plugins/linux/pidhashtable.py b/volatility3/framework/plugins/linux/pidhashtable.py index b4b1643e1..0324ed846 100644 --- a/volatility3/framework/plugins/linux/pidhashtable.py +++ b/volatility3/framework/plugins/linux/pidhashtable.py @@ -194,15 +194,11 @@ class PIDHashTable(plugins.PluginInterface): has_pid_numbers = vmlinux.has_type("pid") and vmlinux.get_type( "pid" - ).has_member( - "numbers" - ) # kernels >= 2.6.24 + ).has_member("numbers") # kernels >= 2.6.24 has_pid_chain = vmlinux.has_type("upid") and vmlinux.get_type( "upid" - ).has_member( - "pid_chain" - ) # 2.6.24 <= kernels < 4.15 + ).has_member("pid_chain") # 2.6.24 <= kernels < 4.15 # kernels >= 4.15 pid_idr = vmlinux.has_type("pid_namespace") and vmlinux.get_type( diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index e9a126374..2c6ebd825 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -70,9 +70,9 @@ class Maps(plugins.PluginInterface): def list_vmas( cls, task: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: True, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + True + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Memory Areas of a specific process. diff --git a/volatility3/framework/plugins/linux/sockscan.py b/volatility3/framework/plugins/linux/sockscan.py new file mode 100644 index 000000000..9288d85f0 --- /dev/null +++ b/volatility3/framework/plugins/linux/sockscan.py @@ -0,0 +1,474 @@ +# This file is Copyright 2024 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 +import struct +from typing import List, Set + +from volatility3.framework import exceptions, constants +from volatility3.framework import renderers +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols import linux +from volatility3.framework import symbols +from volatility3.plugins.linux import lsof, pslist, sockstat +from volatility3.framework.layers import scanners +from volatility3.framework.symbols.linux import network + +vollog = logging.getLogger(__name__) + + +class Sockscan(plugins.PluginInterface): + """Scans for network connections found in memory layer.""" + + _required_framework_version = (2, 6, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="SockHandlers", component=sockstat.SockHandlers, version=(4, 0, 0) + ), + requirements.VersionRequirement( + name="lsof", component=lsof.Lsof, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 1, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="linux_net", component=network.NetSymbols, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="multi_string_scanner", + component=scanners.MultiStringScanner, + version=(1, 0, 0), + ), + ] + + def _canonicalize_symbol_addrs( + self, kernel_module_name: str, symbol_names: List[str] + ) -> Set[bytes]: + """Takes a list of symbol names and converts the address of each to the bytes + as they would appear in memory so that they can be scanned for. + + Symbols that cannot be found are ignored and not included in the results. + + Args: + kernel_module_name: The name of the kernel module on which to operate + symbol_names: A list of symbol names to be looked up + + Returns: + A set of bytes which are the packed addresses. + """ + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # detmine if kernel is 64bit or not. The plugin scans for pointers and these need to formated + # to the correct size so that they can be accurately located in the physical layer. + if symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name): + pack_format = "Q" # 64 bit + else: + pack_format = "I" # 32 bit + + packed_needles = set() + for symbol_name in symbol_names: + try: + needle_addr = vmlinux.object_from_symbol(symbol_name).vol.offset + except exceptions.SymbolError: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to find symbol {symbol_name} this will not be scanned for.", + ) + continue + # use canonicalize to set the appropriate sign extension for the addr + addr = kernel_layer.canonicalize(needle_addr) + packed_addr = struct.pack(pack_format, addr) + packed_needles.add(packed_addr) + vollog.log( + constants.LOGLEVEL_VVVV, + f"Will scan for {symbol_name} using the bytes: {packed_addr.hex()}", + ) + + # make a warning if no symbols at all could be resolved. + if not packed_needles: + vollog.warning( + "_canonicalize_symbol_addrs was unable to resolve any symbols, use -vvvv for more information." + ) + + return packed_needles + + def _find_memory_layer_name(self, kernel_module_name: str): + """Find the memory layer below the kernel. Only returns a single layer, + and will warn the user if multiple layers are found. + + Args: + kernel_module_name: The name of the kernel module on which to operate. + + Returns: + memory_layer_name: The name of the layer below the kernel to be scanned. + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get kernel layer from context so that it's dependencies can be found, and therefore scanned. + # kernel layer will be virtual and built ontop of a physical layer. + kernel_layer = self.context.layers[vmlinux.layer_name] + + # TODO: Update plugin to support multiple dependencies. e.g. a memory layer and swap file. + # This is a shared problem with psscan and having a generic solution would be useful. + + # Find the memory layer to scan, and provide warnings if more than one is located. + if len(kernel_layer.dependencies) > 1: + vollog.warning( + f"Kernel layer depends on multiple layers however only {kernel_layer.dependencies[0]} will be scanned by this plugin." + ) + elif len(kernel_layer.dependencies) == 0: + vollog.error( + "Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + ) + raise exceptions.LayerException( + vmlinux.layer_name, f"Layer {vmlinux.layer_name} has no dependencies" + ) + + memory_layer_name = kernel_layer.dependencies[0] + + return memory_layer_name + + def _find_file_ops_needles(self, kernel_module_name: str): + """Retrieves socket file symbols and the offset to the 'f_op' pointer. + + Args: + kernel_module_name (str): The name of the kernel module to search. + + Returns: + Tuple[List[int], int]: A list of file symbol addresses and, + the offset to the 'f_op' pointer. + """ + + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[kernel_module_name] + + file_ops_symbol_names = [ + "socket_file_ops", + "sockfs_dentry_operations", + ] + file_ops_needles = self._canonicalize_symbol_addrs( + kernel_module_name, file_ops_symbol_names + ) + # get file struct to find the offset to the f_op pointer + # this is so that the file object can be created at the correct offset, + # the results of the scanner will be for the f_op member within the file + f_op_offset = vmlinux.get_type("file").relative_child_offset("f_op") + + return (file_ops_needles, f_op_offset) + + def _find_sk_destruct_needles(self, kernel_module_name: str): + # get vmlinux module from context in order to read symbols + vmlinux = self.context.modules[kernel_module_name] + + socket_destructor_symbol_names = [ + "sock_def_destruct", + "packet_sock_destruct", + "unix_sock_destructor", + "netlink_sock_destruct", + "inet_sock_destruct", + ] + socket_destructor_needles = self._canonicalize_symbol_addrs( + kernel_module_name, socket_destructor_symbol_names + ) + # get sock struct to find the offset to the sk_destruct pointer + # this is so that the sock object can be created at the correct offset, + # the results of the scanner will be for the sk_destruct member within the scock + sk_destruct_offset = vmlinux.get_type("sock").relative_child_offset( + "sk_destruct" + ) + return (socket_destructor_needles, sk_destruct_offset) + + def _walk_file_ops_needles( + self, + kernel_module_name: str, + physical_memory_layer_name: str, + needle_addr: int, + f_op_offset: int, + ): + """ + This method attempts to walk from the f_op member of files to the + corresponding socket. If sucessful the socket object is created on the + memory layer and returned. + + Args: + kernel_module_name (str): The name of the kernel module from which, + to retrieve the file operations. + physical_memory_layer_name (str): The name of the physical memory layer that was scanned + needle_addr: The address of the needle that was found during the scanning + f_op_offset: The offset to the f_op member of the file type + + Returns: + psock: The sock object that was built on the memory layer + """ + + vmlinux = self.context.modules[kernel_module_name] + try: + # create file in the memory_layer, the native layer matches the + # kernel so that pointers can be followed + sock_physical_addr = needle_addr - f_op_offset + pfile = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "file", + offset=sock_physical_addr, + layer_name=physical_memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + dentry = pfile.get_dentry() + if not dentry: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate dentry", + ) + return None + + d_inode = dentry.d_inode + if not d_inode: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as unable to locate inode for dentry", + ) + return None + + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + if not (socket and socket.sk): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping file at {hex(needle_addr)} as socket created by LinuxUtilities.container_of is invalid", + ) + return None + + # sucessfully trversed from file to sock, this will exist in the + # kernel layer, and need to be translated to the memory layer. + vsock = socket.sk.dereference() + + # get virtual offset + virtual_sock_offset = vsock.vol.offset + + # translate this offset to physical + native_layer = self.context.layers[vmlinux.layer_name] + physical_sock_offset, _physical_layer_name = native_layer.translate( + virtual_sock_offset + ) + + # build sock on the memory_layer using the physical_sock_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=physical_sock_offset, + layer_name=physical_memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + return psock + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable to follow file at {hex(needle_addr)} to socket due to invalid address: {error}", + ) + return None + + def _extract_sock_fields(self, psock, sock_handler): + try: + sock_physical_addr = psock.vol.offset + sock_type = psock.get_type() + + family = psock.get_family() + # remove results with no family + if family is None: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to determin family.", + ) + return None + + # TODO: invesitgate options for more invalid address handling in proccess_sock + # and the later formatting of it's results. + sock_fields = sock_handler.process_sock(psock) + # if no sock_fields we're able to be extracted then skip this result. + if not sock_fields: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Skipping socket at {hex(sock_physical_addr)} as unable to process with SockHandlers.", + ) + return None + + sock, sock_stat, extended = sock_fields + src, src_port, dst, dst_port, state = sock_stat + protocol = sock.get_protocol() + + # format results + src = renderers.NotAvailableValue() if src is None else str(src) + src_port = ( + renderers.NotAvailableValue() if src_port is None else str(src_port) + ) + dst = renderers.NotAvailableValue() if dst is None else str(dst) + dst_port = ( + renderers.NotAvailableValue() if dst_port is None else str(dst_port) + ) + state = renderers.NotAvailableValue() if state is None else str(state) + protocol = ( + renderers.NotAvailableValue() if protocol is None else str(protocol) + ) + # extended attributes is a dict, so this is formated to string show each + # key and value pair, seperated with a comma. + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else renderers.NotAvailableValue() + ) + + # remove empty results + if (src == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue)) and ( + dst == "0.0.0.0" or isinstance(src, renderers.NotAvailableValue) + ): + if state == "UNCONNECTED": + return None + elif src_port == "0" and dst_port == "0": + return None + return ( + format_hints.Hex(sock_physical_addr), + family, + sock_type, + protocol, + src, + src_port, + dst, + dst_port, + state, + socket_filter_str, + ) + + except exceptions.InvalidAddressException as error: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Unable create results for socket at {hex(sock_physical_addr)} due to invalid address: {error}", + ) + return None + + def _generator(self, kernel_module_name: str): + """Scans for sockets. Each row represents a kernel socket. + + Args: + kernel_module_name: The name of the kernel module on which to operate + + Yields: + addr: Physical offset + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + """ + + # get vmlinux module from context in order to build objects and read symbols + vmlinux = self.context.modules[kernel_module_name] + + # get the memory layer that is to be scanned. + memory_layer_name = self._find_memory_layer_name(kernel_module_name) + memory_layer = self.context.layers[memory_layer_name] + + # use the init process to build a sock handler + # TODO: look into options so that sockstat.SockHandlers so that process_sock can + # be used without a task object. + init_task = vmlinux.object_from_symbol(symbol_name="init_task") + sock_handler = sockstat.SockHandlers( + self.context, kernel_module_name, init_task + ) + + # get progress_callback in order to use this in the scanners. + # TODO: perhaps add more detail to progress, showing method in progress and number of hits found + progress_callback = self._progress_callback + + # Method 1 - find sockets by file operations, then follow pointers to sockets + file_ops_needles, f_op_offset = self._find_file_ops_needles(kernel_module_name) + + # Method 2 - find sockets by socket destructor directly inside sock objects + socket_destructor_needles, sk_destruct_offset = self._find_sk_destruct_needles( + kernel_module_name + ) + + # TODO Method 3 - find sock by sk_error_report symbols + # sk_error_report_symbol_names = ['sock_def_error_report', 'inet_sk_rebuild_header', 'inet_listen'] + # this would be similar to Method 2, but using a different pointer within sock. + + # add a set of seen addresses to stop possible duplication of results. + seen_sock_physical_addr = set() + + # Using the calculated needles, scan the memory layer and attempt to parse the sockets located. + for needle_addr, match in memory_layer.scan( + self.context, + scanners.MultiStringScanner(socket_destructor_needles | file_ops_needles), + progress_callback, + ): + psock = None + sock_physical_addr = None + + # if match is from socket_destructor_needles simply calculate the offset to the sock + if match in socket_destructor_needles: + sock_physical_addr = needle_addr - sk_destruct_offset + psock = self.context.object( + vmlinux.symbol_table_name + constants.BANG + "sock", + offset=sock_physical_addr, + layer_name=memory_layer_name, + native_layer_name=vmlinux.layer_name, + ) + + # if match is from file_ops_needles attempt to walk from file object to the sock + if match in file_ops_needles: + psock = self._walk_file_ops_needles( + kernel_module_name, memory_layer_name, needle_addr, f_op_offset + ) + + if psock is not None and sock_physical_addr not in seen_sock_physical_addr: + seen_sock_physical_addr.add(sock_physical_addr) + + fields = self._extract_sock_fields(psock, sock_handler) + if fields: + yield (0, fields) + + def run(self): + + tree_grid_args = [ + ("Sock Offset", format_hints.Hex), + ("Family", str), + ("Type", str), + ("Proto", str), + ("Source Addr", str), + ("Source Port", str), + ("Destination Addr", str), + ("Destination Port", str), + ("State", str), + ("Filter", str), + ] + + return renderers.TreeGrid( + tree_grid_args, + self._generator(self.config["kernel"]), + ) diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index ff922784d..c7e4dbe34 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -34,7 +34,9 @@ class PerfEvents(plugins.PluginInterface): ] @classmethod - def list_perf_events(cls, context, vmlinux_module_name: str) -> Generator[ + def list_perf_events( + cls, context, vmlinux_module_name: str + ) -> Generator[ Tuple[ interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface, diff --git a/volatility3/framework/plugins/mac/proc_maps.py b/volatility3/framework/plugins/mac/proc_maps.py index 87f3559ea..b1370afdb 100644 --- a/volatility3/framework/plugins/mac/proc_maps.py +++ b/volatility3/framework/plugins/mac/proc_maps.py @@ -65,9 +65,9 @@ class Maps(interfaces.plugins.PluginInterface): def list_vmas( cls, task: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: True, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + True + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Memory Areas of a specific process. diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 904e4e201..f36bcd831 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -49,7 +49,9 @@ class PsList(interfaces.plugins.PluginInterface): ] @classmethod - def get_list_tasks(cls, method: str) -> Callable[ + def get_list_tasks( + cls, method: str + ) -> Callable[ [interfaces.context.ContextInterface, str, Callable[[int], bool]], Iterable[interfaces.objects.ObjectInterface], ]: diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py index f4eaf9c9b..c08fa7231 100644 --- a/volatility3/framework/plugins/renderers/parquet_renderer.py +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -52,7 +52,7 @@ class ArrowRenderer(text_renderer.CLIRenderer): datetime.datetime: lambda: pa.timestamp("ms"), format_hints.Bin: pa.uint64, format_hints.Hex: pa.uint64, - format_hints.MultiTypeData: pa.utf8, + format_hints.MultiTypeData: pa.binary, format_hints.HexBytes: pa.binary, renderers.LayerData: pa.binary, bytes: pa.binary, diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index f65868705..ddcc393f4 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -153,6 +153,8 @@ orders the results by time.""" ) times[timestamp_type] = timestamp self.timeline[(plugin_name, item)] = times + + for plugin_name, item in self.timeline: data.append( ( 0, diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index c04472eab..96786b586 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -19,9 +19,7 @@ vollog = logging.getLogger(__name__) def createservicesid(svc) -> str: """Calculate the Service SID""" uni = "".join([c + "\x00" for c in svc]) - sha = hashlib.sha1( - uni.upper().encode("utf-8") - ).digest() # pylint: disable-msg=E1101 + sha = hashlib.sha1(uni.upper().encode("utf-8")).digest() # pylint: disable-msg=E1101 dec = list() for i in range(5): ## The use of struct here is OK. It doesn't make much sense diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py index cd898239f..867609f16 100644 --- a/volatility3/framework/plugins/windows/malware/pebmasquerade.py +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -37,7 +37,9 @@ class PebMasquerade(interfaces.plugins.PluginInterface): ] @classmethod - def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[ + def get_process_names( + cls, proc: interfaces.objects.ObjectInterface + ) -> Tuple[ Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], Union[str, renderers.NotAvailableValue], diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index 00c6fd868..b21e39a8c 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -709,17 +709,15 @@ class PESymbols(interfaces.plugins.PluginInterface): # symbol_info will be a symbol name or address requested for symbol_info in wanted_symbols: - if ( - wanted_type == wanted_names_identifier - and type(symbol_info) not in valid_name_types + if wanted_type == wanted_names_identifier and not isinstance( + symbol_info, tuple(valid_name_types) ): raise ValueError( f"The requested symbol name has a type of {type(symbol_info)} which is not in the allowed set of {valid_name_types}" ) - elif ( - wanted_type == wanted_addresses_identifier - and type(symbol_info) not in valid_address_types + elif wanted_type == wanted_addresses_identifier and not isinstance( + symbol_info, tuple(valid_address_types) ): raise ValueError( f"The requested address has a type of {type(symbol_info)} which is not in the allowed set of {valid_address_types}" diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index 1043f8b42..db3e5dc99 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -167,13 +167,15 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Filter function for passing to the `list_processes` method """ - return lambda x: not ( - x.is_valid() - and x.ActiveThreads > 0 - and x.UniqueProcessId != 4 - and x.InheritedFromUniqueProcessId != 4 - and x.ExitTime.QuadPart == 0 - and x.get_handle_count() != renderers.UnreadableValue() + return lambda x: ( + not ( + x.is_valid() + and x.ActiveThreads > 0 + and x.UniqueProcessId != 4 + and x.InheritedFromUniqueProcessId != 4 + and x.ExitTime.QuadPart == 0 + and x.get_handle_count() != renderers.UnreadableValue() + ) ) @classmethod @@ -214,9 +216,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Iterator["extensions.EPROCESS"]: """Lists all the processes in the given layer that are in the pid config option. diff --git a/volatility3/framework/plugins/windows/psscan.py b/volatility3/framework/plugins/windows/psscan.py index ae37c20a1..de69c23be 100644 --- a/volatility3/framework/plugins/windows/psscan.py +++ b/volatility3/framework/plugins/windows/psscan.py @@ -150,9 +150,9 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for processes using the poolscanner module and constraints. diff --git a/volatility3/framework/plugins/windows/pstree.py b/volatility3/framework/plugins/windows/pstree.py index 373c555f6..c8d9a3f66 100644 --- a/volatility3/framework/plugins/windows/pstree.py +++ b/volatility3/framework/plugins/windows/pstree.py @@ -53,9 +53,9 @@ class PsTree(interfaces.plugins.PluginInterface): def find_level( self, pid: int, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> None: """Finds how deep the pid is in the processes list.""" seen = {pid} @@ -77,9 +77,9 @@ class PsTree(interfaces.plugins.PluginInterface): def _generator( self, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ): """Generates the Tree of processes.""" kernel = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/windows/registry/cachedump.py b/volatility3/framework/plugins/windows/registry/cachedump.py index 49c5495c2..5e138ba99 100644 --- a/volatility3/framework/plugins/windows/registry/cachedump.py +++ b/volatility3/framework/plugins/windows/registry/cachedump.py @@ -5,8 +5,13 @@ import logging from struct import unpack from typing import Tuple -from Crypto.Cipher import ARC4, AES -from Crypto.Hash import HMAC +try: + from Crypto.Cipher import ARC4, AES + from Crypto.Hash import HMAC +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, AES + from Cryptodome.Hash import HMAC from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 19bd60e81..6f8a2bce8 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -7,7 +7,11 @@ import logging from struct import pack, unpack from typing import List, Optional, Tuple -from Crypto.Cipher import AES, ARC4, DES +try: + from Crypto.Cipher import ARC4, DES, AES +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions, constants from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/registry/lsadump.py b/volatility3/framework/plugins/windows/registry/lsadump.py index 50ecaebc1..7233f82ac 100644 --- a/volatility3/framework/plugins/windows/registry/lsadump.py +++ b/volatility3/framework/plugins/windows/registry/lsadump.py @@ -6,7 +6,11 @@ from struct import unpack from typing import Optional import hashlib -from Crypto.Cipher import ARC4, DES, AES +try: + from Crypto.Cipher import ARC4, DES, AES +except ImportError: + # Debian/Ubuntu ship pycryptodome under Cryptodome namespace + from Cryptodome.Cipher import ARC4, DES, AES from volatility3.framework import interfaces, renderers, exceptions from volatility3.framework.configuration import requirements diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index 1588f292e..4b2bf47d2 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -99,12 +99,8 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) thread_tid = ethread.Cid.UniqueThread thread_start_addr = ethread.StartAddress thread_win32start_addr = ethread.Win32StartAddress - thread_create_time = ( - ethread.get_create_time() - ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object - thread_exit_time = ( - ethread.get_exit_time() - ) # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object + thread_create_time = ethread.get_create_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object + thread_exit_time = ethread.get_exit_time() # datetime.datetime object / volatility3.framework.renderers.UnparsableValue object owner_proc = None if vads_cache is not None: diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 22d42505f..25abd729e 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -115,9 +115,9 @@ class VadInfo(interfaces.plugins.PluginInterface): def list_vads( cls, proc: interfaces.objects.ObjectInterface, - filter_func: Callable[ - [interfaces.objects.ObjectInterface], bool - ] = lambda _: False, + filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: ( + False + ), ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Address Descriptors of a specific process. @@ -198,7 +198,9 @@ class VadInfo(interfaces.plugins.PluginInterface): return file_handle - def _generator(self, procs: List[interfaces.objects.ObjectInterface]) -> Generator[ + def _generator( + self, procs: List[interfaces.objects.ObjectInterface] + ) -> Generator[ Tuple[ int, Tuple[ diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 19fb8f1d4..7560da3ec 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -52,7 +52,6 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("idr", extensions.IDR) self.set_type_class("address_space", extensions.address_space) self.set_type_class("page", extensions.page) - self.set_type_class("module_sect_attr", extensions.module_sect_attr) # Might not exist in the current symbols self.optional_set_type_class("module", extensions.module) @@ -61,6 +60,8 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) self.optional_set_type_class("scatterlist", extensions.scatterlist) + self.optional_set_type_class("module_sect_attr", extensions.module_sect_attr) + self.optional_set_type_class("bin_attribute", extensions.bin_attribute) # kernels >= 4.18 self.optional_set_type_class("timespec64", extensions.timespec64) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index aa052cf6d..0a6e05951 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -179,25 +179,45 @@ class module(generic.GenericIntelProcess): return None def _get_sect_count(self, grp: interfaces.objects.ObjectInterface) -> int: - """Try to determine the number of valid sections""" - symbol_table_name = self.get_symbol_table_name() - arr = self._context.object( - symbol_table_name + constants.BANG + "array", - layer_name=self.vol.layer_name, - offset=grp.attrs, - subtype=self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "pointer" - ), - count=25, - ) + """Try to determine the number of valid sections. Support for kernels > 6.14-rc1. - idx = 0 - while arr[idx] and arr[idx].is_readable(): - idx = idx + 1 - return idx + Resources: + - https://github.com/torvalds/linux/commit/d8959b947a8dfab1047c6fd5e982808f65717bfe + - https://github.com/torvalds/linux/commit/e0349c46cb4fbbb507fa34476bd70f9c82bad359 + """ + + if grp.has_member("bin_attrs"): + arr_offset_ptr = grp.bin_attrs + arr_subtype = "bin_attribute" + else: + arr_offset_ptr = grp.attrs + arr_subtype = "attribute" + + if not arr_offset_ptr.is_readable(): + vollog.log( + constants.LOGLEVEL_V, + f"Cannot dereference the pointer to the NULL-terminated list of binary attributes for module at offset {self.vol.offset:#x}", + ) + return 0 + + # We chose 100 as an arbitrary guard value to prevent + # looping forever in extreme cases, and because 100 is not expected + # to be a valid number of sections. If that still happens, + # Vol3 module processing will indicate that it is missing information + # with the following message: + # "Unable to reconstruct the ELF for module struct at" + # See PR #1773 for more information. + bin_attrs_list = utility.dynamically_sized_array_of_pointers( + context=self._context, + array=arr_offset_ptr.dereference(), + subtype=self.get_symbol_table_name() + constants.BANG + arr_subtype, + iterator_guard_value=100, + ) + return len(bin_attrs_list) @functools.cached_property def number_of_sections(self) -> int: + # Dropped in 6.14-rc1: d8959b947a8dfab1047c6fd5e982808f65717bfe if self.sect_attrs.has_member("nsections"): return self.sect_attrs.nsections @@ -205,15 +225,18 @@ class module(generic.GenericIntelProcess): def get_sections(self) -> Iterable[interfaces.objects.ObjectInterface]: """Get a list of section attributes for the given module.""" + if self.number_of_sections == 0: + vollog.debug( + f"Invalid number of sections ({self.number_of_sections}) for module at offset {self.vol.offset:#x}" + ) + return [] symbol_table_name = self.get_symbol_table_name() arr = self._context.object( symbol_table_name + constants.BANG + "array", layer_name=self.vol.layer_name, offset=self.sect_attrs.attrs.vol.offset, - subtype=self._context.symbol_space.get_type( - symbol_table_name + constants.BANG + "module_sect_attr" - ), + subtype=self.sect_attrs.attrs.vol.subtype, count=self.number_of_sections, ) @@ -1092,14 +1115,11 @@ class mm_struct(objects.StructType): vm_area_struct objects """ for vma in self._do_get_vma_iter(): - try: - vma.vm_start - vma.vm_end - vma.get_protection() - - yield vma - except exceptions.InvalidAddressException: + if not vma.is_valid(): vollog.debug(f"Skipping invalid vm_area_struct at {vma.vol.offset:#x}") + continue + + yield vma class super_block(objects.StructType): @@ -1235,6 +1255,39 @@ class vm_area_struct(objects.StructType): retval = retval + "-" return retval + def is_valid(self) -> bool: + """Validate a VMA struct to prevent processing smeared entries.""" + try: + start = self.vm_start + end = self.vm_end + self.get_protection() + except exceptions.InvalidAddressException: + return False + + layer = self._context.layers[self.vol.layer_name] + length = end - start + if ( + (start > end) + or (start == 0 and length == 0) + or (length % layer.page_size != 0) + ): + return False + + if self.vm_file != 0: + try: + inode = self.vm_file.get_inode() + except exceptions.InvalidAddressException: + return False + + # Verify that a file-backed VMA's page offset + # is not greater than the size of the file's inode. + # Check only inode sizes greater than 0 to account for + # special devices (e.g. "/dev/dri/card0") and prevent false negatives. + if inode.i_size > 0 and self.get_page_offset() > inode.i_size: + return False + + return True + # only parse the rwx bits def get_protection(self) -> str: return self._parse_flags(self.vm_flags & 0b1111, vm_area_struct.perm_flags) @@ -3158,7 +3211,9 @@ class module_sect_attr(objects.StructType): """ if hasattr(self, "battr"): try: - return utility.pointer_to_string(self.battr.attr.name, count=32) + return utility.pointer_to_string( + self.battr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # if battr is present then its name attribute needs to be valid vollog.debug(f"Invalid battr name for section at {self.vol.offset:#x}") @@ -3166,14 +3221,18 @@ class module_sect_attr(objects.StructType): elif self.name.vol.type_name == "array": try: - return utility.array_to_string(self.name, count=32) + return utility.array_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # specifically do not return here to give `mattr` a chance vollog.debug(f"Invalid direct name for section at {self.vol.offset:#x}") elif self.name.vol.type_name == "pointer": try: - return utility.pointer_to_string(self.name, count=32) + return utility.pointer_to_string( + self.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: # specifically do not return here to give `mattr` a chance vollog.debug( @@ -3183,10 +3242,33 @@ class module_sect_attr(objects.StructType): # if everything else failed... if hasattr(self, "mattr"): try: - return utility.pointer_to_string(self.mattr.attr.name, count=32) + return utility.pointer_to_string( + self.mattr.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) except exceptions.InvalidAddressException: vollog.debug( f"Unresolvable name for for section at {self.vol.offset:#x}" ) return None + + +class bin_attribute(objects.StructType): + def get_name(self) -> Optional[str]: + """ + Performs extraction of the bin_attribute name + """ + try: + return utility.pointer_to_string( + self.attr.name, count=linux_constants.ATTRIBUTE_NAME_MAX_SIZE + ) + except exceptions.InvalidAddressException: + vollog.debug(f"Invalid attr name for bin_attribute at {self.vol.offset:#x}") + return None + + @property + def address(self) -> int: + """Equivalent to module_sect_attr.address: + - https://github.com/torvalds/linux/commit/4b2c11e4aaf7e3d7fd9ce8e5995a32ff5e27d74f + """ + return self.private diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index e55f77668..5ec254368 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -11,11 +11,7 @@ from typing import ( ) from volatility3 import framework -from volatility3.framework import ( - interfaces, - exceptions, - symbols, -) +from volatility3.framework import interfaces, exceptions, symbols, deprecation from volatility3.framework.constants import linux as linux_constants from volatility3.framework.symbols.linux import extensions @@ -35,54 +31,20 @@ vollog = logging.getLogger(__name__) # ModuleExtract.extract_module is the entry point and only visible method for plugins +# See PR #1773 +@deprecation.renamed_class( + deprecated_class_name="ModuleExtract", + removal_date="2026-06-01", + message="volatility3.framework.symbols.linux.utilities.module_extract.ModuleExtract is to be deprecated. Use volatility3.framework.symbols.linux.utilities.modules.ModuleExtract instead.", +) class ModuleExtract(interfaces.configuration.VersionableInterface): """Extracts Linux kernel module structures into an analyzable ELF file""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 25, 0) framework.require_interface_version(*_required_framework_version) - @classmethod - def _get_module_section_count( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - grp: interfaces.objects.ObjectInterface, - ) -> int: - """ - Used to manually determine the section count for kernels that do not track - this count directly within the attribute structures - """ - kernel = context.modules[vmlinux_name] - - count = 0 - - try: - array = kernel.object( - object_type="array", - offset=grp.attrs, - sub_type=kernel.get_type("pointer"), - count=50, - absolute=True, - ) - - # Walk up to 50 sections counting until we reach the end or a page fault - for sect in array: - if sect.vol.offset == 0: - break - - count += 1 - - except exceptions.InvalidAddressException: - # Use whatever count we reached before the error - vollog.debug( - f"Exception hit counting sections for module at {module.vol.offset:#x}" - ) - - return count - @classmethod def _find_section( cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int @@ -261,54 +223,6 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): return sym_table_data - @classmethod - def _enumerate_original_sections( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - module: extensions.module, - ) -> Optional[Dict[int, str]]: - """ - Enumerates the module's sections as maintained by the kernel after load time - 'Early' sections like .init.text and .init.data are discarded after module - initialization, so they are not expected to be in memory during extraction - """ - if hasattr(module.sect_attrs, "nsections"): - num_sections = module.sect_attrs.nsections - else: - num_sections = cls._get_module_section_count( - context, vmlinux_name, module.sect_attrs.grp - ) - - if num_sections > 1024 or num_sections == 0: - vollog.debug( - f"Invalid number of sections ({num_sections}) for module at offset {module.vol.offset:#x}" - ) - return None - - vmlinux = context.modules[vmlinux_name] - - # This is declared as a zero sized array, so we create ourselves - attribute_type = module.sect_attrs.attrs.vol.subtype - - sect_array = vmlinux.object( - object_type="array", - subtype=attribute_type, - offset=module.sect_attrs.attrs.vol.offset, - count=num_sections, - absolute=True, - ) - - sections: Dict[int, str] = {} - - # for each section, gather its name and address - for index, section in enumerate(sect_array): - name = section.get_name() - - sections[section.address] = name - - return sections - @classmethod def _parse_sections( cls, @@ -325,10 +239,12 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): The data of .strtab is read directly off the module structure and not its section as the section from the original module has no meaning after loading as the kernel does not reference it. """ - original_sections = cls._enumerate_original_sections( - context, vmlinux_name, module - ) - if original_sections is None: + original_sections = {} + for index, section in enumerate(module.get_sections()): + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: return None kernel = context.modules[vmlinux_name] @@ -702,9 +618,10 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): return None # Gather sections - updated_sections, strtab_index, symtab_index = cls._parse_sections( - context, vmlinux_name, module - ) + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result kernel = context.modules[vmlinux_name] diff --git a/volatility3/framework/symbols/linux/utilities/modules.py b/volatility3/framework/symbols/linux/utilities/modules.py index 2b16ec6e0..63b25380a 100644 --- a/volatility3/framework/symbols/linux/utilities/modules.py +++ b/volatility3/framework/symbols/linux/utilities/modules.py @@ -1,5 +1,7 @@ import logging import warnings +import functools +import struct from abc import ABCMeta, abstractmethod from typing import ( Callable, @@ -15,7 +17,6 @@ from typing import ( Union, ) -import volatility3.framework.symbols.linux.utilities.module_extract as linux_utilities_module_extract from volatility3 import framework from volatility3.framework import ( constants, @@ -24,12 +25,14 @@ from volatility3.framework import ( interfaces, objects, renderers, + symbols, ) from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.linux import extensions from volatility3.framework.symbols.linux.utilities import tainting +from volatility3.framework.constants import linux as linux_constants vollog = logging.getLogger(__name__) @@ -71,7 +74,7 @@ class ModuleGathererInterface( class Modules(interfaces.configuration.VersionableInterface): """Kernel modules related utilities.""" - _version = (3, 0, 1) + _version = (3, 0, 2) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) @@ -311,6 +314,7 @@ class Modules(interfaces.configuration.VersionableInterface): return run_results @staticmethod + @functools.lru_cache def get_modules_memory_boundaries( context: interfaces.context.ContextInterface, vmlinux_module_name: str, @@ -781,6 +785,740 @@ class Modules(interfaces.configuration.VersionableInterface): yield name, value +# This module is responsible for producing an ELF file of a kernel module (LKM) loaded in memory +# This extraction task is quite complicated as the Linux kernel discards the ELF header at load time +# Due to this, to support static analysis, we must create an ELF header and proper file based on the sections +# There are also several other significant complications that we must deal with when trying to extract an LKM +# that can be analyzed with static analysis tools +# First, the .strtab points somewhere random and is kept off the module structure, not with the other sections +# Second, all of the symbols (.symtab) have mangled members that we must patch for anything to make sense +# Third, the section name string table (.shstrtab) is not an allocated section, meaning its not in memory +# Not having the .shstrtab makes analysis impossible-to-difficult for static analysis tools. To work around this, +# we create the .shstrtab based on the sections in memory and then glue it in as the final section + + +# ModuleExtract.extract_module is the entry point and only visible method for plugins +class ModuleExtract(interfaces.configuration.VersionableInterface): + """Extracts Linux kernel module structures into an analyzable ELF file""" + + _version = (1, 0, 2) + _required_framework_version = (2, 25, 0) + + framework.require_interface_version(*_required_framework_version) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.VersionRequirement( + name="linux_utilities_modules_modules", + component=Modules, + version=(3, 0, 2), + ), + ] + + @classmethod + def _find_section( + cls, section_lookups: List[Tuple[str, int, int, int]], sym_address: int + ) -> Optional[Tuple[str, int, int, int]]: + """ + Finds the section containing `sym_address` + """ + for name, index, address, size in section_lookups: + if address <= sym_address < address + size: + return name, index, address, size + + return None + + @classmethod + def _get_st_info_for_sym( + cls, sym: interfaces.objects.ObjectInterface, sym_address: int, sect_name: str + ) -> bytes: + """ + This is a helper function called from `_fix_sym_table` + + Calculates the `st_info` value for the given symbol + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html + """ + if sym.st_name > 0: + # Global symbol + bind = linux_constants.STB_GLOBAL + + if sym_address == 0: + sect_type = linux_constants.STT_NOTYPE + elif sect_name: + # rela = relocations + if sect_name.find(".text") != -1 and sect_name.find(".rela") == -1: + sect_type = linux_constants.STT_FUNC + else: + sect_type = linux_constants.STT_OBJECT + + else: + # outside the module being extracted + sect_type = linux_constants.STT_NOTYPE + + else: + # Local symbol + bind = linux_constants.STB_LOCAL + sect_type = linux_constants.STT_SECTION + + # Build the st_info as ELF32_ST_INFO/ELF64_ST_INFO + bind_bits = (bind << 4) & 0xF0 + type_bits = sect_type & 0xF + + st_info_int = (bind_bits | type_bits) & 0xFF + + return struct.pack("B", st_info_int) + + @classmethod + def _get_fixed_sym_fields( + cls, + st_fmt: str, + sym: interfaces.objects.ObjectInterface, + sections: List[Tuple[str, int, int, int]], + ) -> Tuple[str, int, int, int]: + """ + This is a helper function called from `_fix_sym_table` + + The st_value, st_info, and st_shndx fields of each symbol are changed/mangled while loading + Static analysis tools do not understand these transformed values as they only make sense to the kernel loader + We must de-mangle these to have analysis tools understand symbols (a key aspect) + """ + # Start by trying to map a symbol to its section + sym_address = sym.st_value + sect_info = cls._find_section(sections, sym_address) + + if not sect_info: + # Symbol does not point into the module being extracted + sect_name, sect_index, sect_address = None, None, None + st_value_int = sym_address + else: + # relative address inside the section + sect_name, sect_index, sect_address, _ = sect_info + st_value_int = sym_address - sect_address + + # Get the fixed st_value, st_info, and st_shndx that are broken in the mapped file + + # formatted to be written into the extracted file + st_value = struct.pack(st_fmt, st_value_int) + + # returns formatted to be written into the extracted file + st_info = cls._get_st_info_for_sym(sym, sym_address, sect_name) + + # format to reference its section, if any + if sect_name: + st_shndx = struct.pack(" Optional[bytes]: + """ + Args: + context: The context on which to operate. + vmlinux_name: The name of the kernel module. + original_sections: Dict of module section addresses and names. + section_sizes: Dict of module section addresses and sizes. + sym_type_name: ELF symbol type name (should be one of "Elf64_Sym" or "Elf32_Sym"). + st_fmt: "struct"-like unpack format string (should be one of " Optional[Tuple[List, int, int]]: + """ + This function first parses the sections as maintained by the kernel + It then orders the sections by load address, and then gathers the data of each section + We also track the file_offset to correctly have alignment in the output file + + .symtab requires special handling as its so broken in memory as described in `_fix_sym_table` + The data of .strtab is read directly off the module structure and not its section + as the section from the original module has no meaning after loading as the kernel does not reference it. + """ + kernel = context.modules[vmlinux_name] + kernel_layer = context.layers[kernel.layer_name] + modules_addr_min, modules_addr_max = Modules.get_modules_memory_boundaries( + context, vmlinux_name + ) + modules_addr_min &= kernel_layer.address_mask + modules_addr_max &= kernel_layer.address_mask + original_sections = {} + for index, section in enumerate(module.get_sections()): + # Extra sanity check, to prevent OOM on heavily smeared samples at line + # "size = next_address - address" + if not ( + modules_addr_min + <= section.address & kernel_layer.address_mask + < modules_addr_max + ): + continue + + name = section.get_name() + original_sections[section.address] = name + + if not original_sections: + return None + + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + sym_type = "Elf64_Sym" + elf_hdr_type = "Elf64_Ehdr" + st_fmt = " Optional[bytes]: + """ + Creates a `bits` bit ELF header for the file based on recovered values + Called last as it needs information computed from the sections + + Spec: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html + """ + if bits == 32: + fmt = " Optional[int]: + """ + This function makes a best effort to map common section names + to their attributes + """ + known_sections = { + ".note.gnu.build-id": linux_constants.SHT_NOTE, + ".text": linux_constants.SHT_PROGBITS, + ".init.text": linux_constants.SHT_PROGBITS, + ".exit.text": linux_constants.SHT_PROGBITS, + ".static_call.text": linux_constants.SHT_PROGBITS, + ".rodata": linux_constants.SHT_PROGBITS, + ".modinfo": linux_constants.SHT_PROGBITS, + "__param": linux_constants.SHT_PROGBITS, + ".data": linux_constants.SHT_PROGBITS, + ".gnu.linkonce.this_module": linux_constants.SHT_PROGBITS, + ".comment": linux_constants.SHT_PROGBITS, + ".shstrtab": linux_constants.SHT_STRTAB, + ".symtab": linux_constants.SHT_SYMTAB, + ".strtab": linux_constants.SHT_STRTAB, + } + + sect_type_val = linux_constants.SHT_PROGBITS + + if section_name.find(".rela.") != -1: + sect_type_val = linux_constants.SHT_RELA + + elif section_name in known_sections: + sect_type_val = known_sections[section_name] + + return sect_type_val + + # all sections from memory are allocated (SHF_ALLOC) + # special check certain other sections to try and ensure extra flags are added where needed + @classmethod + def _calc_sect_flags(cls, name: str) -> int: + """ + Make a best effort to map common section names to their permissions + If we miss a section here, users of common static analysis tools can mark the + sections are writable or executable manually, but that becomes very cumbersome + and breaks initial analysis by the tool + """ + # All sections in memory are allocated (`A` in readelf -S) + flags = linux_constants.SHF_ALLOC + + if name in [".text", ".init.text", ".exit.text", ".static_call.text"]: + flags = flags | linux_constants.SHF_EXECINSTR + + elif name in [ + ".data", + ".init.data", + ".exit.data", + ".bss", + "__tracepoints", + ".data.once", + "_ftrace_events", + ".gnu.linkonce.this_module", + ]: + flags = flags | linux_constants.SHF_WRITE + + return flags + + @classmethod + def _calc_link( + cls, name: str, strtab_index: int, symtab_index: int, sect_type: int + ) -> int: + """ + Calculates the link value for a section + + The most important ones are symtab indexes for relocations + and to point the symbol table to the string tab + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return symtab_index + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + return strtab_index + + return 0 + + @classmethod + def _calc_entsize(cls, name: str, sect_type: int, bits: int) -> int: + """ + Calculates the entsize for relocation sections and the symbol table section + + Spec: https://refspecs.linuxbase.org/elf/gabi4+/ch4.sheader.html + """ + # looking for RELA sections + if name.find(".rela.") != -1: + return 24 + + # per spec: "The section header index of the associated string table." + elif sect_type == linux_constants.SHT_SYMTAB: + if bits == 32: + return 16 + else: + return 24 + + return 0 + + @classmethod + def _make_section_header( + cls, + bits: int, + name_index: int, + name: str, + address: int, + size: int, + file_offset: int, + strtab_index: int, + symtab_index: int, + ) -> Optional[bytes]: + """ + Creates a section header (Elf32_Shdr or Elf64_Shdr) for the given section + """ + if bits == 32: + fmt = " Optional[bytes]: + # Bail early if bad address sent in + try: + hasattr(module.sect_attrs, "nsections") + except exceptions.InvalidAddressException: + vollog.debug(f"module at offset {module.vol.offset:#x} is paged out.") + return None + + # Gather sections + parse_sections_result = cls._parse_sections(context, vmlinux_name, module) + if parse_sections_result is None: + return None + updated_sections, strtab_index, symtab_index = parse_sections_result + + kernel = context.modules[vmlinux_name] + + # Figure out header sizes + if symbols.symbol_table_is_64bit(context, kernel.symbol_table_name): + header_type = "Elf64_Ehdr" + section_type = "Elf64_Shdr" + bits = 64 + else: + header_type = "Elf32_Ehdr" + section_type = "Elf32_Shdr" + bits = 32 + + header_type_size = kernel.get_type(header_type).size + section_type_size = kernel.get_type(section_type).size + + # Per Linux-spec, all LKMs must start with a null section header + # This buffer is used to hold the headers as they are built + sections_headers = b"\x00" * section_type_size + + # Holder of the data of the sections + sections_data = b"" + + # the .shstrtab section is "\x00" + section name for each section + # followed by a terminating null. + # It starts with the null string (\x00) + shstrtab_data = b"\x00" + + # Track where we end the sections and data to glue `.shstrtab` after + last_file_offset = None + last_sect_size = None + + # Start at 1 in the string table + name_index = 1 + + # Create the actual section headers + for index, (name, address, file_offset, section_data) in enumerate( + updated_sections + ): + # Make the section header + header_bytes = cls._make_section_header( + bits, + name_index, + name, + address, + len(section_data), + file_offset, + strtab_index, + symtab_index, + ) + if not header_bytes: + vollog.debug(f"make_section_header failed for section {name}") + return None + + # ndex into the string table + name_index += len(name) + 1 + + # concatenate the header and section bytes + sections_headers += header_bytes + sections_data += section_data + + # track where we are so .shstrtab goes into correct offset + last_file_offset = file_offset + last_sect_size = len(section_data) + + # append each section name to what will become .shstrtab + shstrtab_data += bytes(name, encoding="utf8") + b"\x00" + + # stick our own section reference string at end + # name_index points to the end of the last section string after the loop ends + shstrtab_data += b".shstrtab\x00" + + # create our .shstrtab section so sections have names + sections_headers += cls._make_section_header( + bits, + name_index, + ".shstrtab", + 0, + len(shstrtab_data), + last_file_offset + last_sect_size, + strtab_index, + symtab_index, + ) + + sections_data += shstrtab_data + + num_sections = len(updated_sections) + 1 + + header = cls._make_elf_header( + bits, + header_type_size + len(sections_data), + num_sections, + ) + + if not header: + vollog.error( + f"Hit error creating Elf header for module at {module.vol.offset:#x}" + ) + return None + + # Return our beautiful, hand-crafted, farm raised ELF file + return header + sections_data + sections_headers + + class ModuleGathererLsmod(ModuleGathererInterface): """ Gathers modules from the main kernel list @@ -976,7 +1714,7 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): file_name = renderers.NotApplicableValue() if dump and open_implementation: - elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( + elf_data = ModuleExtract.extract_module( context, kernel_module_name, module ) if not elf_data: diff --git a/volatility3/framework/symbols/windows/extensions/pool.py b/volatility3/framework/symbols/windows/extensions/pool.py index f12182fa7..a3bc3aefe 100644 --- a/volatility3/framework/symbols/windows/extensions/pool.py +++ b/volatility3/framework/symbols/windows/extensions/pool.py @@ -126,8 +126,7 @@ class POOL_HEADER(objects.StructType): infomask_value = infomask_data[addr + infomask_offset] pointercount_value = int.from_bytes( infomask_data[ - addr - + pointercount_offset : addr + addr + pointercount_offset : addr + pointercount_offset + pointercount_size ], @@ -165,8 +164,7 @@ class POOL_HEADER(objects.StructType): (padding_length,) = struct.unpack( "