diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index b1d15eb10..7d9d86fb0 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -18,7 +18,7 @@ jobs: matrix: python-version: ["3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 324390e43..c79f1c086 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/README.md b/README.md index cf735fc8d..79401a18f 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,17 @@ pip install -e ".[dev]" Symbol table packs for the various operating systems are available for download at: + + The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at: + + 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/development/banner_server.py b/development/banner_server.py index b62477a26..dd1df96ab 100644 --- a/development/banner_server.py +++ b/development/banner_server.py @@ -14,7 +14,6 @@ vollog = logging.getLogger(__name__) class BannerCacheGenerator: - def __init__(self, path: str, url_prefix: str): self._path = path self._url_prefix = url_prefix @@ -79,7 +78,6 @@ class BannerCacheGenerator: if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument("--path", default=os.path.dirname(__file__)) parser.add_argument( diff --git a/development/compare-vol.py b/development/compare-vol.py index a01d8e93c..ccfcd9f4a 100644 --- a/development/compare-vol.py +++ b/development/compare-vol.py @@ -208,7 +208,6 @@ class Volatility3PyPyTest(VolatilityTest): class VolatilityTester: - def __init__( self, images: List[VolatilityImage], @@ -339,7 +338,7 @@ if __name__ == "__main__": "--vol3path", type=str, default=os.path.join(os.getcwd(), "volatility3"), - help="Path ot the volatility 3 directory", + help="Path to the volatility 3 directory", ) parser.add_argument( "--vol2path", diff --git a/development/pdbparse-to-json.py b/development/pdbparse-to-json.py index 49b4da009..fe4b8ab60 100644 --- a/development/pdbparse-to-json.py +++ b/development/pdbparse-to-json.py @@ -22,7 +22,6 @@ if __name__ == "__main__": class PDBRetreiver: - def retreive_pdb(self, guid: str, file_name: str) -> Optional[str]: logger.info("Download PDB file...") file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) diff --git a/development/stock-linux-json.py b/development/stock-linux-json.py index 967cc7e18..713283e66 100644 --- a/development/stock-linux-json.py +++ b/development/stock-linux-json.py @@ -13,7 +13,6 @@ DWARF2JSON = "./dwarf2json" class Downloader: - def __init__(self, url_lists: List[List[str]]) -> None: self.url_lists = url_lists diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst index 031b49636..d0b097e0e 100644 --- a/doc/source/getting-started-linux-tutorial.rst +++ b/doc/source/getting-started-linux-tutorial.rst @@ -6,41 +6,47 @@ This guide will give you a brief overview of how volatility3 works as well as a Acquiring memory ---------------- -Volatility3 does not provide the ability to acquire memory. Below are some examples of tools that can be used to acquire memory, but more are available: +Volatility3 does not provide the ability to acquire memory. Below is an example of a tool that can be used to acquire memory on Linux systems: * `AVML - Acquire Volatile Memory for Linux `_ -* `LiME - Linux Memory Extract `_ -Be aware that LiME raw format is not supported by volatility3, the padded or lime option should be used instead. `This issue contains further information `_. +Other tools may exist, but please verify their maintenance status and compatibility with volatility3 before use. -Procedure to create symbol tables for linux --------------------------------------------- +Procedure to create symbol tables for Linux +------------------------------------------- -To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`. +It is recommended to first check the repository `volatility3-symbols `_ for pre-generated JSON.xz symbol table files. +This repository provides files organized by kernel version for popular Linux distributions such as Debian, Ubuntu, and AlmaLinux. + +If you cannot find a suitable symbol table for your kernel version there, please refer to :ref:`symbol-tables:Mac or Linux symbol tables` to create one manually. + +After creating the file, place it under the directory ``volatility3/symbols``. +Volatility3 will automatically detect and use symbol tables from this location. -.. tip:: It may be possible to locate pre-made ISF files from the `Linux ISF Server `_ , - which is built and maintained by `kevthehermit `_. - After creating the file or downloading it from the ISF server, place the file under the directory ``volatility3/symbols/linux``. - If necessary create a linux directory under the symbols directory (this will become unnecessary in future versions). Listing plugins --------------- -The following is a sample of the linux plugins available for volatility3, it is not complete and more plugins may -be added. For a complete reference, please see the volatility 3 :doc:`list of plugins `. -For plugin requests, please create an issue with a description of the requested plugin. +Volatility3 currently supports over 40 Linux-specific plugins covering a wide range of forensic analysis needs, such as process enumeration, memory-mapped file inspection, loaded modules, and kernel tracing features. + +Some representative plugins include: + +- ``linux.pslist``: Lists running processes with their PIDs and PPIDs. +- ``linux.bash``: Recovers bash command history from memory. +- ``linux.lsmod``: Displays loaded kernel modules. +- ``linux.kmsg``: Reads messages from the kernel log buffer. +- ``linux.elfs``: Lists all memory-mapped ELF files. +- ``linux.check_creds``: Checks for suspicious credential structures. +- ``linux.vmayarascan``: Scans process memory using YARA signatures. + +For a full list of supported plugins, run the following command: .. code-block:: shell-session - $ python3 vol.py --help | grep -i linux. | head -n 5 - banners.Banners Attempts to identify potential linux banners in an - linux.bash.Bash Recovers bash command history from memory. - linux.check_afinfo.Check_afinfo - linux.check_creds.Check_creds - linux.check_idt.Check_idt + $ python3 vol.py --help | grep -i linux. -.. note:: Here the command is piped to grep and head to provide the start of the list of linux plugins. +.. note:: You can also filter and inspect available plugins using more sophisticated patterns or tools like ``grep``, ``awk``, or simply explore the source under ``volatility3/framework/plugins/linux``. Using plugins @@ -60,14 +66,14 @@ banners ~~~~~~~ In this example we will be using a memory dump from the Insomni'hack teaser 2020 CTF Challenge called Getdents. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge. -Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. +Thanks go to `stuxnet `_ for providing this memory dump and `writeup `_. .. code-block:: shell-session $ python3 vol.py -f memory.vmem banners - Volatility 3 Framework 2.0.1 + Volatility 3 Framework 2.26.0 Progress: 100.00 PDB scanning finished Offset Banner @@ -79,85 +85,79 @@ Thanks go to `stuxnet `_ for providing this memo 0x7fde0010 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18) -The above command helps us to find the memory dump's kernel version and the distribution version. Now using the above banner we can search for the needed ISF file from the ISF server. -If an ISF file cannot be found then, follow the instructions on :ref:`getting-started-linux-tutorial:Procedure to create symbol tables for linux`. After that, place the ISF file under the ``volatility3/symbols/linux`` directory. +The above command helps us identify the kernel version and distribution from the memory dump. +Using this information, follow the instructions in :ref:`getting-started-linux-tutorial:Procedure to create symbol tables for linux` to generate the required ISF file. +Once created, place the file under the ``volatility3/symbols`` directory so that Volatility3 can recognize it automatically. + +linux.boottime +~~~~~~~~~~~~~~ + +This plugin provides the system boot time extracted from memory. +It is useful for establishing a timeline, particularly when analyzing incident response scenarios or determining system uptime. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.boottime + + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + + TIME NS Boot Time + + - 2022-02-10 06:50:16.450008 UTC + +This timestamp can serve as a reference point for correlating system events, such as process start times, logs, or malicious activity. -.. tip:: Use the banner text which is most repeated to search on the ISF Server. linux.pslist ~~~~~~~~~~~~ +This plugin lists active processes by walking the task list from memory. +It provides detailed metadata for each process, including identifiers and user/group information. + .. code-block:: shell-session $ python3 vol.py -f memory.vmem linux.pslist - Volatility 3 Framework 2.0.1 Stacking attempts finished + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + OFFSET (V) PID TID PPID COMM UID GID EUID EGID CREATION TIME File output - PID PPID COMM + 0x8ca6db1aac80 1 1 0 systemd 0 0 0 0 2022-02-10 06:50:16.364213 UTC Disabled + 0x8ca6db1a9640 2 2 0 kthreadd 0 0 0 0 2022-02-10 06:50:16.364213 UTC Disabled + 0x8ca6db1ac2c0 3 3 2 rcu_gp 0 0 0 0 2022-02-10 06:50:16.372213 UTC Disabled + ... - 1 0 systemd - 2 0 kthreadd - 3 2 kworker/0:0 - 4 2 kworker/0:0H - 5 2 kworker/u256:0 - 6 2 mm_percpu_wq - 7 2 ksoftirqd/0 - 8 2 rcu_sched - 9 2 rcu_bh - 10 2 migration/0 - 11 2 watchdog/0 - 12 2 cpuhp/0 - 13 2 kdevtmpfs - 14 2 netns - 15 2 rcu_tasks_kthre - 16 2 kauditd - ..... +This detailed view allows investigators to correlate user privileges, startup times, and relationships between processes more precisely than before. -``linux.pslist`` helps us to list the processes which are running, their PIDs and PPIDs. linux.pstree ~~~~~~~~~~~~ +This plugin presents the process hierarchy as a tree, clearly showing parent-child relationships between processes. .. code-block:: shell-session $ python3 vol.py -f memory.vmem linux.pstree - Volatility 3 Framework 2.0.1 + + Volatility 3 Framework 2.26.0 Progress: 100.00 Stacking attempts finished - PID PPID COMM + OFFSET (V) PID TID PPID COMM + + 0x8ca6db1aac80 1 1 0 systemd + * 0x8ca6db3342c0 278 278 1 systemd-journal + * 0x8ca6d005ac80 315 315 1 systemd-udevd + * 0x8ca6d0eac2c0 478 478 1 systemd-resolve + * ... + *** 0x8ca67108c2c0 1507 1507 1438 gdm-x-session + **** 0x8ca671215900 1527 1527 1507 Xorg + **** 0x8ca671210000 1608 1608 1507 gnome-session-b + ***** 0x8ca66fba42c0 1765 1765 1608 ssh-agent + + +It helps identify unusual or suspicious process structures such as orphaned child processes, injected children under legitimate parents, or long chains of shell execution. +The tree view is particularly useful for spotting anomalies in process launch sequences or privilege escalations by inspecting unexpected parent-child relationships. - 1 0 systemd - * 636 1 polkitd - * 514 1 acpid - * 1411 1 pulseaudio - * 517 1 rsyslogd - * 637 1 cups-browsed - * 903 1 whoopsie - * 522 1 ModemManager - * 525 1 cron - * 526 1 avahi-daemon - ** 542 526 avahi-daemon - * 657 1 unattended-upgr - * 914 1 kerneloops - * 532 1 dbus-daemon - * 1429 1 ibus-x11 - * 929 1 kerneloops - * 1572 1 gsd-printer - * 933 1 upowerd - * 1071 1 rtkit-daemon - * 692 1 gdm3 - ** 1234 692 gdm-session-wor - *** 1255 1234 gdm-x-session - **** 1257 1255 Xorg - **** 1266 1255 gnome-session-b - ***** 1537 1266 gsd-clipboard - ***** 1539 1266 gsd-color - ***** 1542 1266 gsd-datetime - ***** 2950 1266 deja-dup-monito - ***** 1546 1266 gsd-housekeepin - ***** 1548 1266 gsd-keyboard - ***** 1550 1266 gsd-media-keys -``linux.pstree`` helps us to display the parent-child relationships between processes. linux.bash ~~~~~~~~~~ @@ -168,7 +168,7 @@ Now to find the commands that were run in the bash shell by using ``linux.bash`` $ python3 vol.py -f memory.vmem linux.bash - Volatility 3 Framework 2.0.1 + Volatility 3 Framework 2.26.0 Progress: 100.00 Stacking attempts finished PID Process CommandTime Command @@ -177,17 +177,85 @@ Now to find the commands that were run in the bash shell by using ``linux.bash`` 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade - 1733 bash 2020-01-16 14:00:36.000000 sudo apt update - 1733 bash 2020-01-16 14:00:36.000000 rub - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:36.000000 uname -a - 1733 bash 2020-01-16 14:00:36.000000 uname -a - 1733 bash 2020-01-16 14:00:36.000000 sudo apt autoclean - 1733 bash 2020-01-16 14:00:36.000000 sudo reboot - 1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade 1733 bash 2020-01-16 14:00:41.000000 chmod +x meterpreter 1733 bash 2020-01-16 14:00:42.000000 sudo ./meterpreter + + +linux.ip.Addr and linux.ip.Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Network configuration is an essential aspect of memory forensics. +Analyzing the network interfaces and their IP assignments can reveal active connections, misconfigured settings, or even artifacts of malicious activity. + +Volatility3 provides the following two plugins to examine this information: + +**linux.ip.Addr** displays IP-related metadata for each interface, including IPv4/IPv6 addresses, MAC, scope, and interface status. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.ip.Addr + + NetNS Index Interface MAC Promiscuous IP Prefix Scope Type State + 4026531992 2 enp0s3 08:00:27:8a:4d:eb False 10.0.2.15 24 global UP + ... + +**linux.ip.Link** shows lower-level link information such as MTU, Qdisc, and interface flags. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.ip.Link + + NS Interface MAC State MTU Qdisc Qlen Flags + 4026531992 enp0s3 08:00:27:8a:4d:eb UP 1500 fq_codel 1000 BROADCAST,LOWER_UP,MULTICAST,UP + +Together, these plugins help investigators assess the system’s network exposure and identify anomalies such as multiple network namespaces, unexpected IP addresses, or active interfaces in promiscuous mode. + +linux.malfind +~~~~~~~~~~~~~ + +This plugin scans process memory for suspicious executable regions that may indicate code injection or malicious payloads. +It is particularly useful for detecting fileless malware, injected shellcode, or unpacked runtime payloads that do not correspond to legitimate binary files on disk. + +.. code-block:: shell-session + + $ python3 vol.py -f memory.vmem linux.malfind + + Volatility 3 Framework 2.26.0 + Progress: 100.00 Stacking attempts finished + PID Process Start End Path Protection Hexdump Disasm + + 540 networkd-dispat 0x7f1506482000 0x7f1506483000 Anonymous Mapping rwx + 00 00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 ........C....... + 4c 8d 15 f9 ff ff ff ff 25 03 00 00 00 0f 1f 00 L.......%....... + ... + 0x7f1506482000: add byte ptr [rax], al + 0x7f1506482002: add byte ptr [rax], al + ... + 0x7f1506482013: stc + +In this output: + +- **PID / Process**: Identifies the target process (in this case, `networkd-dispat`, PID 540) +- **Start / End**: The memory address range of the suspicious region +- **Path**: Indicates that the region is an anonymous memory mapping (i.e., not backed by a file) +- **Protection**: The region is marked `rwx` (read-write-execute), which is uncommon for legitimate memory regions +- **Disasm**: Shows the disassembled machine code found in that memory region + +**Key indicators to focus on:** + +- **Anonymous Mapping + rwx**: Memory that is not backed by a file and has execute permissions is often used for injected code +- **Disassembly patterns**: Repetitive `add` instructions, `nop`, or unusual instruction sequences can be artifacts of shellcode, packer stubs, or JIT-compiled code +- **Process context**: The suspicious memory is found in `networkd-dispat`, a system service — if this service is not expected to have dynamic executable memory regions, it may be compromised + +Use this plugin early in an investigation to flag processes for deeper inspection. + +Further Exploration and Contribution +------------------------------------ + +This guide has introduced several key Linux plugins available in Volatility 3 for memory forensics. +However, many more plugins are available, covering topics such as kernel modules, page cache analysis, tracing frameworks, and malware detection. + +If you identify gaps in plugin functionality or wish to extend support for a specific analysis use case, you are encouraged to contribute new plugins or enhancements. +Your insights can help shape the future of Linux memory forensics. + diff --git a/pyproject.toml b/pyproject.toml index b88ac7752..290a31abd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ dev = [ "types-jsonschema>=4.23.0,<5", ] +arrow = ["pyarrow>=17.0.0"] + test = [ "volatility3[dev]", "pytest>=8.3.3,<9", @@ -52,7 +54,7 @@ test = [ docs = [ "volatility3[dev]", "sphinx>=4.0.0,<9", - "sphinx-autodoc-typehints>=2.0.0,<3", + "sphinx-autodoc-typehints>=3.0.0,<4; python_version >= '3.11'", "sphinx-rtd-theme>=3.0.1,<4", ] diff --git a/test/plugins/linux/linux.py b/test/plugins/linux/linux.py index e39c1d15d..e20268566 100644 --- a/test/plugins/linux/linux.py +++ b/test/plugins/linux/linux.py @@ -29,7 +29,7 @@ class TestLinuxPslist: class TestLinuxCheckIdt: def test_linux_generic_check_idt(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.check_idt.Check_idt", image, volatility, python + "linux.malware.check_idt.Check_idt", image, volatility, python ) assert rc == 0 @@ -41,7 +41,7 @@ class TestLinuxCheckIdt: class TestLinuxCheckSyscall: def test_linux_generic_check_syscall(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.check_syscall.Check_syscall", image, volatility, python + "linux.malware.check_syscall.Check_syscall", image, volatility, python ) assert rc == 0 @@ -89,7 +89,7 @@ class TestLinuxProcMaps: class TestLinuxTtyCheck: def test_linux_generic_tty_check(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.tty_check.tty_check", image, volatility, python + "linux.malware.tty_check.Tty_Check", image, volatility, python ) assert rc == 0 @@ -200,7 +200,7 @@ class TestLinuxCapabilities: class TestLinuxCheckCreds: def test_linux_generic_check_creds(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.check_creds.Check_creds", image, volatility, python + "linux.malware.check_creds.Check_creds", image, volatility, python ) # linux-sample-1.bin has no processes sharing credentials. @@ -251,7 +251,7 @@ class TestLinuxKthreads: class TestLinuxMalfind: def test_linux_generic_malfind(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.malfind.Malfind", image, volatility, python + "linux.malware.malfind.Malfind", image, volatility, python ) # linux-sample-1.bin has no process memory ranges with potential injected code. @@ -426,7 +426,7 @@ class TestLinuxPageCacheInodepages: class TestLinuxCheckAfinfo: def test_linux_generic_check_afinfo(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.check_afinfo.Check_afinfo", image, volatility, python + "linux.malware.check_afinfo.Check_afinfo", image, volatility, python ) # linux-sample-1.bin has no suspicious results. @@ -438,7 +438,7 @@ class TestLinuxCheckAfinfo: class TestLinuxCheckModules: def test_linux_generic_check_modules(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.check_modules.Check_modules", image, volatility, python + "linux.malware.check_modules.Check_modules", image, volatility, python ) # linux-sample-1.bin has no suspicious results. @@ -479,7 +479,10 @@ class TestLinuxIomem: class TestLinuxKeyboardNotifiers: def test_linux_generic_keyboard_notifiers(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python + "linux.malware.keyboard_notifiers.Keyboard_notifiers", + image, + volatility, + python, ) # linux-sample-1.bin has no suspicious results for this plugin. @@ -501,7 +504,7 @@ class TestLinuxKmesg: class TestLinuxNetfilter: def test_linux_generic_netfilter(self, image, volatility, python): rc, out, _err = test_volatility.runvol_plugin( - "linux.netfilter.Netfilter", image, volatility, python + "linux.malware.netfilter.Netfilter", image, volatility, python ) # linux-sample-1.bin has no suspicious results for this plugin. @@ -525,7 +528,7 @@ class TestLinuxHiddenModules: # TODO: this check should be specific, against a distinct infected sample image = LinuxSamples.LINUX_GENERIC.value.path rc, out, _err = test_volatility.runvol_plugin( - "linux.hidden_modules.Hidden_modules", image, volatility, python + "linux.malware.hidden_modules.Hidden_modules", image, volatility, python ) # linux-sample-1.bin has no hidden modules. diff --git a/test/plugins/windows/test_scheduled_tasks.py b/test/plugins/windows/test_scheduled_tasks.py index 15d7f79a6..6f8180000 100644 --- a/test/plugins/windows/test_scheduled_tasks.py +++ b/test/plugins/windows/test_scheduled_tasks.py @@ -4,7 +4,7 @@ import traceback import unittest sys.path.insert(0, "../../volatility3") -from volatility3.plugins.windows import scheduled_tasks +from volatility3.plugins.windows.registry import scheduled_tasks class TestActionsDecoding(unittest.TestCase): @@ -101,233 +101,1815 @@ class TestTriggersDecoding(unittest.TestCase): "1808B", # fmt: off *[ - 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48, - 0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48, - 0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00, - 0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00, - 0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00, - 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00, - 0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00, - 0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00, - 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00, - 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00, - 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00, - 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00, - 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00, - 0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00, - 0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00, - 0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, - 0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, - 0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00, - 0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, - 0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00, - 0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00, - 0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00, - 0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00, - 0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00, - 0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00, - 0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00, - 0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00, - 0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00, - 0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, - 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, - 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, - 0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a, - 0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44, - 0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, - 0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00, - 0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00, - 0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00, - 0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00, - 0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00, - 0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00, - 0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - ] + 0x17, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x38, + 0x21, + 0x41, + 0x42, + 0x48, + 0x48, + 0x48, + 0x48, + 0xA0, + 0x12, + 0xA0, + 0xA4, + 0x48, + 0x48, + 0x48, + 0x48, + 0x0E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x41, + 0x00, + 0x75, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x80, + 0xF4, + 0x03, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xDD, + 0xDD, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x07, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x09, + 0x00, + 0x80, + 0x48, + 0x11, + 0xF8, + 0x36, + 0x1A, + 0xDB, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x2E, + 0xE2, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xC2, + 0x31, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xAA, + 0xAA, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xEE, + 0xEE, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDA, + 0xAF, + 0x8D, + 0x09, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0xCC, + 0xCC, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x65, + 0x00, + 0x78, + 0x00, + 0x65, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x53, + 0x00, + 0x74, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x84, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x20, + 0x00, + 0x49, + 0x00, + 0x64, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x30, + 0x00, + 0x22, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x50, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x68, + 0x00, + 0x3D, + 0x00, + 0x22, + 0x00, + 0x49, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x6E, + 0x00, + 0x65, + 0x00, + 0x74, + 0x00, + 0x20, + 0x00, + 0x45, + 0x00, + 0x78, + 0x00, + 0x70, + 0x00, + 0x6C, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x22, + 0x00, + 0x3E, + 0x00, + 0x2A, + 0x00, + 0x5B, + 0x00, + 0x53, + 0x00, + 0x79, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x65, + 0x00, + 0x6D, + 0x00, + 0x5B, + 0x00, + 0x45, + 0x00, + 0x76, + 0x00, + 0x65, + 0x00, + 0x6E, + 0x00, + 0x74, + 0x00, + 0x49, + 0x00, + 0x44, + 0x00, + 0x3D, + 0x00, + 0x32, + 0x00, + 0x5D, + 0x00, + 0x5D, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x53, + 0x00, + 0x65, + 0x00, + 0x6C, + 0x00, + 0x65, + 0x00, + 0x63, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x3E, + 0x00, + 0x3C, + 0x00, + 0x2F, + 0x00, + 0x51, + 0x00, + 0x75, + 0x00, + 0x65, + 0x00, + 0x72, + 0x00, + 0x79, + 0x00, + 0x4C, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x88, + 0x88, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x03, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x04, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x77, + 0x77, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x01, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x08, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x1C, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x01, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + 0x15, + 0x00, + 0x00, + 0x00, + 0x69, + 0xCE, + 0x28, + 0x2A, + 0xCE, + 0xD8, + 0x1F, + 0x77, + 0x37, + 0x9C, + 0xE2, + 0x44, + 0xF4, + 0x01, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x40, + 0x00, + 0x00, + 0x00, + 0x48, + 0x48, + 0x48, + 0x48, + 0x44, + 0x00, + 0x45, + 0x00, + 0x53, + 0x00, + 0x4B, + 0x00, + 0x54, + 0x00, + 0x4F, + 0x00, + 0x50, + 0x00, + 0x2D, + 0x00, + 0x45, + 0x00, + 0x33, + 0x00, + 0x38, + 0x00, + 0x38, + 0x00, + 0x44, + 0x00, + 0x38, + 0x00, + 0x50, + 0x00, + 0x5C, + 0x00, + 0x41, + 0x00, + 0x64, + 0x00, + 0x6D, + 0x00, + 0x69, + 0x00, + 0x6E, + 0x00, + 0x69, + 0x00, + 0x73, + 0x00, + 0x74, + 0x00, + 0x72, + 0x00, + 0x61, + 0x00, + 0x74, + 0x00, + 0x6F, + 0x00, + 0x72, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ], # fmt: on ) triggers = scheduled_tasks.TriggerSet.decode(buf) diff --git a/test/plugins/windows/windows.py b/test/plugins/windows/windows.py index 4272b64d2..ff31ac109 100644 --- a/test/plugins/windows/windows.py +++ b/test/plugins/windows/windows.py @@ -4,6 +4,7 @@ import json import os import shutil import tempfile + from test import WindowsSamples, test_volatility @@ -59,6 +60,16 @@ class TestWindowsPslist: assert test_volatility.match_output_row(expected_row, json.loads(out)) +class TestWindowsTimeliner: + def test_windows_specific_timeliner(self, volatility, python): + image = WindowsSamples.WINDOWSXP_GENERIC.value.path + rc, out, _err = test_volatility.runvol_plugin( + "timeliner.Timeliner", image, volatility, python + ) + assert rc == 0 + assert out.count(b"\n") > 10 + + class TestWindowsPsscan: def test_windows_specific_psscan(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path @@ -427,7 +438,7 @@ class TestWindowsVadyarascan: class TestWindowsAmcache: def test_windows_generic_amcache(self, volatility, python, image): rc, out, _err = test_volatility.runvol_plugin( - "windows.amcache.Amcache", + "windows.registry.amcache.Amcache", image, volatility, python, @@ -482,7 +493,7 @@ class TestWindowsBigPools: # class TestWindowsCachedump: # def test_windows_generic_cachedump(self, volatility, python, image): # rc, out, _err = test_volatility.runvol_plugin( -# "windows.cachedump.Cachedump", +# "windows.registry.cachedump.Cachedump", # image, # volatility, # python, @@ -772,19 +783,19 @@ class TestWindowsSymlinkScan: assert test_volatility.count_entries_flat(json_out) > 5 expected_rows = [ { - "CreateTime": "2005-06-25T16:47:28+00:00", - "From Name": "AUX", - "Offset": 453082584, - "To Name": "\\DosDevices\\COM1", - "__children": [] + "CreateTime": "2005-06-25T16:47:28+00:00", + "From Name": "AUX", + "Offset": 453082584, + "To Name": "\\DosDevices\\COM1", + "__children": [], }, { - "CreateTime": "2005-06-25T16:47:28+00:00", - "From Name": "UNC", - "Offset": 453176664, - "To Name": "\\Device\\Mup", - "__children": [] - } + "CreateTime": "2005-06-25T16:47:28+00:00", + "From Name": "UNC", + "Offset": 453176664, + "To Name": "\\Device\\Mup", + "__children": [], + }, ] for expected_row in expected_rows: @@ -795,7 +806,7 @@ class TestWindowsLdrModules: def test_windows_specific_ldrmodules(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path rc, out, _err = test_volatility.runvol_plugin( - "windows.ldrmodules.LdrModules", + "windows.malware.ldrmodules.LdrModules", image, volatility, python, @@ -810,7 +821,7 @@ class TestWindowsLsadump: def test_windows_specific_lsadump(self, volatility, python): image = WindowsSamples.WINDOWSXP_GENERIC.value.path rc, out, _err = test_volatility.runvol_plugin( - "windows.lsadump.Lsadump", + "windows.registry.lsadump.Lsadump", image, volatility, python, diff --git a/test/renderers/__init__.py b/test/renderers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/renderers/test_parquet_renderers.py b/test/renderers/test_parquet_renderers.py new file mode 100644 index 000000000..c9c07b2db --- /dev/null +++ b/test/renderers/test_parquet_renderers.py @@ -0,0 +1,151 @@ +import io +import pytest +from abc import ABC, abstractmethod +from test import test_volatility + +HAS_PYARROW = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + import pyarrow.compute as pc + + HAS_PYARROW = True +except ImportError: + # The user doesn't have pyarrow installed, but HAS_PYARROW will be false so just continue + pass + + +@pytest.mark.skipif(not HAS_PYARROW, reason="pyarrow not installed") +class TestArrowRendererBase(ABC): + """Base class for testing Arrow-based renderers. + + Re-implements Windows and Linux plugin tests using PyArrow operations + instead of text-based assertions. + """ + + renderer_format = None # Override in subclasses + + @abstractmethod + def _get_table_from_output(self, output_bytes) -> "pa.Table": + """Parse output bytes into Arrow table. Override in subclasses.""" + + def test_windows_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "system" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "csrss.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("ImageFileName")), "svchost.exe" + ) + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) + + def test_linux_generic_pslist(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.pslist.PsList", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 10 + + init_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "init") + ) + systemd_rows = table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "systemd") + ) + assert (init_rows.num_rows > 0) or (systemd_rows.num_rows > 0) + + assert ( + table.filter( + pc.match_substring(pc.utf8_lower(table.column("COMM")), "watchdog") + ).num_rows + > 0 + ) + assert ( + table.filter(pc.greater(table.column("PID"), 0)).num_rows == table.num_rows + ) + + def test_windows_generic_handles(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "windows.handles.Handles", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + pluginargs=("--pid", "4"), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 500 + assert ( + table.filter( + pc.match_substring( + pc.utf8_lower(table.column("Name")), "machine\\system" + ) + ).num_rows + > 0 + ) + + def test_linux_generic_lsof(self, volatility, python, image): + rc, out, _err = test_volatility.runvol_plugin( + "linux.lsof.Lsof", + image, + volatility, + python, + globalargs=("-r", self.renderer_format), + ) + assert rc == 0 + + table = self._get_table_from_output(out) + assert table.num_rows > 35 + + +class TestParquetRenderer(TestArrowRendererBase): + renderer_format = "parquet" + + def _get_table_from_output(self, output_bytes): + return pq.read_table(io.BytesIO(output_bytes)) + + +class TestArrowRenderer(TestArrowRendererBase): + renderer_format = "arrow" + + def _get_table_from_output(self, output_bytes): + return pa.ipc.open_stream(io.BytesIO(output_bytes)).read_all() diff --git a/test/volatility3_code_analysis.py b/test/volatility3_code_analysis.py index 100ad3074..43fed459b 100644 --- a/test/volatility3_code_analysis.py +++ b/test/volatility3_code_analysis.py @@ -82,7 +82,6 @@ class CodeViolation(metaclass=abc.ABCMeta): class UnrequiredVersionableUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -107,7 +106,6 @@ class UnrequiredVersionableUsage(CodeViolation): class DirectVolatilityImportUsage(CodeViolation): - def __init__( self, module: types.ModuleType, @@ -174,8 +172,11 @@ class ModuleVisitor(NodeVisitor): """ if ( node.module - and node.module.startswith("volatility3.") # Give a pass to volatility3 module - and node.module != "volatility3.framework.constants._version" # make an exception for this + and node.module.startswith( + "volatility3." + ) # Give a pass to volatility3 module + and node.module + != "volatility3.framework.constants._version" # make an exception for this ): for name in node.names: try: @@ -204,7 +205,6 @@ class ModuleVisitor(NodeVisitor): def enter_ImportFrom(self, node: ast.ImportFrom): self._check_vol3_import_from(node) - def enter_ClassDef(self, node: ast.ClassDef) -> Any: logger.debug("Entering class %s", node.name) clazz = None diff --git a/volatility3/__init__.py b/volatility3/__init__.py index 94a6721e1..9867f5d94 100644 --- a/volatility3/__init__.py +++ b/volatility3/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 - An open-source memory forensics framework""" + import inspect import sys from importlib import abc diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index cc97c4fcb..15e6cc7b4 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -10,6 +10,7 @@ User interfaces make use of the framework to: * run the plugin * display the results """ + import argparse import inspect import io @@ -106,13 +107,6 @@ class CommandLine: volatility3.framework.require_interface_version(2, 0, 0) - renderers = dict( - [ - (x.name.lower(), x) - for x in framework.class_subclasses(text_renderer.CLIRenderer) - ] - ) - # Load up system defaults delayed_logs, default_config = self.load_system_defaults("vol.json") @@ -193,14 +187,6 @@ class CommandLine: default=False, action="store_true", ) - parser.add_argument( - "-r", - "--renderer", - metavar="RENDERER", - help=f"Determines how to render the output ({', '.join(list(renderers))})", - default="quick", - choices=list(renderers), - ) parser.add_argument( "-f", "--file", @@ -270,11 +256,6 @@ class CommandLine: known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"] partial_args, _ = parser.parse_known_args(known_args) - banner_output = sys.stdout - if renderers[partial_args.renderer].structured_output: - banner_output = sys.stderr - banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") - ### Start up logging if partial_args.log: file_logger = logging.FileHandler(partial_args.log) @@ -346,6 +327,24 @@ class CommandLine: plugin_list = framework.list_plugins() + # Discover renderers after plugin directories are loaded + # This allows custom renderers to be found in plugin directories + renderers = dict( + [ + (x.name.lower(), x) + for x in framework.class_subclasses(text_renderer.CLIRenderer) + ] + ) + + parser.add_argument( + "-r", + "--renderer", + metavar="RENDERER", + help=f"Determines how to render the output ({', '.join(list(renderers))})", + default="quick", + choices=list(renderers), + ) + seen_automagics = set() chosen_configurables_list = {} for amagic in automagics: @@ -392,6 +391,13 @@ class CommandLine: # before all the plugins have been added argcomplete.autocomplete(parser) args = parser.parse_args() + + # Display banner - redirect to stderr if using structured output + banner_output = sys.stdout + if renderers[args.renderer].structured_output: + banner_output = sys.stderr + banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n") + if args.plugin is None: parser.error( f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options" @@ -453,8 +459,9 @@ class CommandLine: raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value @@ -569,7 +576,7 @@ class CommandLine: delayed_logs.append( ( logging.DEBUG, - f"Loaded configuration: {json.dumps(result, indent = 2, sort_keys = True)}", + f"Loaded configuration: {json.dumps(result, indent=2, sort_keys=True)}", ) ) return delayed_logs, result @@ -758,7 +765,7 @@ class CommandLine: constants.LOGLEVEL_VVVV, ] ): - logging.addLevelName(level_value, f"DETAIL {level+1}") + logging.addLevelName(level_value, f"DETAIL {level + 1}") def file_handler_class_factory(self, direct=True): output_dir = self.output_dir diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index 044f33ed1..d55201371 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -49,7 +49,7 @@ def hex_bytes_as_text(value: bytes, width: int = 16) -> str: output += "\n" printables = "" - # Handle leftovers when the length is not mutiple of width + # Handle leftovers when the length is not a multiple of width if printables: padding = width - len(printables) output += " " * padding @@ -182,7 +182,7 @@ class LayerDataRenderer(CLITypeRenderer): output += "\n" printables = "" - # Handle leftovers when the length is not mutiple of width + # Handle leftovers when the length is not a multiple of width if printables: padding = self.width - len(printables) output += " " * padding @@ -278,7 +278,6 @@ class CLIRenderer(interfaces.renderers.Renderer): class QuickTextRenderer(CLIRenderer): - name = "quick" def get_render_options(self): @@ -348,7 +347,6 @@ class NoneRenderer(CLIRenderer): class CSVRenderer(CLIRenderer): - name = "csv" structured_output = True diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index 0affe5d59..5bc29f220 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -344,8 +344,9 @@ class VolShell(cli.CommandLine): raise ValueError( "Invalid extension (extensions must be of the format \"conf.path.value='value'\")" ) - address, value = extension[: extension.find("=")], json.loads( - extension[extension.find("=") + 1 :] + address, value = ( + extension[: extension.find("=")], + json.loads(extension[extension.find("=") + 1 :]), ) ctx.config[address] = value diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index e68b0334e..2a8039ff2 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -469,7 +469,7 @@ class Volshell(interfaces.plugins.PluginInterface): and dereference_count < MAX_DEREFERENCE_COUNT ): # before defreerencing the pointer, show it's information - print(f'{" " * dereference_count}{self._display_simple_type(volobject)}') + print(f"{' ' * dereference_count}{self._display_simple_type(volobject)}") # check that we can follow the pointer before dereferencing and do not # attempt to follow null pointers. @@ -485,8 +485,8 @@ class Volshell(interfaces.plugins.PluginInterface): return if hasattr(volobject.vol, "members"): - # display the header for this object, if the orginal object was just a type string, display the type information - struct_header = f'{" " * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)' + # display the header for this object, if the original object was just a type string, display the type information + struct_header = f"{' ' * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)" if isinstance(object, str) and offset is None: suffix = ":" else: @@ -523,7 +523,7 @@ class Volshell(interfaces.plugins.PluginInterface): len_typename = len(member_type_name) if len(member_type_name) > MAX_TYPENAME_DISPLAY_LENGTH: len_typename = MAX_TYPENAME_DISPLAY_LENGTH - member_type_name = f"{member_type_name[:len_typename - 3]}..." + member_type_name = f"{member_type_name[: len_typename - 3]}..." if isinstance(volobject, interfaces.objects.ObjectInterface): # We're an instance, so also display the data @@ -558,7 +558,7 @@ class Volshell(interfaces.plugins.PluginInterface): ) else: # simple type with no members, only one line to print - # if the orginal object was just a type string, display the type information + # if the original object was just a type string, display the type information if isinstance(object, str) and offset is None: print(self._display_simple_type(volobject, include_value=False)) diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 0bbdefa43..6943598cf 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Volatility 3 framework.""" + # Check the python version to ensure it's suitable import glob import sys @@ -13,7 +14,7 @@ import os import traceback from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar -from volatility3.framework import constants, interfaces +from volatility3.framework import constants, interfaces, versionutils if ( sys.version_info.major != constants.REQUIRED_PYTHON_VERSION[0] @@ -48,19 +49,13 @@ vollog = logging.getLogger(__name__) def require_interface_version(*args) -> None: """Checks the required version of a plugin.""" - if len(args): - if args[0] != interface_version()[0]: - raise RuntimeError( - f"Framework interface version {interface_version()[0]} is incompatible with required version {args[0]}" + if not versionutils.matches_required(args, interface_version()): + raise RuntimeError( + "Framework interface version {} is incompatible with required version {}".format( + ".".join(str(x) for x in interface_version()[0:2]), + ".".join(str(x) for x in args[0:2]), ) - if len(args) > 1: - if args[1] > interface_version()[1]: - raise RuntimeError( - "Framework interface version {} is an older revision than the required version {}".format( - ".".join(str(x) for x in interface_version()[0:2]), - ".".join(str(x) for x in args[0:2]), - ) - ) + ) class NonInheritable: @@ -218,4 +213,4 @@ def clear_cache(complete=True): os.unlink(cache_filename) os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)) except FileNotFoundError: - vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache") + vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existent cache") diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 55b9b81e1..c03db7582 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -7,6 +7,7 @@ from loaded PE files. This module contains a standalone scanner, and also a :class:`~volatility3.framework.interfaces.layers.ScannerInterface` based scanner for use within the framework by calling :func:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan`. """ + import contextlib import logging import math @@ -449,7 +450,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface): while (kernel_base + 0x2000000) > kernel_hint: for i in range(0, 0x200000, 0x1000): valid_kernel = self.check_kernel_offset( - context, vlayer, kernel_base, progress_callback + context, vlayer, kernel_base + i, progress_callback ) if valid_kernel: return valid_kernel diff --git a/volatility3/framework/automagic/stacker.py b/volatility3/framework/automagic/stacker.py index 596864264..2e9875086 100644 --- a/volatility3/framework/automagic/stacker.py +++ b/volatility3/framework/automagic/stacker.py @@ -153,8 +153,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface): constructor(context, config_path, requirement) # Stash the changed config items - self._cached = context.config.get(path, None), context.config.branch( - path + self._cached = ( + context.config.get(path, None), + context.config.branch(path), ) vollog.debug( f"physical_layer maximum_address: {physical_layer.maximum_address}" diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index cd1a348a4..327575e96 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -507,7 +507,7 @@ def load_cache_manager(cache_file: Optional[str] = None) -> CacheManagerInterfac cache_file = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME) # Different implementations of cache if not os.path.exists(cache_file): - raise ValueError("Non-existant cache file provided") + raise ValueError("Non-existent cache file provided") with open(cache_file, "rb") as fp: header = fp.read(4) if header not in [b"SQLi"]: diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 52296f5ad..7d56b01b3 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -26,6 +26,7 @@ The self-referential indices for older versions of windows are listed below: | x64 | 0x1ED | +--------------+-------+ """ + import logging import struct from typing import Generator, Iterable, List, Optional, Tuple, Type diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 3e3608000..b1cc716e5 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -8,13 +8,14 @@ These requirement types allow plugins to request simple information types (such as strings, integers, etc) as well as indicating what they expect to be in the context (such as particular layers or symboltables). """ + import abc import logging import os from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type from urllib import parse, request -from volatility3.framework import constants, interfaces +from volatility3.framework import constants, interfaces, deprecation, versionutils vollog = logging.getLogger(__name__) @@ -551,7 +552,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): ) -> Dict[str, interfaces.configuration.RequirementInterface]: # Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type config_path = interfaces.configuration.path_join(config_path, self.name) - if not self.matches_required(self._version, self._component.version): + if not versionutils.matches_required(self._version, self._component.version): return {config_path: self} recurse = True @@ -593,13 +594,14 @@ class VersionRequirement(interfaces.configuration.RequirementInterface): def matches_required( cls, required: Tuple[int, ...], version: Tuple[int, int, int] ) -> bool: - if len(required) > 0 and version[0] != required[0]: - return False - if len(required) > 1 and version[1] < required[1]: - return False - return True + return versionutils.matches_required(required, version) +@deprecation.renamed_class( + deprecated_class_name="PluginRequirement", + removal_date="2026-06-01", + message="PluginRequirement is to be deprecated. Use VersionRequirement instead.", +) class PluginRequirement(VersionRequirement): def __init__( self, 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 f45bed926..3f8c52b43 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,6 +5,7 @@ Linux-specific values that aren't found in debug symbols """ + import enum from dataclasses import dataclass diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index a000fce90..125f41f8a 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -8,6 +8,7 @@ This has been made an object to allow quick swapping and changing of contexts, to allow a plugin to act on multiple different contexts without them interfering with each other. """ + import functools import hashlib import logging @@ -287,7 +288,7 @@ class Module(interfaces.context.ModuleInterface): symbol_name: Name of the symbol (within the module) to construct native_layer_name: Name of the layer in which constructed objects are made (for pointers) absolute: whether the symbol's address is absolute or relative to the module - object_type: Override for the type from the symobl to use (or if the symbol type is missing) + object_type: Override for the type from the symbol to use (or if the symbol type is missing) """ if constants.BANG not in symbol_name: symbol_name = self.symbol_table_name + constants.BANG + symbol_name diff --git a/volatility3/framework/deprecation.py b/volatility3/framework/deprecation.py index 859a32bad..581647922 100644 --- a/volatility3/framework/deprecation.py +++ b/volatility3/framework/deprecation.py @@ -10,8 +10,7 @@ import inspect from typing import Callable, Tuple -from volatility3.framework import interfaces, exceptions -from volatility3.framework.configuration import requirements +from volatility3.framework import interfaces, exceptions, versionutils def method_being_removed(message: str, removal_date: str): @@ -70,7 +69,7 @@ def deprecated_method( interfaces.configuration.VersionableInterface, ): # SemVer check - if not requirements.VersionRequirement.matches_required( + if not versionutils.matches_required( replacement_version, replacement_base_class.version ): raise exceptions.VersionMismatchException( @@ -80,7 +79,7 @@ def deprecated_method( "This is a bug, the deprecated call needs to be removed and the caller needs to update their code to use the new method.", ) - deprecation_msg = f"Method \"{deprecated_func.__module__ + '.' + deprecated_func.__qualname__}\" is deprecated and will be removed in the first release after {removal_date}, use \"{replacement.__module__ + '.' + replacement.__qualname__}\" instead. {additional_information}" + deprecation_msg = f'Method "{deprecated_func.__module__ + "." + deprecated_func.__qualname__}" is deprecated and will be removed in the first release after {removal_date}, use "{replacement.__module__ + "." + replacement.__qualname__}" instead. {additional_information}' warnings.warn(deprecation_msg, FutureWarning) # Return the wrapped function with its original arguments return deprecated_func(*args, **kwargs) @@ -134,6 +133,15 @@ class PluginRenameClass: ), ) else: - if not attr.startswith("__"): + if attr == "run": + setattr( + cls, + attr, + method_being_removed( + removal_date=removal_date, + message=f"This plugin has been renamed, please call {replacement_class.__module__}.{replacement_class.__qualname__} rather than {deprecated_class_name}.", + )(value), + ) + elif not attr.startswith("__"): setattr(cls, attr, value) return super(PluginRenameClass).__init_subclass__(**kwargs) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index 34b41727a..3b70c5c29 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -8,6 +8,7 @@ space or symbol tables, and by layers when an address is invalid. The :class:`PagedInvalidAddressException` contains information about the size of the invalid page. """ + from typing import Callable, Dict, Optional, Tuple from volatility3.framework import interfaces @@ -161,4 +162,4 @@ class VersionMismatchException(VolatilityException): self.failure_reason = failure_reason def __str__(self): - return f"{self.source_component.__module__+ '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__+ '.' + self.target_component.__name__} {self.target_component.version} unmet." + return f"{self.source_component.__module__ + '.' + self.source_component.__qualname__}: Version {self.target_version} dependency on {self.target_component.__module__ + '.' + self.target_component.__name__} {self.target_component.version} unmet." diff --git a/volatility3/framework/interfaces/automagic.py b/volatility3/framework/interfaces/automagic.py index 4ac386fc0..744a33bab 100644 --- a/volatility3/framework/interfaces/automagic.py +++ b/volatility3/framework/interfaces/automagic.py @@ -7,6 +7,7 @@ runs. Automagic objects attempt to automatically fill configuration values that a user has not filled. """ + import logging from abc import ABCMeta from typing import Any, List, Optional, Tuple, Type, Union diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index 723f2fd46..0b71e4cb2 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -11,6 +11,7 @@ convenience functions, most notably the object constructor function, `object`, which will construct a symbol on a layer at a particular offset. """ + import collections import copy from abc import ABCMeta, abstractmethod @@ -267,7 +268,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface): symbol_name: The name of a symbol (that must be present in the module's symbol table). The symbol's associated type will be used to construct an object at the symbol's offset. native_layer_name: The native layer for objects that reference a different layer (if not the default provided during module construction) absolute: A boolean specifying whether the offset is absolute within the layer, or relative to the start of the module - object_type: Override for the type from the symobl to use (or if the symbol type is missing) + object_type: Override for the type from the symbol to use (or if the symbol type is missing) Returns: The constructed object diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index 6c4e4b419..2e328124d 100644 --- a/volatility3/framework/interfaces/layers.py +++ b/volatility3/framework/interfaces/layers.py @@ -6,6 +6,7 @@ One layer may combine other layers, map data based on the data itself, or map a procedure (such as decryption) across another layer of data. """ + import collections.abc import functools import logging diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 2d8024465..1228c4d0a 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -3,12 +3,14 @@ # """Objects are the core of volatility, and provide pythonic access to interpreted values of data from a layer.""" + import abc import collections import collections.abc import contextlib +import dataclasses import logging -from typing import Any, Dict, List, Mapping, NamedTuple, Optional +from typing import Any, Dict, List, Mapping, Optional from volatility3.framework import constants, interfaces @@ -52,7 +54,8 @@ class ReadOnlyMapping(collections.abc.Mapping): return dict(self) == dict(other) -class ObjectInformation(NamedTuple): +@dataclasses.dataclass +class ObjectInformation: """Contains common information useful/pertinent only to an individual object (like an instance) @@ -71,12 +74,12 @@ class ObjectInformation(NamedTuple): size: Optional[int] = None def __getitem__(self, key): - if key in self._fields: + if key in self: return getattr(self, key) - raise KeyError(f"NamedTuple does not have a key {key}") + raise KeyError(f"No {key} present in ObjectInformation") def __contains__(self, key): - return key in self._fields + return key in [field.name for field in dataclasses.fields(self)] class ObjectInterface(metaclass=abc.ABCMeta): diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 925be72c9..1159fd290 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """Symbols provide structural information about a set of bytes.""" + import bisect import collections.abc from abc import ABC, abstractmethod diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index 2e5572192..7c052f70a 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -6,6 +6,7 @@ The user of the file doesn't have to worry about the compression, but random access is not allowed.""" + import ctypes import logging import struct diff --git a/volatility3/framework/layers/codecs/__init__.py b/volatility3/framework/layers/codecs/__init__.py index e019bcbcd..5b9f2602b 100644 --- a/volatility3/framework/layers/codecs/__init__.py +++ b/volatility3/framework/layers/codecs/__init__.py @@ -2,7 +2,4 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""Codecs used for encoding or decoding data should live here - - -""" +"""Codecs used for encoding or decoding data should live here""" diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 1069b7f6d..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}, @@ -136,7 +136,7 @@ class Intel(linear.LinearlyMappedLayer): return bool(entry & (1 << 6)) def canonicalize(self, addr: int) -> int: - """Canonicalizes an address by performing an appropiate sign extension on the higher addresses""" + """Canonicalizes an address by performing an appropriate sign extension on the higher addresses""" if self._bits_per_register <= self._maxvirtaddr: return addr & self.address_mask elif addr < (1 << self._maxvirtaddr - 1): @@ -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 @@ -315,7 +344,13 @@ class Intel(linear.LinearlyMappedLayer): ): # The block isn't contiguous if stashed_offset is not None: - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) # Update all the stashed values after output stashed_offset = offset stashed_mapped_offset = mapped_offset @@ -334,7 +369,13 @@ class Intel(linear.LinearlyMappedLayer): and stashed_mapped_size is not None and stashed_map_layer is not None ): - yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer + yield ( + stashed_offset, + stashed_size, + stashed_mapped_offset, + stashed_mapped_size, + stashed_map_layer, + ) def _mapping( self, offset: int, length: int, ignore_errors: bool = False @@ -359,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 ) @@ -374,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: @@ -417,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) @@ -437,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/layers/msf.py b/volatility3/framework/layers/msf.py index 2b4fae963..a7bf6466e 100644 --- a/volatility3/framework/layers/msf.py +++ b/volatility3/framework/layers/msf.py @@ -234,9 +234,13 @@ class PdbMSFStream(linear.LinearlyMappedLayer): layer_name=self.name, invalid_address=offset + returned ) else: - yield offset + returned, chunk_size, ( - self._pages[page] * page_size - ) + page_position, chunk_size, self._base_layer + yield ( + offset + returned, + chunk_size, + (self._pages[page] * page_size) + page_position, + chunk_size, + self._base_layer, + ) returned += chunk_size length -= chunk_size diff --git a/volatility3/framework/layers/resources.py b/volatility3/framework/layers/resources.py index 6121c2cff..66fb617af 100644 --- a/volatility3/framework/layers/resources.py +++ b/volatility3/framework/layers/resources.py @@ -305,8 +305,9 @@ class JarHandler(VolatilityHandler): def default_open(req: urllib.request.Request) -> Optional[Any]: """Handles the request if it's the jar scheme.""" if req.type == "jar": - subscheme, remainder = req.full_url.split(":")[1], ":".join( - req.full_url.split(":")[2:] + subscheme, remainder = ( + req.full_url.split(":")[1], + ":".join(req.full_url.split(":")[2:]), ) if subscheme != "file": vollog.log( diff --git a/volatility3/framework/layers/segmented.py b/volatility3/framework/layers/segmented.py index 9825ae15c..96b9618dc 100644 --- a/volatility3/framework/layers/segmented.py +++ b/volatility3/framework/layers/segmented.py @@ -129,7 +129,13 @@ class NonLinearlySegmentedLayer( return None # Crop it to the amount we need left chunk_size = min(size, length + offset - logical_offset) - yield logical_offset, chunk_size, mapped_offset, mapped_size, self._base_layer + yield ( + logical_offset, + chunk_size, + mapped_offset, + mapped_size, + self._base_layer, + ) current_offset += chunk_size # Terminate if we've gone (or reached) our required limit if current_offset >= offset + length: diff --git a/volatility3/framework/layers/vmware.py b/volatility3/framework/layers/vmware.py index 39fb21b63..5dc00f344 100644 --- a/volatility3/framework/layers/vmware.py +++ b/volatility3/framework/layers/vmware.py @@ -65,10 +65,10 @@ class VmwareLayer(segmented.SegmentedLayer): data = meta_layer.read(0, header_size) magic, unknown, groupCount = struct.unpack(self.header_structure, data) if magic not in [ - b"\xD0\xBE\xD2\xBE", - b"\xD1\xBA\xD1\xBA", - b"\xD2\xBE\xD2\xBE", - b"\xD3\xBE\xD3\xBE", + b"\xd0\xbe\xd2\xbe", + b"\xd1\xba\xd1\xba", + b"\xd2\xbe\xd2\xbe", + b"\xd3\xbe\xd3\xbe", ]: raise VmwareFormatException( self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}" diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index ef702060c..59ac0ee55 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -152,7 +152,7 @@ def bytes_to_decoded_string( """ Args: data: The `bytes` buffer containing the string of a string at offset 0 - encoding: An encoding value for the encoding paramater of `bytes.decode` + encoding: An encoding value for the encoding parameter of `bytes.decode` errors: An errors value for the errors parameter of `bytes.decode` return_truncated: Dictates whether truncated strings should be returned or if a ValueError should be thrown if a truncated (broken) string was decoded diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index d4e6e2aa8..eea39206d 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -60,8 +60,9 @@ class Banners(interfaces.plugins.PluginInterface): not in b" #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~" ] if not failed: - yield format_hints.Hex(offset), str( - data, encoding="latin-1", errors="?" + yield ( + format_hints.Hex(offset), + str(data, encoding="latin-1", errors="?"), ) def run(self): diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 1c2ac52e9..10f391321 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -72,9 +72,12 @@ class IsfInfo(plugins.PluginInterface): for extension in constants.ISF_EXTENSIONS: # By ending with an extension (and therefore, not /), we should not return any directories if name.endswith(extension): - yield "jar:file:" + str( - pathlib.Path(base_name) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(base_name)) + + "!" + + name + ) else: for extension in constants.ISF_EXTENSIONS: diff --git a/volatility3/framework/plugins/layerwriter.py b/volatility3/framework/plugins/layerwriter.py index 10e7a7a72..bcc999aba 100644 --- a/volatility3/framework/plugins/layerwriter.py +++ b/volatility3/framework/plugins/layerwriter.py @@ -62,7 +62,7 @@ class LayerWriter(plugins.PluginInterface): Args: context: the context from which to read the memory layer layer_name: the name of the layer to write out - preferred_name: a string with the preferred filename for hte file + preferred_name: a string with the preferred filename for the file chunk_size: an optional size for the chunks that should be written (defaults to 0x500000) open_method: class for creating FileHandler context managers progress_callback: an optional function that takes a percentage and a string that displays output diff --git a/volatility3/framework/plugins/linux/capabilities.py b/volatility3/framework/plugins/linux/capabilities.py index dae6aac6a..364047893 100644 --- a/volatility3/framework/plugins/linux/capabilities.py +++ b/volatility3/framework/plugins/linux/capabilities.py @@ -29,7 +29,7 @@ class TaskData: @dataclass class CapabilitiesData: - """Stores each set of capabilties for a task""" + """Stores each set of capabilities for a task""" cap_inheritable: interfaces.objects.ObjectInterface cap_permitted: interfaces.objects.ObjectInterface diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 47da21615..3f9e14161 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -1,215 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""A module containing a plugin that verifies the operation function -pointers of network protocols.""" import logging -from typing import List, Tuple, Generator - -from volatility3.framework import exceptions, interfaces -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import format_hints +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_afinfo vollog = logging.getLogger(__name__) -class Check_afinfo(plugins.PluginInterface): - """Verifies the operation function pointers of network protocols.""" +class Check_afinfo( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_afinfo.Check_afinfo, + removal_date="2026-06-07", +): + """Verifies the operation function pointers of network protocols (deprecated).""" _version = (1, 0, 0) _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - ] - - @classmethod - def _check_members( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - var_ops: interfaces.objects.ObjectInterface, - var_name: str, - members: List[str], - ) -> Generator[Tuple[str, str, int], None, None]: - """ - Yields any members that are not pointing inside the kernel - """ - - vmlinux = context.modules[vmlinux_name] - - for check in members: - # redhat-specific garbage - if check.startswith("__UNIQUE_ID_rh_kabi_hide"): - continue - - # These structures have members like `write` and `next`, which are built in Python functions - addr = var_ops.member(attr=check) - - # Unimplemented handlers are set to 0 - if not addr: - continue - - if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0: - yield var_name, check, addr - - @classmethod - def _check_pre_4_18_ops( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - var_name: str, - var: interfaces.objects.ObjectInterface, - op_members: List[str], - seq_members: List[str], - ): - """ - Finds the correct way to reference `op_members` - """ - vmlinux = context.modules[vmlinux_name] - - if var.has_member("seq_fops"): - yield from cls._check_members( - context, vmlinux_name, var.seq_fops, var_name, op_members - ) - # newer kernels - if var.has_member("seq_ops"): - yield from cls._check_members( - context, vmlinux_name, var.seq_ops, var_name, seq_members - ) - - # this is the most commonly hooked member by rootkits, so a force a check on it - elif var.has_member("seq_show"): - if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0: - yield var_name, "show", var.seq_show - else: - raise exceptions.VolatilityException( - "_check_afinfo_pre_4_18: Unable to find sequence operations members for checking." - ) - - @classmethod - def _check_afinfo_pre_4_18( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - seq_members: str, - ) -> Generator[Tuple[str, str, int], None, None]: - """ - Checks the operations structures for network protocols of < 4.18 systems - """ - tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) - udp = ( - "udp_seq_afinfo", - [ - "udplite6_seq_afinfo", - "udp6_seq_afinfo", - "udplite4_seq_afinfo", - "udp4_seq_afinfo", - ], - ) - protocols = [tcp, udp] - - vmlinux = context.modules[vmlinux_name] - - op_members = vmlinux.get_type("file_operations").members - - # loop through all symbols - for struct_type, global_vars in protocols: - for global_var_name in global_vars: - # this will lookup fail for the IPv6 protocols on kernels without IPv6 support - try: - global_var = vmlinux.object_from_symbol(global_var_name) - except exceptions.SymbolError: - continue - - yield from cls._check_pre_4_18_ops( - context, - vmlinux_name, - global_var_name, - global_var, - op_members, - seq_members, - ) - - @classmethod - def _check_afinfo_post_4_18( - cls, - context: interfaces.context.ContextInterface, - vmlinux_name: str, - seq_members: str, - ) -> Generator[Tuple[str, str, int], None, None]: - """ - Checks the operations structures for network protocols of >= 4.18 systems - """ - vmlinux = context.modules[vmlinux_name] - - ops_structs = [ - "raw_seq_ops", - "udp_seq_ops", - "arp_seq_ops", - "unix_seq_ops", - "udp6_seq_ops", - "raw6_seq_ops", - "tcp_seq_ops", - "tcp4_seq_ops", - "tcp6_seq_ops", - "packet_seq_ops", - ] - - for protocol_ops_var in ops_structs: - # These will fail if the particular kernel doesn't have support for a protocol like IPv6 - try: - protocol_ops = vmlinux.object_from_symbol(protocol_ops_var) - except exceptions.SymbolError: - continue - - yield from cls._check_members( - context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members - ) - - @classmethod - def check_afinfo( - cls, context: interfaces.context.ContextInterface, vmlinux_name - ) -> Generator[Tuple[str, str, int], None, None]: - """ - Walks the network protocol operations structures for common network protocols. - Reports any initialized operations members that do not point inside the kernel. - """ - vmlinux = context.modules[vmlinux_name] - - type_check = vmlinux.get_type("tcp_seq_afinfo") - if type_check.has_member("seq_fops"): - checker = cls._check_afinfo_pre_4_18 - else: - checker = cls._check_afinfo_post_4_18 - - seq_members = vmlinux.get_type("seq_operations").members - - yield from checker(context, vmlinux_name, seq_members) - - def _generator(self): - """ - A simple wrapper around `check_afino` - """ - for name, member, address in self.check_afinfo( - self.context, self.config["kernel"] - ): - yield 0, (name, member, format_hints.Hex(address)) - - def run(self): - return renderers.TreeGrid( - [ - ("Symbol Name", str), - ("Member", str), - ("Handler Address", format_hints.Hex), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index e2b84d679..6c2c6f3d5 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -1,71 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import logging +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_creds -from volatility3.framework import interfaces, renderers -from volatility3.framework.renderers import format_hints -from volatility3.framework.configuration import requirements -from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) -class Check_creds(interfaces.plugins.PluginInterface): - """Checks if any processes are sharing credential structures""" +class Check_creds( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_creds.Check_creds, + removal_date="2026-06-07", +): + """Checks if any processes are sharing credential structures (deprecated).""" _required_framework_version = (2, 0, 0) _version = (2, 0, 2) - - @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) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - type_task = vmlinux.get_type("task_struct") - - if not type_task.has_member("cred"): - raise TypeError( - "This plugin requires the task_struct structure to have a cred member. " - "This member is not present in the supplied symbol table. " - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - creds = {} - - tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) - - for task in tasks: - task_cred_ptr = task.cred - if not (task_cred_ptr and task_cred_ptr.is_readable()): - continue - - cred_addr = task_cred_ptr.dereference().vol.offset - - creds.setdefault(cred_addr, []) - creds[cred_addr].append(task.pid) - - for cred_addr, pids in creds.items(): - if len(pids) > 1: - pid_str = ", ".join(str(pid) for pid in pids) - - fields = [ - format_hints.Hex(cred_addr), - pid_str, - ] - yield (0, fields) - - def run(self): - headers = [ - ("CredVAddr", format_hints.Hex), - ("PIDs", str), - ] - return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index e199d98d3..449f85e1e 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -1,168 +1,20 @@ # This file is Copyright 2020 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 typing import List, Optional - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, renderers, symbols -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_idt vollog = logging.getLogger(__name__) -class Check_idt(interfaces.plugins.PluginInterface): - """Checks if the IDT has been altered""" +class Check_idt( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_idt.Check_idt, + removal_date="2026-06-07", +): + """Checks if the IDT has been altered (deprecated).""" _required_framework_version = (2, 0, 0) - - # 2.0.0 - Add versioning at all, add `get_idt_type` _version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherers", - component=linux_utilities_modules.ModuleGatherers, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - ] - - @staticmethod - def get_idt_type(context, vmlinux_name) -> Optional[str]: - """ - Determines the IDT type for this symbol table or returns None - - The original version ended clauses with an `else` leading to bad fall through - of returning a type that did not exist in the symbol table. - - Future updates should not leave fall through cases to avoid this repeating. - """ - - vmlinux = context.modules[vmlinux_name] - - is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) - - # These are in a specific order. Only append to the lists going forward - # or ask Andrew to run tests before merging. - if is_32bit: - idt_types = ["gate_struct", "desc_struct", "gate_struct32"] - else: - idt_types = ["gate_struct64", "gate_struct", "idt_desc"] - - for idt_type in idt_types: - if vmlinux.has_type(idt_type): - return idt_type - - return None - - def _generator(self): - idt_type = self.get_idt_type(self.context, self.config["kernel"]) - if not idt_type: - vollog.error( - "Unable to determine the data structure type for IDT entries. Please file a bug on the GitHub tracker with your kernel version." - ) - return - - vmlinux = self.context.modules[self.config["kernel"]] - - known_modules = linux_utilities_modules.Modules.run_modules_scanners( - context=self.context, - kernel_module_name=self.config["kernel"], - caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, - ) - - idt_table_size = 256 - - kernel_layer = self.context.layers[vmlinux.layer_name] - - address_mask = kernel_layer.address_mask - - # hw handlers + system call - check_idxs = list(range(20)) + [128] - - addrs = vmlinux.object_from_symbol("idt_table") - - table = vmlinux.object( - object_type="array", - offset=addrs.vol.offset, - subtype=vmlinux.get_type(idt_type), - count=idt_table_size, - absolute=True, - ) - - for i in check_idxs: - ent = table[i] - - if not ent or not kernel_layer.is_valid(ent.vol.offset): - continue - - if hasattr(ent, "a"): - idt_addr = (ent.b & 0xFFFF0000) | (ent.a & 0x0000FFFF) - else: - low = ent.offset_low - middle = ent.offset_middle - - # offset_high is for 64bit systems - if hasattr(ent, "offset_high"): - high = ent.offset_high - else: - high = 0 - - idt_addr = (high << 32) | (middle << 16) | low - - idt_addr = idt_addr & address_mask - - # 0 means unintialized/unused, not a rootkit - if idt_addr == 0: - module_name = renderers.NotAvailableValue() - symbol_name = renderers.NotAvailableValue() - else: - module_info, symbol_name = ( - linux_utilities_modules.Modules.module_lookup_by_address( - self.context, vmlinux.name, known_modules, idt_addr - ) - ) - - if module_info: - module_name = module_info.name - else: - module_name = renderers.NotAvailableValue() - - yield ( - 0, - [ - format_hints.Hex(i), - format_hints.Hex(idt_addr), - module_name, - symbol_name or renderers.NotAvailableValue(), - ], - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Index", format_hints.Hex), - ("Address", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 7805bbd8a..d8b3ddcf1 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -1,70 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List, Dict, Generator - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules from volatility3.framework import interfaces, deprecation -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins +from volatility3.plugins.linux.malware import check_modules vollog = logging.getLogger(__name__) -class Check_modules(plugins.PluginInterface): - """Compares module list to sysfs info, if available""" +class Check_modules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_modules.Check_modules, + removal_date="2026-06-07", +): + """Compares module list to sysfs info, if available (deprecated).""" _version = (3, 0, 1) _required_framework_version = (2, 0, 0) - - @classmethod - def compare_kset_and_lsmod( - cls, context: str, vmlinux_name: str - ) -> Generator[extensions.module, None, None]: - kset_modules = linux_utilities_modules.Modules.get_kset_modules( - context=context, vmlinux_name=vmlinux_name - ) - - lsmod_modules = set( - str(utility.array_to_string(modules.name)) - for modules in linux_utilities_modules.Modules.list_modules( - context=context, vmlinux_module_name=vmlinux_name - ) - ) - - for mod_name in set(kset_modules.keys()).difference(lsmod_modules): - yield kset_modules[mod_name] - - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator - implementation = compare_kset_and_lsmod - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.VersionRequirement( - name="modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 1), - ), - requirements.VersionRequirement( - name="linux_utilities_modules_module_display_plugin", - component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), - ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() - - @classmethod - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.get_kset_modules, - removal_date="2025-09-25", - replacement_version=(3, 0, 0), - ) - def get_kset_modules( - cls, context: interfaces.context.ContextInterface, vmlinux_name: str - ) -> Dict[str, extensions.module]: - return linux_utilities_modules.Modules.get_kset_modules(context, vmlinux_name) diff --git a/volatility3/framework/plugins/linux/check_syscall.py b/volatility3/framework/plugins/linux/check_syscall.py index 724a67810..5e3e40cbb 100644 --- a/volatility3/framework/plugins/linux/check_syscall.py +++ b/volatility3/framework/plugins/linux/check_syscall.py @@ -1,214 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -"""A module containing a plugin that checks the system call table for hooks.""" -import contextlib import logging -from typing import List - -from volatility3.framework import constants, exceptions, interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import format_hints +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import check_syscall vollog = logging.getLogger(__name__) -try: - import capstone - has_capstone = True -except ImportError: - has_capstone = False - - -class Check_syscall(plugins.PluginInterface): - """Check system call table for hooks.""" +class Check_syscall( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=check_syscall.Check_syscall, + removal_date="2026-06-07", +): + """Check system call table for hooks (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - ] - - def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux): - """Returns the size of the table based on the next symbol.""" - ret = 0 - - symbol_list = [] - for sn in vmlinux.symbols: - with contextlib.suppress(exceptions.SymbolError): - # When requesting the symbol from the module, a full resolve is performed - symbol_list.append((vmlinux.get_symbol(sn).address, sn)) - sorted_symbols = sorted(symbol_list) - - sym_address = 0 - - for tmp_sym_address, sym_name in sorted_symbols: - if tmp_sym_address > table_addr: - sym_address = tmp_sym_address - break - - if sym_address > 0: - ret = int((sym_address - table_addr) / ptr_sz) - - return ret - - def _get_table_size_meta(self, vmlinux): - """returns the number of symbols that start with __syscall_meta__ this - is a fast way to determine the number of system calls, but not the most - accurate.""" - - return len( - [ - sym - for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols - if sym.startswith("__syscall_meta__") - ] - ) - - def _get_table_info_other(self, table_addr, ptr_sz, vmlinux): - table_size_meta = self._get_table_size_meta(vmlinux) - table_size_syms = self._get_table_size_next_symbol(table_addr, ptr_sz, vmlinux) - - sizes = [size for size in [table_size_meta, table_size_syms] if size > 0] - - table_size = min(sizes) - - return table_size - - def _get_table_info_disassembly(self, ptr_sz, vmlinux) -> int: - """Find the size of the system call table by disassembling functions - that immediately reference it in their first instruction This is in the - form 'cmp reg,NR_syscalls'.""" - table_size = 0 - - if not has_capstone: - return table_size - - if ptr_sz == 4: - syscall_entry_func = "sysenter_do_call" - mode = capstone.CS_MODE_32 - else: - syscall_entry_func = "system_call_fastpath" - mode = capstone.CS_MODE_64 - - md = capstone.Cs(capstone.CS_ARCH_X86, mode) - - try: - func_addr = vmlinux.get_symbol(syscall_entry_func).address - except exceptions.SymbolError: - # if we can't find the disassemble function then bail and rely on a different method - return 0 - - vmlinux = self.context.modules[self.config["kernel"]] - vmlinux_layer = self.context.layers[vmlinux.layer_name] - try: - data = vmlinux_layer.read(func_addr, 6) - except exceptions.InvalidAddressException: - return 0 - - for _address, _size, mnemonic, op_str in md.disasm_lite(data, func_addr): - if mnemonic == "CMP": - table_size = int(op_str.split(",")[1].strip()) & 0xFFFF - break - - return table_size - - def _get_table_info(self, vmlinux, table_name, ptr_sz): - table_sym = vmlinux.get_symbol(table_name) - - table_size = self._get_table_info_disassembly(ptr_sz, vmlinux) - - if table_size == 0: - table_size = self._get_table_info_other(table_sym.address, ptr_sz, vmlinux) - - if table_size == 0: - vollog.error("Unable to get system call table size") - return 0, 0 - - return table_sym.address, table_size - - # TODO - add finding and parsing unistd.h once cached file enumeration is added - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - ptr_sz = vmlinux.get_type("pointer").size - if ptr_sz == 4: - table_name = "32bit" - else: - table_name = "64bit" - - try: - table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz) - except exceptions.SymbolError: - vollog.error("Unable to find the system call table. Exiting.") - return None - - tables = [(table_name, table_info)] - - # this table is only present on 64 bit systems with 32 bit emulation - # enabled in order to support 32 bit programs and libraries - # if the symbol isn't there then the support isn't in the kernel and so we skip it - try: - ia32_symbol = vmlinux.get_symbol("ia32_sys_call_table") - except exceptions.SymbolError: - ia32_symbol = None - - if ia32_symbol is not None: - ia32_info = self._get_table_info(vmlinux, "ia32_sys_call_table", ptr_sz) - tables.append(("32bit", ia32_info)) - - for table_name, (tableaddr, tblsz) in tables: - table = vmlinux.object( - object_type="array", - subtype=vmlinux.get_type("pointer"), - offset=tableaddr, - count=tblsz, - ) - - for i in range(len(table)): - try: - call_addr = table[i] - except exceptions.InvalidAddressException: - vollog.debug(f"Failed to get system call table entry at index {i}") - continue - - symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr)) - - if len(symbols) > 0: - sym_name = ( - str(symbols[0].split(constants.BANG)[1]) - if constants.BANG in symbols[0] - else str(symbols[0]) - ) - else: - sym_name = "UNKNOWN" - - yield ( - 0, - ( - format_hints.Hex(tableaddr), - table_name, - i, - format_hints.Hex(call_addr), - sym_name, - ), - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Table Address", format_hints.Hex), - ("Table Name", str), - ("Index", int), - ("Handler Address", format_hints.Hex), - ("Handler Symbol", str), - ], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/graphics/__init__.py b/volatility3/framework/plugins/linux/graphics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/volatility3/framework/plugins/linux/graphics/fbdev.py b/volatility3/framework/plugins/linux/graphics/fbdev.py index f7cde1bf0..ebdbd1706 100644 --- a/volatility3/framework/plugins/linux/graphics/fbdev.py +++ b/volatility3/framework/plugins/linux/graphics/fbdev.py @@ -6,14 +6,9 @@ import io from dataclasses import dataclass from typing import Type, List, Dict, Tuple -from volatility3.framework import constants, exceptions, interfaces +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import ( - format_hints, - TreeGrid, - NotAvailableValue, - UnreadableValue, -) +from volatility3.framework.renderers import format_hints from volatility3.framework.objects import utility from volatility3.framework.constants import architectures from volatility3.framework.symbols import linux @@ -181,7 +176,7 @@ class Fbdev(interfaces.plugins.PluginInterface): """ kernel = context.modules[kernel_name] kernel_layer = context.layers[kernel.layer_name] - id = "N-A" if isinstance(fb.id, NotAvailableValue) else fb.id + id = "N-A" if isinstance(fb.id, renderers.NotAvailableValue) else fb.id base_filename = f"{id}_{fb.xres_virtual}x{fb.yres_virtual}_{fb.bpp}bpp" if convert_to_png_image: image_object = cls.convert_fb_raw_buffer_to_image(context, kernel_name, fb) @@ -193,9 +188,9 @@ class Fbdev(interfaces.plugins.PluginInterface): final_fb_buffer = kernel_layer.read(fb.fb_info.screen_base, fb.size) filename = f"{base_filename}.raw" - with open_method(filename) as f: - f.write(final_fb_buffer) - return f.preferred_filename + with open_method(filename) as fp: + fp.write(final_fb_buffer) + return fp.preferred_filename @classmethod def parse_fb_info( @@ -216,7 +211,7 @@ class Fbdev(interfaces.plugins.PluginInterface): - struct fb_var_screeninfo stores device independent changeable information about a frame buffer device, its current format and video mode, as well as other miscellaneous parameters. """ - id = utility.array_to_string(fb_info.fix.id) or NotAvailableValue() + id = utility.array_to_string(fb_info.fix.id) or renderers.NotAvailableValue() color_fields = None # 0 = color, 1 = grayscale, >1 = FOURCC @@ -250,7 +245,6 @@ You can try using ffmpeg to decode the raw buffer. Example usage: return fb def _generator(self): - if not has_pil: vollog.error( "PIL (pillow) module is required to use this plugin. Please install it manually or through pyproject.toml." @@ -299,14 +293,14 @@ You can try using ffmpeg to decode the raw buffer. Example usage: vollog.error( f'Layer {excp.layer_name} failed to read address {hex(excp.invalid_address)} when dumping framebuffer "{fb.id}".' ) - file_output = UnreadableValue() + file_output = renderers.UnreadableValue() try: fb_device_name = utility.pointer_to_string( fb.fb_info.dev.kobj.name, 256 ) except exceptions.InvalidAddressException: - fb_device_name = NotAvailableValue() + fb_device_name = renderers.NotAvailableValue() yield ( 0, @@ -334,7 +328,7 @@ You can try using ffmpeg to decode the raw buffer. Example usage: ("Filename", str), ] - return TreeGrid( + return renderers.TreeGrid( columns, self._generator(), ) diff --git a/volatility3/framework/plugins/linux/hidden_modules.py b/volatility3/framework/plugins/linux/hidden_modules.py index dcd602c5d..f7bdd6b0b 100644 --- a/volatility3/framework/plugins/linux/hidden_modules.py +++ b/volatility3/framework/plugins/linux/hidden_modules.py @@ -1,197 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Set, Tuple, Iterable -from volatility3.framework.symbols.linux.utilities import ( - modules as linux_utilities_modules, -) -from volatility3.framework import interfaces, exceptions, deprecation -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.linux import extensions -from volatility3.framework.interfaces import plugins +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import hidden_modules vollog = logging.getLogger(__name__) -class Hidden_modules(plugins.PluginInterface): - """Carves memory to find hidden kernel modules""" +class Hidden_modules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=hidden_modules.Hidden_modules, + removal_date="2026-06-07", +): + """Carves memory to find hidden kernel modules (deprecated).""" _required_framework_version = (2, 25, 0) _version = (3, 0, 2) - - @classmethod - def find_hidden_modules( - cls, context, vmlinux_module_name: str - ) -> extensions.module: - if context.symbol_space.verify_table_versions( - "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) - ): - raise exceptions.SymbolSpaceError( - "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" - ) - - known_module_addresses = cls.get_lsmod_module_addresses( - context, vmlinux_module_name - ) - modules_memory_boundaries = ( - linux_utilities_modules.Modules.get_modules_memory_boundaries( - context, vmlinux_module_name - ) - ) - - yield from linux_utilities_modules.Modules.get_hidden_modules( - context, - vmlinux_module_name, - known_module_addresses, - modules_memory_boundaries, - ) - - @classmethod - def get_hidden_modules( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - known_module_addresses: Set[int], - modules_memory_boundaries: Tuple, - ) -> Iterable[interfaces.objects.ObjectInterface]: - """Enumerate hidden modules by taking advantage of memory address alignment patterns - - This technique is much faster and uses less memory than the traditional scan method - in Volatility2, but it doesn't work with older kernels. - - From kernels 4.2 struct module allocation are aligned to the L1 cache line size. - In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in - the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can - also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json - doesn't support this feature yet. - In kernels < 4.2, alignment attributes are absent in the struct module, meaning - alignment cannot be guaranteed. Therefore, for older kernels, it's better to use - the traditional scan technique. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - known_module_addresses: Set with known module addresses - modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. - Yields: - module objects - """ - return linux_utilities_modules.get_hidden_modules( - vmlinux_module_name, known_module_addresses, modules_memory_boundaries - ) - - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator - implementation = find_hidden_modules - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.VersionRequirement( - name="linux_utilities_modules_module_display_plugin", - component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 1), - ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() - - @staticmethod - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, - removal_date="2025-09-25", - replacement_version=(3, 0, 0), - ) - def get_modules_memory_boundaries( - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> Tuple[int, int]: - return linux_utilities_modules.Modules.get_modules_memory_boundaries( - context, vmlinux_module_name - ) - - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.get_module_address_alignment, - removal_date="2025-09-25", - replacement_version=(3, 0, 0), - ) - @classmethod - def _get_module_address_alignment( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> int: - """Obtain the module memory address alignment. - - struct module is aligned to the L1 cache line, which is typically 64 bytes for most - common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this - will still work. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - The struct module alignment - """ - return linux_utilities_modules.get_module_address_alignment( - context, vmlinux_module_name - ) - - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.get_hidden_modules, - removal_date="2025-09-25", - replacement_version=(3, 0, 0), - ) - @staticmethod - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.validate_alignment_patterns, - removal_date="2025-09-25", - replacement_version=(3, 0, 0), - ) - def _validate_alignment_patterns( - addresses: Iterable[int], - address_alignment: int, - ) -> bool: - """Check if the memory addresses meet our alignments patterns - - Args: - addresses: Iterable with the address values - address_alignment: Number of bytes for alignment validation - - Returns: - True if all the addresses meet the alignment - """ - return linux_utilities_modules.validate_alignment_patterns( - addresses, address_alignment - ) - - @classmethod - def get_lsmod_module_addresses( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - ) -> Set[int]: - """Obtain a set the known module addresses from linux.lsmod plugin - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - vmlinux_module_name: The name of the kernel module on which to operate - - Returns: - A set containing known kernel module addresses - """ - vmlinux = context.modules[vmlinux_module_name] - vmlinux_layer = context.layers[vmlinux.layer_name] - - known_module_addresses = { - vmlinux_layer.canonicalize(module.vol.offset) - for module in linux_utilities_modules.Modules.list_modules( - context, vmlinux_module_name - ) - } - return known_module_addresses diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py index 348f3c13f..164fe62dd 100644 --- a/volatility3/framework/plugins/linux/ip.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -47,7 +47,17 @@ class Addr(plugins.PluginInterface): prefix_len = in_ifaddr.get_prefix_len() scope_type = in_ifaddr.get_scope_type() ip_addr = in_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip_addr, + prefix_len, + scope_type, + operational_state, + ) # Interface IPv6 Addresses inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev") @@ -55,7 +65,17 @@ class Addr(plugins.PluginInterface): prefix_len = inet6_ifaddr.get_prefix_len() scope_type = inet6_ifaddr.get_scope_type() ip6_addr = inet6_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type, operational_state + yield ( + net_ns_id, + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ) def _enumerate_net_namespace_list(self): vmlinux = self.context.modules[self.config["kernel"]] @@ -82,16 +102,19 @@ class Addr(plugins.PluginInterface): scope_type, operational_state, ) in self._gather_net_dev_info(net_dev): - yield 0, ( - net_ns_id or renderers.NotAvailableValue(), - iface_ifindex, - iface_name, - mac_addr, - promisc, - ip6_addr, - prefix_len, - scope_type, - operational_state, + yield ( + 0, + ( + net_ns_id or renderers.NotAvailableValue(), + iface_ifindex, + iface_name, + mac_addr, + promisc, + ip6_addr, + prefix_len, + scope_type, + operational_state, + ), ) def run(self): @@ -150,7 +173,16 @@ class Link(plugins.PluginInterface): ] flags_str = ",".join(flags_list) - yield net_ns_id or renderers.NotAvailableValue(), iface_name, mac_addr, operational_state, mtu, qdisc_name or renderers.NotAvailableValue(), qlen, flags_str + yield ( + net_ns_id or renderers.NotAvailableValue(), + iface_name, + mac_addr, + operational_state, + mtu, + qdisc_name or renderers.NotAvailableValue(), + qlen, + flags_str, + ) def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 215704350..72dcc7bad 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -1,104 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, renderers, exceptions -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import keyboard_notifiers vollog = logging.getLogger(__name__) -class Keyboard_notifiers(interfaces.plugins.PluginInterface): - """Parses the keyboard notifier call chain""" +class Keyboard_notifiers( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=keyboard_notifiers.Keyboard_notifiers, + removal_date="2026-06-07", +): + """Parses the keyboard notifier call chain (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherers", - component=linux_utilities_modules.ModuleGatherers, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - try: - knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") - except exceptions.SymbolError: - knl_addr = None - - if not knl_addr: - raise TypeError( - "This plugin requires the keyboard_notifier_list structure. " - "This structure is not present in the supplied symbol table. " - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - if not self.context.layers[vmlinux.layer_name].is_valid(knl_addr.vol.offset): - vollog.error("The head of the keyboard notifier list is paged out.") - return - - known_modules = linux_utilities_modules.Modules.run_modules_scanners( - context=self.context, - kernel_module_name=self.config["kernel"], - caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, - ) - - knl = vmlinux.object( - object_type="atomic_notifier_head", - offset=knl_addr.vol.offset, - absolute=True, - ) - - for call_back in linux.LinuxUtilities.walk_internal_list( - vmlinux, "notifier_block", "next", knl.head - ): - call_addr = call_back.notifier_call - - module_info, symbol_name = ( - linux_utilities_modules.Modules.module_lookup_by_address( - self.context, vmlinux.name, known_modules, call_addr - ) - ) - - if module_info: - module_name = module_info.name - else: - module_name = renderers.NotAvailableValue() - - yield ( - 0, - [ - format_hints.Hex(call_addr), - module_name, - symbol_name or renderers.NotAvailableValue(), - ], - ) - - def run(self): - return renderers.TreeGrid( - [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/kmsg.py b/volatility3/framework/plugins/linux/kmsg.py index 1069a312f..ba02763d0 100644 --- a/volatility3/framework/plugins/linux/kmsg.py +++ b/volatility3/framework/plugins/linux/kmsg.py @@ -551,12 +551,15 @@ class Kmsg(interfaces.plugins.PluginInterface): for facility, level, timestamp, caller, line in ABCKmsg.run_all( context=self.context, config=self.config ): - yield 0, ( - facility, - level, - timestamp, - caller or renderers.NotAvailableValue(), - line, + yield ( + 0, + ( + facility, + level, + timestamp, + caller or renderers.NotAvailableValue(), + line, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index 8ed52e3b7..e0878647f 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -4,10 +4,10 @@ """A module containing a plugin that lists loaded kernel modules.""" import logging -from typing import List, Iterable +from typing import Iterable, List import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, deprecation +from volatility3.framework import constants, deprecation, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins @@ -18,27 +18,41 @@ class Lsmod(plugins.PluginInterface): """Lists loaded kernel modules.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 1) + _version = (3, 0, 3) - run = linux_utilities_modules.ModuleDisplayPlugin.run - _generator = linux_utilities_modules.ModuleDisplayPlugin.generator implementation = linux_utilities_modules.Modules.list_modules @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), requirements.VersionRequirement( name="linux_utilities_modules_module_display_plugin", component=linux_utilities_modules.ModuleDisplayPlugin, - version=(1, 0, 0), + version=(2, 0, 0), ), - ] + linux_utilities_modules.ModuleDisplayPlugin.get_requirements() + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] @classmethod @deprecation.deprecated_method( replacement=linux_utilities_modules.Modules.list_modules, replacement_version=(3, 0, 0), - removal_date="2025-09-25", + removal_date="2026-03-25", ) def list_modules( cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str @@ -46,3 +60,18 @@ class Lsmod(plugins.PluginInterface): return linux_utilities_modules.Modules.list_modules( context, vmlinux_module_name ) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index 283eabca0..75c2a1c2b 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists open files for each processes.""" _required_framework_version = (2, 0, 0) - _version = (2, 0, 2) + _version = (2, 1, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -137,6 +137,12 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): element_type=int, optional=True, ), + requirements.BooleanRequirement( + name="files_only", + description="Include only file descriptors of type file", + optional=True, + default=False, + ), ] @classmethod @@ -145,6 +151,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): context: interfaces.context.ContextInterface, vmlinux_module_name: str, filter_func: Callable[[int], bool] = lambda _: False, + include_files_only: bool = False, ) -> Iterable[FDInternal]: """Enumerates open file descriptors in tasks @@ -167,16 +174,20 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0] fd_generator = linux.LinuxUtilities.files_descriptors_for_process( - context, linuxutils_symbol_table, task + context, linuxutils_symbol_table, task, files_only=include_files_only ) for fd_fields in fd_generator: yield FDInternal(task=task, fd_fields=fd_fields) - def _generator(self, pids, vmlinux_module_name): + def _generator(self, pids, vmlinux_module_name, include_files_only): filter_func = pslist.PsList.create_pid_filter(pids) + for fd_internal in self.list_fds( - self.context, vmlinux_module_name, filter_func=filter_func + self.context, + vmlinux_module_name, + filter_func=filter_func, + include_files_only=include_files_only, ): fd_user = fd_internal.to_user() yield (0, dataclasses.astuple(fd_user)) @@ -184,6 +195,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): def run(self): pids = self.config.get("pid", None) vmlinux_module_name = self.config["kernel"] + include_files_only = self.config.get("files_only") tree_grid_args = [ ("PID", int), @@ -201,7 +213,10 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", int), ] return renderers.TreeGrid( - tree_grid_args, self._generator(pids, vmlinux_module_name) + tree_grid_args, + self._generator( + pids, vmlinux_module_name, include_files_only=include_files_only + ), ) def generate_timeline(self): @@ -220,5 +235,9 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface): ) yield description, timeliner.TimeLinerType.CHANGED, fd_user.change_time - yield description, timeliner.TimeLinerType.MODIFIED, fd_user.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + fd_user.modification_time, + ) yield description, timeliner.TimeLinerType.ACCESSED, fd_user.access_time diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index c7e141c02..647e1531a 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -1,114 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -from typing import List, Tuple, Optional import logging -from volatility3.framework import interfaces -from volatility3.framework import renderers, symbols -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.linux import pslist +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import malfind vollog = logging.getLogger(__name__) -class Malfind(interfaces.plugins.PluginInterface): - """Lists process memory ranges that potentially contain injected code.""" +class Malfind( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=malfind.Malfind, + removal_date="2026-06-07", +): + """Lists process memory ranges that potentially contain injected code (deprecated).""" _required_framework_version = (2, 0, 0) _version = (1, 0, 3) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(4, 0, 0) - ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, - ), - ] - - def _list_injections( - self, task - ) -> Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes]: - """Generate memory regions for a process that may contain injected - code.""" - - proc_layer_name = task.add_process_layer() - if not proc_layer_name: - return None - - proc_layer = self.context.layers[proc_layer_name] - - for vma in task.mm.get_vma_iter(): - vma_name = vma.get_name(self.context, task) - vollog.debug( - f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" - ) - if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": - data = proc_layer.read(vma.vm_start, 64, pad=True) - yield vma, vma_name, data - - def _generator(self, tasks): - # determine if we're on a 32 or 64 bit kernel - vmlinux = self.context.modules[self.config["kernel"]] - is_32bit_arch = not symbols.symbol_table_is_64bit( - context=self.context, symbol_table_name=vmlinux.symbol_table_name - ) - - for task in tasks: - process_name = utility.array_to_string(task.comm) - - for vma, vma_name, data in self._list_injections(task): - if is_32bit_arch: - architecture = "intel" - else: - architecture = "intel64" - - disasm = renderers.Disassembly(data, vma.vm_start, architecture) - - yield ( - 0, - ( - task.pid, - process_name, - format_hints.Hex(vma.vm_start), - format_hints.Hex(vma.vm_end), - vma_name or renderers.NotAvailableValue(), - vma.get_protection(), - format_hints.HexBytes(data), - disasm, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Start", format_hints.Hex), - ("End", format_hints.Hex), - ("Path", str), - ("Protection", str), - ("Hexdump", format_hints.HexBytes), - ("Disasm", renderers.Disassembly), - ], - self._generator( - pslist.PsList.list_tasks( - self.context, self.config["kernel"], filter_func=filter_func - ) - ), - ) diff --git a/volatility3/framework/plugins/linux/malware/__init__.py b/volatility3/framework/plugins/linux/malware/__init__.py new file mode 100644 index 000000000..89458befc --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core linux malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/linux/malware/check_afinfo.py b/volatility3/framework/plugins/linux/malware/check_afinfo.py new file mode 100644 index 000000000..4ecf5f788 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_afinfo.py @@ -0,0 +1,216 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""A module containing a plugin that verifies the operation function +pointers of network protocols.""" + +import logging +from typing import List, Tuple, Generator + +from volatility3.framework import exceptions, interfaces +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class Check_afinfo(plugins.PluginInterface): + """Verifies the operation function pointers of network protocols.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + @classmethod + def _check_members( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_ops: interfaces.objects.ObjectInterface, + var_name: str, + members: List[str], + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Yields any members that are not pointing inside the kernel + """ + + vmlinux = context.modules[vmlinux_name] + + for check in members: + # redhat-specific garbage + if check.startswith("__UNIQUE_ID_rh_kabi_hide"): + continue + + # These structures have members like `write` and `next`, which are built in Python functions + addr = var_ops.member(attr=check) + + # Unimplemented handlers are set to 0 + if not addr: + continue + + if len(vmlinux.get_symbols_by_absolute_location(addr)) == 0: + yield var_name, check, addr + + @classmethod + def _check_pre_4_18_ops( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + var_name: str, + var: interfaces.objects.ObjectInterface, + op_members: List[str], + seq_members: List[str], + ): + """ + Finds the correct way to reference `op_members` + """ + vmlinux = context.modules[vmlinux_name] + + if var.has_member("seq_fops"): + yield from cls._check_members( + context, vmlinux_name, var.seq_fops, var_name, op_members + ) + # newer kernels + if var.has_member("seq_ops"): + yield from cls._check_members( + context, vmlinux_name, var.seq_ops, var_name, seq_members + ) + + # this is the most commonly hooked member by rootkits, so a force a check on it + elif var.has_member("seq_show"): + if len(vmlinux.get_symbols_by_location(var.seq_show)) == 0: + yield var_name, "show", var.seq_show + else: + raise exceptions.VolatilityException( + "_check_afinfo_pre_4_18: Unable to find sequence operations members for checking." + ) + + @classmethod + def _check_afinfo_pre_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of < 4.18 systems + """ + tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) + udp = ( + "udp_seq_afinfo", + [ + "udplite6_seq_afinfo", + "udp6_seq_afinfo", + "udplite4_seq_afinfo", + "udp4_seq_afinfo", + ], + ) + protocols = [tcp, udp] + + vmlinux = context.modules[vmlinux_name] + + op_members = vmlinux.get_type("file_operations").members + + # loop through all symbols + for struct_type, global_vars in protocols: + for global_var_name in global_vars: + # this will lookup fail for the IPv6 protocols on kernels without IPv6 support + try: + global_var = vmlinux.object_from_symbol(global_var_name) + except exceptions.SymbolError: + continue + + yield from cls._check_pre_4_18_ops( + context, + vmlinux_name, + global_var_name, + global_var, + op_members, + seq_members, + ) + + @classmethod + def _check_afinfo_post_4_18( + cls, + context: interfaces.context.ContextInterface, + vmlinux_name: str, + seq_members: str, + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Checks the operations structures for network protocols of >= 4.18 systems + """ + vmlinux = context.modules[vmlinux_name] + + ops_structs = [ + "raw_seq_ops", + "udp_seq_ops", + "arp_seq_ops", + "unix_seq_ops", + "udp6_seq_ops", + "raw6_seq_ops", + "tcp_seq_ops", + "tcp4_seq_ops", + "tcp6_seq_ops", + "packet_seq_ops", + ] + + for protocol_ops_var in ops_structs: + # These will fail if the particular kernel doesn't have support for a protocol like IPv6 + try: + protocol_ops = vmlinux.object_from_symbol(protocol_ops_var) + except exceptions.SymbolError: + continue + + yield from cls._check_members( + context, vmlinux_name, protocol_ops, protocol_ops_var, seq_members + ) + + @classmethod + def check_afinfo( + cls, context: interfaces.context.ContextInterface, vmlinux_name + ) -> Generator[Tuple[str, str, int], None, None]: + """ + Walks the network protocol operations structures for common network protocols. + Reports any initialized operations members that do not point inside the kernel. + """ + vmlinux = context.modules[vmlinux_name] + + type_check = vmlinux.get_type("tcp_seq_afinfo") + if type_check.has_member("seq_fops"): + checker = cls._check_afinfo_pre_4_18 + else: + checker = cls._check_afinfo_post_4_18 + + seq_members = vmlinux.get_type("seq_operations").members + + yield from checker(context, vmlinux_name, seq_members) + + def _generator(self): + """ + A simple wrapper around `check_afino` + """ + for name, member, address in self.check_afinfo( + self.context, self.config["kernel"] + ): + yield 0, (name, member, format_hints.Hex(address)) + + def run(self): + return renderers.TreeGrid( + [ + ("Symbol Name", str), + ("Member", str), + ("Handler Address", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/check_creds.py b/volatility3/framework/plugins/linux/malware/check_creds.py new file mode 100644 index 000000000..e2b84d679 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_creds.py @@ -0,0 +1,71 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.framework.configuration import requirements +from volatility3.plugins.linux import pslist + + +class Check_creds(interfaces.plugins.PluginInterface): + """Checks if any processes are sharing credential structures""" + + _required_framework_version = (2, 0, 0) + _version = (2, 0, 2) + + @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) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + type_task = vmlinux.get_type("task_struct") + + if not type_task.has_member("cred"): + raise TypeError( + "This plugin requires the task_struct structure to have a cred member. " + "This member is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + creds = {} + + tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) + + for task in tasks: + task_cred_ptr = task.cred + if not (task_cred_ptr and task_cred_ptr.is_readable()): + continue + + cred_addr = task_cred_ptr.dereference().vol.offset + + creds.setdefault(cred_addr, []) + creds[cred_addr].append(task.pid) + + for cred_addr, pids in creds.items(): + if len(pids) > 1: + pid_str = ", ".join(str(pid) for pid in pids) + + fields = [ + format_hints.Hex(cred_addr), + pid_str, + ] + yield (0, fields) + + def run(self): + headers = [ + ("CredVAddr", format_hints.Hex), + ("PIDs", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/malware/check_idt.py b/volatility3/framework/plugins/linux/malware/check_idt.py new file mode 100644 index 000000000..e199d98d3 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_idt.py @@ -0,0 +1,168 @@ +# This file is Copyright 2020 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 typing import List, Optional + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Check_idt(interfaces.plugins.PluginInterface): + """Checks if the IDT has been altered""" + + _required_framework_version = (2, 0, 0) + + # 2.0.0 - Add versioning at all, add `get_idt_type` + _version = (2, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + @staticmethod + def get_idt_type(context, vmlinux_name) -> Optional[str]: + """ + Determines the IDT type for this symbol table or returns None + + The original version ended clauses with an `else` leading to bad fall through + of returning a type that did not exist in the symbol table. + + Future updates should not leave fall through cases to avoid this repeating. + """ + + vmlinux = context.modules[vmlinux_name] + + is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) + + # These are in a specific order. Only append to the lists going forward + # or ask Andrew to run tests before merging. + if is_32bit: + idt_types = ["gate_struct", "desc_struct", "gate_struct32"] + else: + idt_types = ["gate_struct64", "gate_struct", "idt_desc"] + + for idt_type in idt_types: + if vmlinux.has_type(idt_type): + return idt_type + + return None + + def _generator(self): + idt_type = self.get_idt_type(self.context, self.config["kernel"]) + if not idt_type: + vollog.error( + "Unable to determine the data structure type for IDT entries. Please file a bug on the GitHub tracker with your kernel version." + ) + return + + vmlinux = self.context.modules[self.config["kernel"]] + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + idt_table_size = 256 + + kernel_layer = self.context.layers[vmlinux.layer_name] + + address_mask = kernel_layer.address_mask + + # hw handlers + system call + check_idxs = list(range(20)) + [128] + + addrs = vmlinux.object_from_symbol("idt_table") + + table = vmlinux.object( + object_type="array", + offset=addrs.vol.offset, + subtype=vmlinux.get_type(idt_type), + count=idt_table_size, + absolute=True, + ) + + for i in check_idxs: + ent = table[i] + + if not ent or not kernel_layer.is_valid(ent.vol.offset): + continue + + if hasattr(ent, "a"): + idt_addr = (ent.b & 0xFFFF0000) | (ent.a & 0x0000FFFF) + else: + low = ent.offset_low + middle = ent.offset_middle + + # offset_high is for 64bit systems + if hasattr(ent, "offset_high"): + high = ent.offset_high + else: + high = 0 + + idt_addr = (high << 32) | (middle << 16) | low + + idt_addr = idt_addr & address_mask + + # 0 means unintialized/unused, not a rootkit + if idt_addr == 0: + module_name = renderers.NotAvailableValue() + symbol_name = renderers.NotAvailableValue() + else: + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, idt_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(i), + format_hints.Hex(idt_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Index", format_hints.Hex), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/check_modules.py b/volatility3/framework/plugins/linux/malware/check_modules.py new file mode 100644 index 000000000..1834a616e --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_modules.py @@ -0,0 +1,93 @@ +# This file is Copyright 2020 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 typing import Dict, Generator, List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import constants, deprecation, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.symbols.linux import extensions + +vollog = logging.getLogger(__name__) + + +class Check_modules(interfaces.plugins.PluginInterface): + """Compares module list to sysfs info, if available""" + + _version = (3, 0, 1) + _required_framework_version = (2, 0, 0) + + @classmethod + def compare_kset_and_lsmod( + cls, context: interfaces.context.ContextInterface, vmlinux_name: str + ) -> Generator[extensions.module, None, None]: + kset_modules = linux_utilities_modules.Modules.get_kset_modules( + context=context, vmlinux_name=vmlinux_name + ) + + lsmod_modules = set( + str(utility.array_to_string(modules.name)) + for modules in linux_utilities_modules.Modules.list_modules( + context=context, vmlinux_module_name=vmlinux_name + ) + ) + + for mod_name in set(kset_modules.keys()).difference(lsmod_modules): + yield kset_modules[mod_name] + + implementation = compare_kset_and_lsmod + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 1), + ), + requirements.VersionRequirement( + name="linux_utilities_modules_module_display_plugin", + component=linux_utilities_modules.ModuleDisplayPlugin, + version=(2, 0, 0), + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] + + @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_kset_modules, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + def get_kset_modules( + cls, context: interfaces.context.ContextInterface, vmlinux_name: str + ) -> Dict[str, extensions.module]: + return linux_utilities_modules.Modules.get_kset_modules(context, vmlinux_name) + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/check_syscall.py b/volatility3/framework/plugins/linux/malware/check_syscall.py new file mode 100644 index 000000000..6476d6621 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/check_syscall.py @@ -0,0 +1,216 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""A module containing a plugin that checks the system call table for hooks.""" + +import contextlib +import logging +from typing import List + +from volatility3.framework import constants, exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + + +class Check_syscall(plugins.PluginInterface): + """Check system call table for hooks.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux): + """Returns the size of the table based on the next symbol.""" + ret = 0 + + symbol_list = [] + for sn in vmlinux.symbols: + with contextlib.suppress(exceptions.SymbolError): + # When requesting the symbol from the module, a full resolve is performed + symbol_list.append((vmlinux.get_symbol(sn).address, sn)) + sorted_symbols = sorted(symbol_list) + + sym_address = 0 + + for tmp_sym_address, sym_name in sorted_symbols: + if tmp_sym_address > table_addr: + sym_address = tmp_sym_address + break + + if sym_address > 0: + ret = int((sym_address - table_addr) / ptr_sz) + + return ret + + def _get_table_size_meta(self, vmlinux): + """returns the number of symbols that start with __syscall_meta__ this + is a fast way to determine the number of system calls, but not the most + accurate.""" + + return len( + [ + sym + for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols + if sym.startswith("__syscall_meta__") + ] + ) + + def _get_table_info_other(self, table_addr, ptr_sz, vmlinux): + table_size_meta = self._get_table_size_meta(vmlinux) + table_size_syms = self._get_table_size_next_symbol(table_addr, ptr_sz, vmlinux) + + sizes = [size for size in [table_size_meta, table_size_syms] if size > 0] + + table_size = min(sizes) + + return table_size + + def _get_table_info_disassembly(self, ptr_sz, vmlinux) -> int: + """Find the size of the system call table by disassembling functions + that immediately reference it in their first instruction This is in the + form 'cmp reg,NR_syscalls'.""" + table_size = 0 + + if not has_capstone: + return table_size + + if ptr_sz == 4: + syscall_entry_func = "sysenter_do_call" + mode = capstone.CS_MODE_32 + else: + syscall_entry_func = "system_call_fastpath" + mode = capstone.CS_MODE_64 + + md = capstone.Cs(capstone.CS_ARCH_X86, mode) + + try: + func_addr = vmlinux.get_symbol(syscall_entry_func).address + except exceptions.SymbolError: + # if we can't find the disassemble function then bail and rely on a different method + return 0 + + vmlinux = self.context.modules[self.config["kernel"]] + vmlinux_layer = self.context.layers[vmlinux.layer_name] + try: + data = vmlinux_layer.read(func_addr, 6) + except exceptions.InvalidAddressException: + return 0 + + for _address, _size, mnemonic, op_str in md.disasm_lite(data, func_addr): + if mnemonic == "CMP": + table_size = int(op_str.split(",")[1].strip()) & 0xFFFF + break + + return table_size + + def _get_table_info(self, vmlinux, table_name, ptr_sz): + table_sym = vmlinux.get_symbol(table_name) + + table_size = self._get_table_info_disassembly(ptr_sz, vmlinux) + + if table_size == 0: + table_size = self._get_table_info_other(table_sym.address, ptr_sz, vmlinux) + + if table_size == 0: + vollog.error("Unable to get system call table size") + return 0, 0 + + return table_sym.address, table_size + + # TODO - add finding and parsing unistd.h once cached file enumeration is added + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + ptr_sz = vmlinux.get_type("pointer").size + if ptr_sz == 4: + table_name = "32bit" + else: + table_name = "64bit" + + try: + table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz) + except exceptions.SymbolError: + vollog.error("Unable to find the system call table. Exiting.") + return None + + tables = [(table_name, table_info)] + + # this table is only present on 64 bit systems with 32 bit emulation + # enabled in order to support 32 bit programs and libraries + # if the symbol isn't there then the support isn't in the kernel and so we skip it + try: + ia32_symbol = vmlinux.get_symbol("ia32_sys_call_table") + except exceptions.SymbolError: + ia32_symbol = None + + if ia32_symbol is not None: + ia32_info = self._get_table_info(vmlinux, "ia32_sys_call_table", ptr_sz) + tables.append(("32bit", ia32_info)) + + for table_name, (tableaddr, tblsz) in tables: + table = vmlinux.object( + object_type="array", + subtype=vmlinux.get_type("pointer"), + offset=tableaddr, + count=tblsz, + ) + + for i in range(len(table)): + try: + call_addr = table[i] + except exceptions.InvalidAddressException: + vollog.debug(f"Failed to get system call table entry at index {i}") + continue + + symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr)) + + if len(symbols) > 0: + sym_name = ( + str(symbols[0].split(constants.BANG)[1]) + if constants.BANG in symbols[0] + else str(symbols[0]) + ) + else: + sym_name = "UNKNOWN" + + yield ( + 0, + ( + format_hints.Hex(tableaddr), + table_name, + i, + format_hints.Hex(call_addr), + sym_name, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Table Address", format_hints.Hex), + ("Table Name", str), + ("Index", int), + ("Handler Address", format_hints.Hex), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/hidden_modules.py b/volatility3/framework/plugins/linux/malware/hidden_modules.py new file mode 100644 index 000000000..a9f0b5b51 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/hidden_modules.py @@ -0,0 +1,228 @@ +# 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 +from typing import Generator, Iterable, List, Set, Tuple + +from volatility3.framework import ( + constants, + deprecation, + exceptions, + interfaces, + renderers, +) +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols.linux import extensions +from volatility3.framework.symbols.linux.utilities import ( + modules as linux_utilities_modules, +) + +vollog = logging.getLogger(__name__) + + +class Hidden_modules(plugins.PluginInterface): + """Carves memory to find hidden kernel modules""" + + _required_framework_version = (2, 25, 0) + _version = (3, 0, 3) + + @classmethod + def find_hidden_modules( + cls, context, vmlinux_module_name: str + ) -> Generator[extensions.module, None, None]: + if context.symbol_space.verify_table_versions( + "dwarf2json", lambda version, _: (not version) or version < (0, 8, 0) + ): + raise exceptions.SymbolSpaceError( + "Invalid symbol table, please ensure the ISF table produced by dwarf2json was created with version 0.8.0 or later" + ) + + known_module_addresses = cls.get_lsmod_module_addresses( + context, vmlinux_module_name + ) + modules_memory_boundaries = ( + linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_module_name + ) + ) + + yield from linux_utilities_modules.Modules.get_hidden_modules( + context, + vmlinux_module_name, + known_module_addresses, + modules_memory_boundaries, + ) + + @classmethod + def get_hidden_modules( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + known_module_addresses: Set[int], + modules_memory_boundaries: Tuple, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Enumerate hidden modules by taking advantage of memory address alignment patterns + + This technique is much faster and uses less memory than the traditional scan method + in Volatility2, but it doesn't work with older kernels. + + From kernels 4.2 struct module allocation are aligned to the L1 cache line size. + In i386/amd64/arm64 this is typically 64 bytes. However, this can be changed in + the Linux kernel configuration via CONFIG_X86_L1_CACHE_SHIFT. The alignment can + also be obtained from the DWARF info i.e. DW_AT_alignment<64>, but dwarf2json + doesn't support this feature yet. + In kernels < 4.2, alignment attributes are absent in the struct module, meaning + alignment cannot be guaranteed. Therefore, for older kernels, it's better to use + the traditional scan technique. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + known_module_addresses: Set with known module addresses + modules_memory_boundaries: Minimum and maximum address boundaries for module allocation. + Yields: + module objects + """ + return linux_utilities_modules.get_hidden_modules( + vmlinux_module_name, known_module_addresses, modules_memory_boundaries + ) + + implementation = find_hidden_modules + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=constants.architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linux_utilities_modules_module_display_plugin", + component=linux_utilities_modules.ModuleDisplayPlugin, + version=(2, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 1), + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + ] + + @staticmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_modules_memory_boundaries, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + def get_modules_memory_boundaries( + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Tuple[int, int]: + return linux_utilities_modules.Modules.get_modules_memory_boundaries( + context, vmlinux_module_name + ) + + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_module_address_alignment, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + @classmethod + def _get_module_address_alignment( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> int: + """Obtain the module memory address alignment. + + struct module is aligned to the L1 cache line, which is typically 64 bytes for most + common i386/AMD64/ARM64 configurations. In some cases, it can be 128 bytes, but this + will still work. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + The struct module alignment + """ + return linux_utilities_modules.get_module_address_alignment( + context, vmlinux_module_name + ) + + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.get_hidden_modules, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + @staticmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.validate_alignment_patterns, + removal_date="2026-03-25", + replacement_version=(3, 0, 0), + ) + def _validate_alignment_patterns( + addresses: Iterable[int], + address_alignment: int, + ) -> bool: + """Check if the memory addresses meet our alignments patterns + + Args: + addresses: Iterable with the address values + address_alignment: Number of bytes for alignment validation + + Returns: + True if all the addresses meet the alignment + """ + return linux_utilities_modules.validate_alignment_patterns( + addresses, address_alignment + ) + + @classmethod + def get_lsmod_module_addresses( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ) -> Set[int]: + """Obtain a set the known module addresses from linux.lsmod plugin + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + + Returns: + A set containing known kernel module addresses + """ + vmlinux = context.modules[vmlinux_module_name] + vmlinux_layer = context.layers[vmlinux.layer_name] + + known_module_addresses = { + vmlinux_layer.canonicalize(module.vol.offset) + for module in linux_utilities_modules.Modules.list_modules( + context, vmlinux_module_name + ) + } + return known_module_addresses + + def run(self): + return renderers.TreeGrid( + linux_utilities_modules.ModuleDisplayPlugin.columns_results, + self._generator(), + ) + + def _generator(self): + yield from linux_utilities_modules.ModuleDisplayPlugin.generate_results( + self.context, + self.implementation, + self.config["kernel"], + self.config["dump"], + self.open, + ) diff --git a/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py new file mode 100644 index 000000000..9e99809b2 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/keyboard_notifiers.py @@ -0,0 +1,105 @@ +# This file is Copyright 2020 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 volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Keyboard_notifiers(interfaces.plugins.PluginInterface): + """Parses the keyboard notifier call chain""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + try: + knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") + except exceptions.SymbolError: + knl_addr = None + + if not knl_addr: + raise TypeError( + "This plugin requires the keyboard_notifier_list structure. " + "This structure is not present in the supplied symbol table. " + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + if not self.context.layers[vmlinux.layer_name].is_valid(knl_addr.vol.offset): + vollog.error("The head of the keyboard notifier list is paged out.") + return + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + knl = vmlinux.object( + object_type="atomic_notifier_head", + offset=knl_addr.vol.offset, + absolute=True, + ) + + for call_back in linux.LinuxUtilities.walk_internal_list( + vmlinux, "notifier_block", "next", knl.head + ): + call_addr = call_back.notifier_call + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, call_addr + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + [ + format_hints.Hex(call_addr), + module_name, + symbol_name or renderers.NotAvailableValue(), + ], + ) + + def run(self): + return renderers.TreeGrid( + [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/malfind.py b/volatility3/framework/plugins/linux/malware/malfind.py new file mode 100644 index 000000000..cbd9f87c1 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/malfind.py @@ -0,0 +1,150 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import List, Tuple, Optional +import logging +from volatility3.framework import interfaces +from volatility3.framework import renderers, symbols +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class Malfind(interfaces.plugins.PluginInterface): + """Lists process memory ranges that potentially contain injected code.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 4) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.IntRequirement( + name="dump-size", + description="Amount of bytes to dump for each dirty region/page found - Default 64 bytes", + optional=True, + default=64, + ), + requirements.BooleanRequirement( + name="dump-page", + description="Dump each dirty page and content - Default off", + optional=True, + default=False, + ), + ] + + def _list_injections( + self, task + ) -> Tuple[interfaces.objects.ObjectInterface, Optional[str], bytes]: + """Generate memory regions for a process that may contain injected + code.""" + + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + return None + + proc_layer = self.context.layers[proc_layer_name] + + dump_size = self.config["dump-size"] + + # Dumping page defaults to off, as in case a whole r-xp region is dirty + # this would likely dump 1000's of pages which might not always be wise nor necessary + + dump_page = self.config["dump-page"] + + for vma in task.mm.get_vma_iter(): + vma_name = vma.get_name(self.context, task) + vollog.debug( + f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" + ) + + # If is_suspicious returns true, this means at least one page + # in the region is dirty. If dump_page is true, then we dump + # all dirty pages + + if vma.is_suspicious(proc_layer) and vma_name != "[vdso]": + malicious_pages = vma.get_malicious_pages(proc_layer) + offset = 0 + if dump_page: + # Dumping each dirty page + for page_addr in malicious_pages: + offset = page_addr - vma.vm_start + data = proc_layer.read(page_addr, dump_size, pad=True) + yield vma, f"{vma_name}, page address: {page_addr:#x}, offset: {offset:#x}", data, offset + else: + # Original behaviour - Dump the start of the region (not necessarily matching the dirty page) + data = proc_layer.read(vma.vm_start, dump_size, pad=True) + yield vma, vma_name, data, offset + + def _generator(self, tasks): + # determine if we're on a 32 or 64 bit kernel + vmlinux = self.context.modules[self.config["kernel"]] + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=vmlinux.symbol_table_name + ) + + for task in tasks: + process_name = utility.array_to_string(task.comm) + + for vma, vma_name, data, offset in self._list_injections(task): + if is_32bit_arch: + architecture = "intel" + else: + architecture = "intel64" + + disasm = renderers.Disassembly( + data, vma.vm_start + offset, architecture + ) + + yield ( + 0, + ( + task.pid, + process_name, + format_hints.Hex(vma.vm_start), + format_hints.Hex(vma.vm_end), + vma_name or renderers.NotAvailableValue(), + vma.get_protection(), + format_hints.HexBytes(data), + disasm, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Path", str), + ("Protection", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/malware/modxview.py b/volatility3/framework/plugins/linux/malware/modxview.py new file mode 100644 index 000000000..63b265202 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/modxview.py @@ -0,0 +1,180 @@ +# 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 +from typing import Dict, Iterator, List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import deprecation, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.constants import architectures +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.linux import extensions +from volatility3.framework.symbols.linux.utilities import tainting + +vollog = logging.getLogger(__name__) + + +class Modxview(interfaces.plugins.PluginInterface): + """Centralize lsmod, check_modules and hidden_modules results to efficiently \ +spot modules presence and taints.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 17, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=architectures.LINUX_ARCHS, + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_lsmod", + component=linux_utilities_modules.ModuleGathererLsmod, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_sysfs", + component=linux_utilities_modules.ModuleGathererSysFs, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherer_scanner", + component=linux_utilities_modules.ModuleGathererScanner, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="plain_taints", + description="Display the plain taints string for each module.", + optional=True, + default=False, + ), + ] + + @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.flatten_run_modules_results, + replacement_version=(3, 0, 0), + removal_date="2026-03-25", + ) + def flatten_run_modules_results( + cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True + ) -> Iterator[extensions.module]: + """Flatten a dictionary mapping plugin names and modules list, to a single merged list. + This is useful to get a generic lookup list of all the detected modules. + + Args: + run_results: dictionary of plugin names mapping a list of detected modules + deduplicate: remove duplicate modules, based on their offsets + + Returns: + Iterator of modules objects + """ + return linux_utilities_modules.Modules.flatten_run_modules_results( + run_results, deduplicate + ) + + @classmethod + @deprecation.deprecated_method( + replacement=linux_utilities_modules.Modules.run_modules_scanners, + replacement_version=(3, 0, 0), + removal_date="2026-03-25", + ) + def run_modules_scanners( + cls, + context: interfaces.context.ContextInterface, + kernel_name: str, + run_hidden_modules: bool = True, + ) -> Dict[str, List[extensions.module]]: + """Run module scanning plugins and aggregate the results. It is designed + to not operate any inter-plugin results triage.""" + return linux_utilities_modules.Modules.run_modules_scanners( + context, kernel_name, run_hidden_modules + ) + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + wanted_gatherers = [ + linux_utilities_modules.ModuleGathererLsmod, + linux_utilities_modules.ModuleGathererSysFs, + linux_utilities_modules.ModuleGathererScanner, + ] + + run_results = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=wanted_gatherers, + flatten=False, + ) + + aggregated_modules = {} + # We want to be explicit on the plugins results we are interested in + for gatherer in wanted_gatherers: + # Iterate over each recovered module + for mod_info in run_results[gatherer.name]: + # Use offsets as unique keys, whether a module + # appears in many plugin runs or not + if aggregated_modules.get(mod_info.offset, None) is not None: + # Append the plugin to the list of originating plugins + aggregated_modules[mod_info.offset].append(gatherer.name) + else: + aggregated_modules[mod_info.offset] = [gatherer.name] + + for module_offset, gatherers in aggregated_modules.items(): + module = kernel.object("module", offset=module_offset, absolute=True) + + # Tainting parsing capabilities applied to the module + if self.config.get("plain_taints"): + taints = tainting.Tainting.get_taints_as_plain_string( + self.context, + self.config["kernel"], + module.taints, + True, + ) + else: + taints = ",".join( + tainting.Tainting.get_taints_parsed( + self.context, + self.config["kernel"], + module.taints, + True, + ) + ) + + yield ( + 0, + ( + module.get_name() or renderers.NotAvailableValue(), + format_hints.Hex(module_offset), + linux_utilities_modules.ModuleGathererLsmod.name in gatherers, + linux_utilities_modules.ModuleGathererSysFs.name in gatherers, + linux_utilities_modules.ModuleGathererScanner.name in gatherers, + taints or renderers.NotAvailableValue(), + ), + ) + + def run(self): + columns = [ + ("Name", str), + ("Address", format_hints.Hex), + ("In procfs", bool), + ("In sysfs", bool), + ("In scan", bool), + ("Taints", str), + ] + + return renderers.TreeGrid( + columns, + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/malware/netfilter.py b/volatility3/framework/plugins/linux/malware/netfilter.py new file mode 100644 index 000000000..bd7f2b7cc --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/netfilter.py @@ -0,0 +1,815 @@ +# 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 +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Iterator, List, Optional, Tuple + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3 import framework +from volatility3.framework import ( + constants, + deprecation, + exceptions, + interfaces, + renderers, +) +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.linux import network + +vollog = logging.getLogger(__name__) + + +@dataclass +class Proto: + name: str + hooks: Tuple[str] = field(default_factory=tuple) + + +PROTO_NOT_IMPLEMENTED = Proto(name="UNSPEC") + +NF_INET_HOOKS = ("PRE_ROUTING", "LOCAL_IN", "FORWARD", "LOCAL_OUT", "POST_ROUTING") +NF_DEC_HOOKS = ( + "PRE_ROUTING", + "LOCAL_IN", + "FORWARD", + "LOCAL_OUT", + "POST_ROUTING", + "HELLO", + "ROUTE", +) +NF_ARP_HOOKS = ("IN", "OUT", "FORWARD") +NF_NETDEV_HOOKS = ("INGRESS", "EGRESS") +LARGEST_HOOK_NUMBER = max( + len(NF_INET_HOOKS), len(NF_DEC_HOOKS), len(NF_ARP_HOOKS), len(NF_NETDEV_HOOKS) +) + + +class AbstractNetfilter(ABC): + """Netfilter Abstract Base Classes handling details across various + Netfilter implementations, including constants, helpers, and common + routines. + """ + + PROTO_HOOKS = ( + PROTO_NOT_IMPLEMENTED, # NFPROTO_UNSPEC + Proto(name="INET", hooks=NF_INET_HOOKS), # From kernels 3.14 + Proto(name="IPV4", hooks=NF_INET_HOOKS), + Proto(name="ARP", hooks=NF_ARP_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="NETDEV", hooks=NF_NETDEV_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="BRIDGE", hooks=NF_INET_HOOKS), + PROTO_NOT_IMPLEMENTED, + PROTO_NOT_IMPLEMENTED, + Proto(name="IPV6", hooks=NF_INET_HOOKS), + PROTO_NOT_IMPLEMENTED, + Proto(name="DECNET", hooks=NF_DEC_HOOKS), # Removed in kernel 6.1 + ) + NF_MAX_HOOKS = LARGEST_HOOK_NUMBER + 1 + + def __init__( + self, context: interfaces.context.ContextInterface, kernel_module_name: str + ): + self._context = context + self.vmlinux = context.modules[kernel_module_name] + self.layer_name = self.vmlinux.layer_name + + # Set data sizes + self.ptr_size = self.vmlinux.get_type("pointer").size + self.list_head_size = self.vmlinux.get_type("list_head").size + + linuxutils_modulegatherers_required_version = ( + Netfilter._required_linuxutils_gatherers_version + ) + linuxutils_modulegatherers_current_version = ( + linux_utilities_modules.ModuleGatherers.version + ) + if not requirements.VersionRequirement.matches_required( + linuxutils_modulegatherers_required_version, + linuxutils_modulegatherers_current_version, + ): + raise exceptions.PluginRequirementException( + f"linux_utilities_modules.ModuleGatherer version not suitable: required {linuxutils_modulegatherers_required_version} found {linuxutils_modulegatherers_current_version}" + ) + + linux_net_required_version = Netfilter._required_linuxnet_version + linux_net_current_version = network.NetSymbols.version + if not requirements.VersionRequirement.matches_required( + linux_net_required_version, linux_net_current_version + ): + raise exceptions.PluginRequirementException( + f"symbols.linux.net.NetSymbols version not suitable: required {linux_net_required_version} found {linux_net_current_version}" + ) + + linux_utilities_modules_required_version = ( + Netfilter._required_linux_utilities_modules_version + ) + linux_utilities_modules_current_version = ( + linux_utilities_modules.Modules.version + ) + if not requirements.VersionRequirement.matches_required( + linux_utilities_modules_required_version, + linux_utilities_modules_current_version, + ): + raise exceptions.PluginRequirementException( + f"linux_utilities_modules.Modules version not suitable: required {linux_utilities_modules_required_version} found {linux_utilities_modules_current_version}" + ) + + symbol_table = context.symbol_space[self.vmlinux.symbol_table_name] + network.NetSymbols.apply(symbol_table) + + self.handlers = linux_utilities_modules.Modules.run_modules_scanners( + context=context, + kernel_module_name=kernel_module_name, + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + @classmethod + def run_all( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: + """It calls each subclass symtab_checks() to test the required + conditions to that specific kernel implementation. + + Args: + context: The volatility3 context on which to operate + kernel_module_name: The name of the table containing the kernel symbols + + Yields: + The kmsg records. Same as _run() + """ + vmlinux = context.modules[kernel_module_name] + + implementation_inst = None # type: ignore + for subclass in framework.class_subclasses(cls): + if not subclass.symtab_checks(vmlinux=vmlinux): + vollog.log( + constants.LOGLEVEL_VVVV, + "Netfilter implementation '%s' doesn't match this memory dump", + subclass.__name__, + ) + continue + + vollog.log( + constants.LOGLEVEL_VVVV, + "Netfilter implementation '%s' matches!", + subclass.__name__, + ) + implementation_inst = subclass( + context=context, kernel_module_name=kernel_module_name + ) + # More than one class could be executed for an specific kernel version + # For instance: Netfilter Ingress hooks + yield from implementation_inst._run() + + if implementation_inst is None: + vollog.error("Unsupported Netfilter kernel implementation") + + def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: + """Iterates over namespaces and protocols, executing various callbacks that + allow customization of the code to the specific data structure used in a + particular kernel implementation + + get_hooks_container(net, proto_name, hook_name) + It returns the data structure used in a specific kernel implementation + to store the hooks for a respective namespace and protocol, basically: + For Ingress hooks: + network_namespace[] -> net_device[] -> nf_hooks_ingress[] + For egress hooks: + network_namespace[] -> net_device[] -> nf_hooks_egress[] + For all the other Netfilter hooks: + <= 4.2.8 + nf_hooks[] + >= 4.3 + network_namespace[] -> nf.hooks[] + + get_hook_ops(hook_container, proto_idx, hook_idx) + Give the 'hook_container' got in get_hooks_container(), it + returns an iterable of 'nf_hook_ops' elements for a respective protocol + and hook type. + + Returns: + netns [int]: Network namespace id + proto_name [str]: Protocol name + hook_name [str]: Hook name + priority [int]: Priority + hook_ops_hook [int]: Hook address + module_name [str]: Linux kernel module name + hooked [bool]: "True" if the network stack has been hijacked + """ + for netns, net in self.get_net_namespaces(): + for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop(): + hooks_container = self.get_hooks_container(net, proto_name, hook_name) + + for hook_container in hooks_container: + for hook_ops in self.get_hook_ops( + hook_container, proto_idx, hook_idx + ): + if not hook_ops: + continue + + priority = int(hook_ops.priority) + hook_ops_hook = hook_ops.hook + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self._context, + self.vmlinux.name, + self.handlers, + hook_ops_hook, + ) + ) + hooked = module_info is None + + yield ( + netns, + proto_name, + hook_name, + priority, + hook_ops_hook, + module_info, + symbol_name, + hooked, + ) + + @classmethod + @abstractmethod + def symtab_checks(cls, vmlinux: interfaces.context.ModuleInterface) -> bool: + """This method on each sublasss will be called to evaluate if the kernel + being analyzed fulfill the type & symbols requirements for the implementation. + The first class returning True will be instantiated and called via the + run() method. + + Returns: + bool: True if the kernel being analyzed fulfill the class requirements. + """ + + def _proto_hook_loop(self) -> Iterator[Tuple[int, str, int, str]]: + """Flattens the protocol families and hooks""" + for proto_idx, proto in enumerate(AbstractNetfilter.PROTO_HOOKS): + if proto == PROTO_NOT_IMPLEMENTED: + continue + if proto.name not in self.subscribed_protocols(): + # This protocol is not managed in this object + continue + for hook_idx, hook_name in enumerate(proto.hooks): + yield proto_idx, proto.name, hook_idx, hook_name + + def build_nf_hook_ops_array( + self, nf_hook_entries + ) -> Optional[interfaces.objects.ObjectInterface]: + """Function helper to build the nf_hook_ops array when it is not part of the + struct 'nf_hook_entries' definition. + + nf_hook_ops was stored adjacent in memory to the nf_hook_entry array, in the + new struct 'nf_hook_entries'. However, this 'nf_hooks_ops' array 'orig_ops' is + not part of the 'nf_hook_entries' struct. So, we need to calculate the offset. + + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; + } + """ + nf_hook_entry_size = self.vmlinux.get_type("nf_hook_entry").size + + try: + num_hook_entries = nf_hook_entries.num_hook_entries + except exceptions.InvalidAddressException: + return None + + orig_ops_addr = ( + nf_hook_entries.hooks.vol.offset + nf_hook_entry_size * num_hook_entries + ) + + if not self.vmlinux._context.layers[self.vmlinux.layer_name].is_valid( + orig_ops_addr + ): + return None + + orig_ops = self._context.object( + object_type=self.get_symbol_fullname("array"), + offset=orig_ops_addr, + subtype=self.vmlinux.get_type("pointer"), + layer_name=self.layer_name, + count=num_hook_entries, + ) + + return orig_ops + + def subscribed_protocols(self) -> Tuple[str]: + """Allows to select which PROTO_HOOKS protocols will be processed by the + Netfiler subclass. + """ + + # Most implementation handlers respond to these protocols, except for + # the ingress hook, which specifically handles the 'NETDEV' protocol. + # However, there is no corresponding Netfilter hook implementation for + # the INET protocol in the kernel. AFAIU, this is used as + # 'NFPROTO_INET = NFPROTO_IPV4 || NFPROTO_IPV6' + # in other parts of the kernel source code. + return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") + + @deprecation.method_being_removed( + removal_date="2026-03-25", + message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", + ) + def get_module_name_for_address(self, addr) -> str: + """Helper to obtain the module and symbol name in the format needed for the + output of this plugin. + """ + module_name, symbol_name = ( + linux_utilities_modules.Modules.lookup_module_address( + self._context, self.vmlinux.name, self.handlers, addr + ) + ) + + if module_name == "UNKNOWN": + module_name = None + + if symbol_name != "N/A": + module_name = f"[{symbol_name}]" + + return module_name + + def get_net_namespaces(self): + """Common function to retrieve the different namespaces. + From 4.3 on, all the implementations use network namespaces. + """ + nethead = self.vmlinux.object_from_symbol("net_namespace_list") + symbol_net_name = self.get_symbol_fullname("net") + for net in nethead.to_list(symbol_net_name, "list"): + net_ns_id = net.ns.inum + yield net_ns_id, net + + def get_hooks_container(self, net, proto_name, hook_name): + """Returns the data structure used in a specific kernel implementation to store + the hooks for a respective namespace and protocol. + + Except for kernels < 4.3, all the implementations use network namespaces. + Also the data structure which contains the hooks, even though it changes its + implementation and/or data type, it is always in this location. + """ + yield net.nf.hooks + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + """Given the hook_container obtained from get_hooks_container(), it + returns an iterable of 'nf_hook_ops' elements for a corresponding protocol + and hook type. + + This is the most variable/unstable part of all Netfilter hook designs, it + changes almost in every single implementation. + """ + raise NotImplementedError("You must implement this method") + + def get_symbol_fullname(self, symbol_basename: str) -> str: + """Given a short symbol or type name, it returns its full name""" + return self.vmlinux.symbol_table_name + constants.BANG + symbol_basename + + @staticmethod + def get_member_type( + vol_type: interfaces.objects.Template, member_name: str + ) -> List[str]: + """Returns a list of types/subtypes belonging to the given type member. + + Args: + vol_type (interfaces.objects.Template): A vol3 type object + member_name (str): The member name + + Returns: + list: A list of types/subtypes + """ + _size, vol_obj = vol_type.vol.members[member_name] + type_name = vol_obj.type_name + type_basename = type_name.split(constants.BANG)[1] + member_type = [type_basename] + cur_type = vol_obj + while hasattr(cur_type, "subtype"): + subtype_name = cur_type.subtype.type_name + subtype_basename = subtype_name.split(constants.BANG)[1] + member_type.append(subtype_basename) + cur_type = cur_type.subtype + + return member_type + + +class NetfilterImp_to_4_3(AbstractNetfilter): + """At this point, Netfilter hooks were implemented as a linked list of struct + 'nf_hook_ops' type. One linked list per protocol per hook type. + It was like that until 4.2.8. + + struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return vmlinux.has_symbol("nf_hooks") + + def get_net_namespaces(self): + # In kernels <= 4.2.8 netfilter hooks are not implemented per namespaces + netns, net = renderers.NotAvailableValue(), renderers.NotAvailableValue() + yield netns, net + + def get_hooks_container(self, net, proto_name, hook_name): + nf_hooks = self.vmlinux.object_from_symbol("nf_hooks") + if not nf_hooks: + return + + yield nf_hooks + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + +class NetfilterImp_4_3_to_4_9(AbstractNetfilter): + """Netfilter hooks were added to network namespaces in 4.3. + It is still implemented as a linked list of 'struct nf_hook_ops' type but inside a + network namespace. One linked list per protocol per hook type. + + struct net { ... struct netns_nf nf; ... } + struct netns_nf { ... + struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") + == ["array", "array", "list_head"] + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + +class NetfilterImp_4_9_to_4_14(AbstractNetfilter): + """In this range of kernel versions, the doubly-linked lists of netfilter hooks were + replaced by an array of arrays of 'nf_hook_entry' pointers in a singly-linked lists. + struct net { ... struct netns_nf nf; ... } + struct netns_nf { .. + struct nf_hook_entry __rcu *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + + Also in v4.10 the struct nf_hook_entry changed, a hook function pointer was added to + it. However, for simplicity of this design, we will still take the hook address from + the 'nf_hook_ops'. As per v5.0-rc2, the hook address is duplicated in both sides. + - v4.9: + struct nf_hook_entry { + struct nf_hook_entry *next; + struct nf_hook_ops ops; + const struct nf_hook_ops *orig_ops; }; + - v4.10: + struct nf_hook_entry { + struct nf_hook_entry *next; + nf_hookfn *hook; + void *priv; + const struct nf_hook_ops *orig_ops; }; + (*) Even though the hook address is in the struct 'nf_hook_entry', we use the + original 'nf_hook_ops' hook address value, the one which was filled by the user, to + make it uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["array", "array", "pointer", "nf_hook_entry"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type + ) + + def _get_hook_ops(self, hook_container, proto_idx, hook_idx): + list_head = hook_container[proto_idx][hook_idx] + nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") + return list_head.to_list(nf_hooks_ops_name, "list") + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entry_list = hook_container[proto_idx][hook_idx] + while nf_hook_entry_list: + yield nf_hook_entry_list.orig_ops + nf_hook_entry_list = nf_hook_entry_list.next + + +class NetfilterImp_4_14_to_4_16(AbstractNetfilter): + """'nf_hook_ops' was removed from struct 'nf_hook_entry'. Instead, it was stored + adjacent in memory to the 'nf_hook_entry' array, in the new struct 'nf_hook_entries' + However, 'orig_ops' is not part of the 'nf_hook_entries' struct definition. So, we + have to craft it by hand. + + struct net { ... struct netns_nf nf; ... } + struct netns_nf { + struct nf_hook_entries *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + struct nf_hook_entry { + nf_hookfn *hook; + void *priv; } + + (*) Even though the hook address is in the struct 'nf_hook_entry', we use the + original 'nf_hook_ops' hook address value, the one which was filled by the user, to + make it uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["array", "array", "pointer", "nf_hook_entries"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks") + and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type + ) + + def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): + """This allows to support different hook array implementations from this version + on. For instance, in kernels >= 4.16 this multi-dimensional array is split in + one-dimensional array of pointers to 'nf_hooks_entries' per each protocol.""" + return nf_hooks_addr[proto_idx][hook_idx] + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entries = self.get_nf_hook_entries(hook_container, proto_idx, hook_idx) + if not nf_hook_entries: + return + + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) + if not nf_hook_ops_ptr_arr: + return + + for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: + nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) + yield nf_hook_ops + + +class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16): + """The multidimensional array of nf_hook_entries was split in a one-dimensional + array per each protocol. + + struct net { + struct netns_nf nf; ... } + struct netns_nf { + struct nf_hook_entries * hooks_ipv4[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_ipv6[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_arp[NF_ARP_NUMHOOKS]; + struct nf_hook_entries * hooks_bridge[NF_INET_NUMHOOKS]; + struct nf_hook_entries * hooks_decnet[NF_DN_NUMHOOKS]; ... } + struct nf_hook_entries { + u16 num_hook_entries; /* plus padding */ + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + struct nf_hook_entry { + nf_hookfn *hook; + void *priv; } + + (*) Even though the hook address is in the struct nf_hook_entry, we use the original + nf_hook_ops hook address value, the one which was filled by the user, to make it + uniform to all the implementations. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("netns_nf") + and vmlinux.get_type("netns_nf").has_member("hooks_ipv4") + ) + + def get_hooks_container(self, net, proto_name, hook_name): + try: + if proto_name == "IPV4": + net_nf_hooks = net.nf.hooks_ipv4 + elif proto_name == "ARP": + net_nf_hooks = net.nf.hooks_arp + elif proto_name == "BRIDGE": + net_nf_hooks = net.nf.hooks_bridge + elif proto_name == "IPV6": + net_nf_hooks = net.nf.hooks_ipv6 + elif proto_name == "DECNET": + net_nf_hooks = net.nf.hooks_decnet + else: + return + + yield net_nf_hooks + + except AttributeError: + # Protocol family disabled at kernel compilation + # CONFIG_NETFILTER_FAMILY_ARP=n || + # CONFIG_NETFILTER_FAMILY_BRIDGE=n || + # CONFIG_DECNET=n + pass + + def _get_nf_hook_entries_ptr(self, nf_hooks_addr, proto_idx, hook_idx): + nf_hook_entries_ptr = nf_hooks_addr[hook_idx] + return nf_hook_entries_ptr + + def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): + return nf_hooks_addr[hook_idx] + + +class AbstractNetfilterNetDev(AbstractNetfilter): + """Base class to handle the Netfilter NetDev hooks. + It won't be executed. It has some common functions to all Netfilter NetDev hook + implementations. + + Netfilter NetDev hooks are set per network device which belongs to a network + namespace. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return False + + def subscribed_protocols(self): + return ("NETDEV",) + + def get_hooks_container(self, net, proto_name, hook_name): + net_device_type = self.vmlinux.get_type("net_device") + net_device_name = self.get_symbol_fullname("net_device") + for net_device in net.dev_base_head.to_list(net_device_name, "dev_list"): + if hook_name == "INGRESS": + if net_device_type.has_member("nf_hooks_ingress"): + # CONFIG_NETFILTER_INGRESS=y + yield net_device.nf_hooks_ingress + + elif hook_name == "EGRESS": + if net_device_type.has_member("nf_hooks_egress"): + # CONFIG_NETFILTER_EGRESS=y + yield net_device.nf_hooks_egress + + +class NetfilterNetDevImp_4_2_to_4_9(AbstractNetfilterNetDev): + """This is the first version of Netfilter Ingress hooks which was implemented using + a doubly-linked list of 'nf_hook_ops'. + struct list_head nf_hooks_ingress; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["list_head"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hooks_ingress = hook_container + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + return nf_hooks_ingress.to_list(nf_hook_ops_name, "list") + + +class NetfilterNetDevImp_4_9_to_4_14(AbstractNetfilterNetDev): + """In 4.9 it was changed to a simple singly-linked list. + struct nf_hook_entry * nf_hooks_ingress; + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["pointer", "nf_hook_entry"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hooks_ingress_ptr = hook_container + if not nf_hooks_ingress_ptr: + return + + while nf_hooks_ingress_ptr: + nf_hook_entry = nf_hooks_ingress_ptr.dereference() + orig_ops = nf_hook_entry.orig_ops.dereference() + yield orig_ops + nf_hooks_ingress_ptr = nf_hooks_ingress_ptr.next + + +class NetfilterNetDevImp_4_14_to_latest(AbstractNetfilterNetDev): + """In 4.14 the hook list was converted to an array of pointers inside the struct + 'nf_hook_entries': + struct nf_hook_entries * nf_hooks_ingress; + struct nf_hook_entries { + u16 num_hook_entries; + struct nf_hook_entry hooks[]; + //const struct nf_hook_ops *orig_ops[]; } + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + hooks_type = ["pointer", "nf_hook_entries"] + return ( + vmlinux.has_symbol("net_namespace_list") + and vmlinux.has_type("net_device") + and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") + and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") + == hooks_type + ) + + def get_hook_ops(self, hook_container, proto_idx, hook_idx): + nf_hook_entries = hook_container + if not nf_hook_entries: + return + + nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") + nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) + if not nf_hook_ops_ptr_arr: + return + + for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: + nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) + yield nf_hook_ops + + +class Netfilter(interfaces.plugins.PluginInterface): + """Lists Netfilter hooks.""" + + _required_framework_version = (2, 22, 0) + + _version = (2, 0, 0) + + _required_linux_utilities_modules_version = (3, 0, 0) + _required_linuxutils_gatherers_version = (1, 0, 0) + _required_linuxnet_version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=cls._required_linuxutils_gatherers_version, + ), + requirements.VersionRequirement( + name="linuxnet", + component=network.NetSymbols, + version=cls._required_linuxnet_version, + ), + ] + + def _format_fields(self, fields): + ( + netns, + proto_name, + hook_name, + priority, + hook_func, + module_info, + symbol_name, + hooked, + ) = fields + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + return ( + netns, + proto_name, + hook_name, + priority, + format_hints.Hex(hook_func), + module_name, + symbol_name or renderers.NotAvailableValue(), + str(hooked), + ) + + def _generator(self): + kernel_module_name = self.config["kernel"] + for fields in AbstractNetfilter.run_all( + context=self.context, kernel_module_name=kernel_module_name + ): + yield (0, self._format_fields(fields)) + + def run(self): + headers = [ + ("Net NS", int), + ("Proto", str), + ("Hook", str), + ("Priority", int), + ("Handler", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ("Is Hooked", str), + ] + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/plugins/linux/malware/tty_check.py b/volatility3/framework/plugins/linux/malware/tty_check.py new file mode 100644 index 000000000..00e85f251 --- /dev/null +++ b/volatility3/framework/plugins/linux/malware/tty_check.py @@ -0,0 +1,122 @@ +# This file is Copyright 2020 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 typing import List + +import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules +from volatility3.framework import interfaces, renderers, exceptions, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import linux + +vollog = logging.getLogger(__name__) + + +class Tty_Check(plugins.PluginInterface): + """Checks tty devices for hooks""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linux_utilities_modules", + component=linux_utilities_modules.Modules, + version=(3, 0, 0), + ), + requirements.VersionRequirement( + name="linux_utilities_module_gatherers", + component=linux_utilities_modules.ModuleGatherers, + version=(1, 0, 0), + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + ] + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + try: + tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") + except exceptions.SymbolError: + tty_drivers = None + + if not tty_drivers: + raise TypeError( + "This plugin requires the tty_drivers structure." + "This structure is not present in the supplied symbol table." + "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + known_modules = linux_utilities_modules.Modules.run_modules_scanners( + context=self.context, + kernel_module_name=self.config["kernel"], + caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, + ) + + for tty in tty_drivers.to_list( + vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" + ): + try: + ttys = utility.array_of_pointers( + tty.ttys.dereference(), + count=tty.num, + subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct", + context=self.context, + ) + except exceptions.PagedInvalidAddressException: + continue + + for tty_dev in ttys: + if tty_dev == 0: + continue + + try: + name = utility.array_to_string(tty_dev.name) + recv_buf = tty_dev.ldisc.ops.receive_buf + except exceptions.InvalidAddressException: + continue + + module_info, symbol_name = ( + linux_utilities_modules.Modules.module_lookup_by_address( + self.context, vmlinux.name, known_modules, recv_buf + ) + ) + + if module_info: + module_name = module_info.name + else: + module_name = renderers.NotAvailableValue() + + yield ( + 0, + ( + name, + format_hints.Hex(recv_buf), + module_name, + symbol_name or renderers.NotAvailableValue(), + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Name", str), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/module_extract.py b/volatility3/framework/plugins/linux/module_extract.py index 97824aca0..a8864281a 100644 --- a/volatility3/framework/plugins/linux/module_extract.py +++ b/volatility3/framework/plugins/linux/module_extract.py @@ -75,10 +75,13 @@ class ModuleExtract(interfaces.plugins.PluginInterface): with self.open(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(base_address), - len(elf_data), - file_handle.preferred_filename, + yield ( + 0, + ( + format_hints.Hex(base_address), + len(elf_data), + file_handle.preferred_filename, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/modxview.py b/volatility3/framework/plugins/linux/modxview.py index c1707d26f..d91f37587 100644 --- a/volatility3/framework/plugins/linux/modxview.py +++ b/volatility3/framework/plugins/linux/modxview.py @@ -1,181 +1,21 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import List, Dict, Iterator - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules - -from volatility3.framework import interfaces, deprecation, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols.linux import extensions -from volatility3.framework.constants import architectures -from volatility3.framework.symbols.linux.utilities import tainting +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import modxview vollog = logging.getLogger(__name__) -class Modxview(interfaces.plugins.PluginInterface): +class Modxview( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=modxview.Modxview, + removal_date="2026-06-07", +): """Centralize lsmod, check_modules and hidden_modules results to efficiently \ -spot modules presence and taints.""" +spot modules presence and taints (deprecated).""" _version = (1, 0, 0) _required_framework_version = (2, 17, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=architectures.LINUX_ARCHS, - ), - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherer_lsmod", - component=linux_utilities_modules.ModuleGathererLsmod, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherer_sysfs", - component=linux_utilities_modules.ModuleGathererSysFs, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherer_scanner", - component=linux_utilities_modules.ModuleGathererScanner, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) - ), - requirements.BooleanRequirement( - name="plain_taints", - description="Display the plain taints string for each module.", - optional=True, - default=False, - ), - ] - - @classmethod - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.flatten_run_modules_results, - replacement_version=(3, 0, 0), - removal_date="2025-09-25", - ) - def flatten_run_modules_results( - cls, run_results: Dict[str, List[extensions.module]], deduplicate: bool = True - ) -> Iterator[extensions.module]: - """Flatten a dictionary mapping plugin names and modules list, to a single merged list. - This is useful to get a generic lookup list of all the detected modules. - - Args: - run_results: dictionary of plugin names mapping a list of detected modules - deduplicate: remove duplicate modules, based on their offsets - - Returns: - Iterator of modules objects - """ - return linux_utilities_modules.Modules.flatten_run_modules_results( - run_results, deduplicate - ) - - @classmethod - @deprecation.deprecated_method( - replacement=linux_utilities_modules.Modules.run_modules_scanners, - replacement_version=(3, 0, 0), - removal_date="2025-09-25", - ) - def run_modules_scanners( - cls, - context: interfaces.context.ContextInterface, - kernel_name: str, - run_hidden_modules: bool = True, - ) -> Dict[str, List[extensions.module]]: - """Run module scanning plugins and aggregate the results. It is designed - to not operate any inter-plugin results triage.""" - return linux_utilities_modules.Modules.run_modules_scanners( - context, kernel_name, run_hidden_modules - ) - - def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - wanted_gatherers = [ - linux_utilities_modules.ModuleGathererLsmod, - linux_utilities_modules.ModuleGathererSysFs, - linux_utilities_modules.ModuleGathererScanner, - ] - - run_results = linux_utilities_modules.Modules.run_modules_scanners( - context=self.context, - kernel_module_name=self.config["kernel"], - caller_wanted_gatherers=wanted_gatherers, - flatten=False, - ) - - aggregated_modules = {} - # We want to be explicit on the plugins results we are interested in - for gatherer in wanted_gatherers: - # Iterate over each recovered module - for mod_info in run_results[gatherer.name]: - # Use offsets as unique keys, whether a module - # appears in many plugin runs or not - if aggregated_modules.get(mod_info.offset, None) is not None: - # Append the plugin to the list of originating plugins - aggregated_modules[mod_info.offset].append(gatherer.name) - else: - aggregated_modules[mod_info.offset] = [gatherer.name] - - for module_offset, gatherers in aggregated_modules.items(): - module = kernel.object("module", offset=module_offset, absolute=True) - - # Tainting parsing capabilities applied to the module - if self.config.get("plain_taints"): - taints = tainting.Tainting.get_taints_as_plain_string( - self.context, - self.config["kernel"], - module.taints, - True, - ) - else: - taints = ",".join( - tainting.Tainting.get_taints_parsed( - self.context, - self.config["kernel"], - module.taints, - True, - ) - ) - - yield ( - 0, - ( - module.get_name() or renderers.NotAvailableValue(), - format_hints.Hex(module_offset), - linux_utilities_modules.ModuleGathererLsmod.name in gatherers, - linux_utilities_modules.ModuleGathererSysFs.name in gatherers, - linux_utilities_modules.ModuleGathererScanner.name in gatherers, - taints or renderers.NotAvailableValue(), - ), - ) - - def run(self): - columns = [ - ("Name", str), - ("Address", format_hints.Hex), - ("In procfs", bool), - ("In sysfs", bool), - ("In scan", bool), - ("Taints", str), - ] - - return renderers.TreeGrid( - columns, - self._generator(), - ) diff --git a/volatility3/framework/plugins/linux/netfilter.py b/volatility3/framework/plugins/linux/netfilter.py index 9079adef9..741241039 100644 --- a/volatility3/framework/plugins/linux/netfilter.py +++ b/volatility3/framework/plugins/linux/netfilter.py @@ -1,806 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from dataclasses import dataclass, field -from abc import ABC, abstractmethod import logging - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from typing import Iterator, List, Tuple, Optional -from volatility3 import framework -from volatility3.framework import ( - constants, - interfaces, - renderers, - exceptions, - deprecation, -) -from volatility3.framework.renderers import format_hints -from volatility3.framework.configuration import requirements -from volatility3.framework.symbols.linux import network +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import netfilter vollog = logging.getLogger(__name__) -@dataclass -class Proto: - name: str - hooks: Tuple[str] = field(default_factory=tuple) - - -PROTO_NOT_IMPLEMENTED = Proto(name="UNSPEC") - -NF_INET_HOOKS = ("PRE_ROUTING", "LOCAL_IN", "FORWARD", "LOCAL_OUT", "POST_ROUTING") -NF_DEC_HOOKS = ( - "PRE_ROUTING", - "LOCAL_IN", - "FORWARD", - "LOCAL_OUT", - "POST_ROUTING", - "HELLO", - "ROUTE", -) -NF_ARP_HOOKS = ("IN", "OUT", "FORWARD") -NF_NETDEV_HOOKS = ("INGRESS", "EGRESS") -LARGEST_HOOK_NUMBER = max( - len(NF_INET_HOOKS), len(NF_DEC_HOOKS), len(NF_ARP_HOOKS), len(NF_NETDEV_HOOKS) -) - - -class AbstractNetfilter(ABC): - """Netfilter Abstract Base Classes handling details across various - Netfilter implementations, including constants, helpers, and common - routines. - """ - - PROTO_HOOKS = ( - PROTO_NOT_IMPLEMENTED, # NFPROTO_UNSPEC - Proto(name="INET", hooks=NF_INET_HOOKS), # From kernels 3.14 - Proto(name="IPV4", hooks=NF_INET_HOOKS), - Proto(name="ARP", hooks=NF_ARP_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="NETDEV", hooks=NF_NETDEV_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="BRIDGE", hooks=NF_INET_HOOKS), - PROTO_NOT_IMPLEMENTED, - PROTO_NOT_IMPLEMENTED, - Proto(name="IPV6", hooks=NF_INET_HOOKS), - PROTO_NOT_IMPLEMENTED, - Proto(name="DECNET", hooks=NF_DEC_HOOKS), # Removed in kernel 6.1 - ) - NF_MAX_HOOKS = LARGEST_HOOK_NUMBER + 1 - - def __init__( - self, context: interfaces.context.ContextInterface, kernel_module_name: str - ): - self._context = context - self.vmlinux = context.modules[kernel_module_name] - self.layer_name = self.vmlinux.layer_name - - # Set data sizes - self.ptr_size = self.vmlinux.get_type("pointer").size - self.list_head_size = self.vmlinux.get_type("list_head").size - - linuxutils_modulegatherers_required_version = ( - Netfilter._required_linuxutils_gatherers_version - ) - linuxutils_modulegatherers_current_version = ( - linux_utilities_modules.ModuleGatherers.version - ) - if not requirements.VersionRequirement.matches_required( - linuxutils_modulegatherers_required_version, - linuxutils_modulegatherers_current_version, - ): - raise exceptions.PluginRequirementException( - f"linux_utilities_modules.ModuleGatherer version not suitable: required {linuxutils_modulegatherers_required_version} found {linuxutils_modulegatherers_current_version}" - ) - - linux_net_required_version = Netfilter._required_linuxnet_version - linux_net_current_version = network.NetSymbols.version - if not requirements.VersionRequirement.matches_required( - linux_net_required_version, linux_net_current_version - ): - raise exceptions.PluginRequirementException( - f"symbols.linux.net.NetSymbols version not suitable: required {linux_net_required_version} found {linux_net_current_version}" - ) - - linux_utilities_modules_required_version = ( - Netfilter._required_linux_utilities_modules_version - ) - linux_utilities_modules_current_version = ( - linux_utilities_modules.Modules.version - ) - if not requirements.VersionRequirement.matches_required( - linux_utilities_modules_required_version, - linux_utilities_modules_current_version, - ): - raise exceptions.PluginRequirementException( - f"linux_utilities_modules.Modules version not suitable: required {linux_utilities_modules_required_version} found {linux_utilities_modules_current_version}" - ) - - symbol_table = context.symbol_space[self.vmlinux.symbol_table_name] - network.NetSymbols.apply(symbol_table) - - self.handlers = linux_utilities_modules.Modules.run_modules_scanners( - context=context, - kernel_module_name=kernel_module_name, - caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, - ) - - @classmethod - def run_all( - cls, context: interfaces.context.ContextInterface, kernel_module_name: str - ) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: - """It calls each subclass symtab_checks() to test the required - conditions to that specific kernel implementation. - - Args: - context: The volatility3 context on which to operate - kernel_module_name: The name of the table containing the kernel symbols - - Yields: - The kmsg records. Same as _run() - """ - vmlinux = context.modules[kernel_module_name] - - implementation_inst = None # type: ignore - for subclass in framework.class_subclasses(cls): - if not subclass.symtab_checks(vmlinux=vmlinux): - vollog.log( - constants.LOGLEVEL_VVVV, - "Netfilter implementation '%s' doesn't match this memory dump", - subclass.__name__, - ) - continue - - vollog.log( - constants.LOGLEVEL_VVVV, - "Netfilter implementation '%s' matches!", - subclass.__name__, - ) - implementation_inst = subclass( - context=context, kernel_module_name=kernel_module_name - ) - # More than one class could be executed for an specific kernel version - # For instance: Netfilter Ingress hooks - yield from implementation_inst._run() - - if implementation_inst is None: - vollog.error("Unsupported Netfilter kernel implementation") - - def _run(self) -> Iterator[Tuple[int, str, str, int, int, str, bool]]: - """Iterates over namespaces and protocols, executing various callbacks that - allow customization of the code to the specific data structure used in a - particular kernel implementation - - get_hooks_container(net, proto_name, hook_name) - It returns the data structure used in a specific kernel implementation - to store the hooks for a respective namespace and protocol, basically: - For Ingress hooks: - network_namespace[] -> net_device[] -> nf_hooks_ingress[] - For egress hooks: - network_namespace[] -> net_device[] -> nf_hooks_egress[] - For all the other Netfilter hooks: - <= 4.2.8 - nf_hooks[] - >= 4.3 - network_namespace[] -> nf.hooks[] - - get_hook_ops(hook_container, proto_idx, hook_idx) - Give the 'hook_container' got in get_hooks_container(), it - returns an iterable of 'nf_hook_ops' elements for a respective protocol - and hook type. - - Returns: - netns [int]: Network namespace id - proto_name [str]: Protocol name - hook_name [str]: Hook name - priority [int]: Priority - hook_ops_hook [int]: Hook address - module_name [str]: Linux kernel module name - hooked [bool]: "True" if the network stack has been hijacked - """ - for netns, net in self.get_net_namespaces(): - for proto_idx, proto_name, hook_idx, hook_name in self._proto_hook_loop(): - hooks_container = self.get_hooks_container(net, proto_name, hook_name) - - for hook_container in hooks_container: - for hook_ops in self.get_hook_ops( - hook_container, proto_idx, hook_idx - ): - if not hook_ops: - continue - - priority = int(hook_ops.priority) - hook_ops_hook = hook_ops.hook - module_info, symbol_name = ( - linux_utilities_modules.Modules.module_lookup_by_address( - self._context, - self.vmlinux.name, - self.handlers, - hook_ops_hook, - ) - ) - hooked = module_info is None - - yield netns, proto_name, hook_name, priority, hook_ops_hook, module_info, symbol_name, hooked - - @classmethod - @abstractmethod - def symtab_checks(cls, vmlinux: interfaces.context.ModuleInterface) -> bool: - """This method on each sublasss will be called to evaluate if the kernel - being analyzed fulfill the type & symbols requirements for the implementation. - The first class returning True will be instantiated and called via the - run() method. - - Returns: - bool: True if the kernel being analyzed fulfill the class requirements. - """ - - def _proto_hook_loop(self) -> Iterator[Tuple[int, str, int, str]]: - """Flattens the protocol families and hooks""" - for proto_idx, proto in enumerate(AbstractNetfilter.PROTO_HOOKS): - if proto == PROTO_NOT_IMPLEMENTED: - continue - if proto.name not in self.subscribed_protocols(): - # This protocol is not managed in this object - continue - for hook_idx, hook_name in enumerate(proto.hooks): - yield proto_idx, proto.name, hook_idx, hook_name - - def build_nf_hook_ops_array( - self, nf_hook_entries - ) -> Optional[interfaces.objects.ObjectInterface]: - """Function helper to build the nf_hook_ops array when it is not part of the - struct 'nf_hook_entries' definition. - - nf_hook_ops was stored adjacent in memory to the nf_hook_entry array, in the - new struct 'nf_hook_entries'. However, this 'nf_hooks_ops' array 'orig_ops' is - not part of the 'nf_hook_entries' struct. So, we need to calculate the offset. - - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; - } - """ - nf_hook_entry_size = self.vmlinux.get_type("nf_hook_entry").size - - try: - num_hook_entries = nf_hook_entries.num_hook_entries - except exceptions.InvalidAddressException: - return None - - orig_ops_addr = ( - nf_hook_entries.hooks.vol.offset + nf_hook_entry_size * num_hook_entries - ) - - if not self.vmlinux._context.layers[self.vmlinux.layer_name].is_valid( - orig_ops_addr - ): - return None - - orig_ops = self._context.object( - object_type=self.get_symbol_fullname("array"), - offset=orig_ops_addr, - subtype=self.vmlinux.get_type("pointer"), - layer_name=self.layer_name, - count=num_hook_entries, - ) - - return orig_ops - - def subscribed_protocols(self) -> Tuple[str]: - """Allows to select which PROTO_HOOKS protocols will be processed by the - Netfiler subclass. - """ - - # Most implementation handlers respond to these protocols, except for - # the ingress hook, which specifically handles the 'NETDEV' protocol. - # However, there is no corresponding Netfilter hook implementation for - # the INET protocol in the kernel. AFAIU, this is used as - # 'NFPROTO_INET = NFPROTO_IPV4 || NFPROTO_IPV6' - # in other parts of the kernel source code. - return ("IPV4", "ARP", "BRIDGE", "IPV6", "DECNET") - - @deprecation.method_being_removed( - removal_date="2025-09-25", - message="Callers to this method should adapt `linux_utilities_modules.Modules.run_module_scanners`", - ) - def get_module_name_for_address(self, addr) -> str: - """Helper to obtain the module and symbol name in the format needed for the - output of this plugin. - """ - module_name, symbol_name = ( - linux_utilities_modules.Modules.lookup_module_address( - self._context, self.vmlinux.name, self.handlers, addr - ) - ) - - if module_name == "UNKNOWN": - module_name = None - - if symbol_name != "N/A": - module_name = f"[{symbol_name}]" - - return module_name - - def get_net_namespaces(self): - """Common function to retrieve the different namespaces. - From 4.3 on, all the implementations use network namespaces. - """ - nethead = self.vmlinux.object_from_symbol("net_namespace_list") - symbol_net_name = self.get_symbol_fullname("net") - for net in nethead.to_list(symbol_net_name, "list"): - net_ns_id = net.ns.inum - yield net_ns_id, net - - def get_hooks_container(self, net, proto_name, hook_name): - """Returns the data structure used in a specific kernel implementation to store - the hooks for a respective namespace and protocol. - - Except for kernels < 4.3, all the implementations use network namespaces. - Also the data structure which contains the hooks, even though it changes its - implementation and/or data type, it is always in this location. - """ - yield net.nf.hooks - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - """Given the hook_container obtained from get_hooks_container(), it - returns an iterable of 'nf_hook_ops' elements for a corresponding protocol - and hook type. - - This is the most variable/unstable part of all Netfilter hook designs, it - changes almost in every single implementation. - """ - raise NotImplementedError("You must implement this method") - - def get_symbol_fullname(self, symbol_basename: str) -> str: - """Given a short symbol or type name, it returns its full name""" - return self.vmlinux.symbol_table_name + constants.BANG + symbol_basename - - @staticmethod - def get_member_type( - vol_type: interfaces.objects.Template, member_name: str - ) -> List[str]: - """Returns a list of types/subtypes belonging to the given type member. - - Args: - vol_type (interfaces.objects.Template): A vol3 type object - member_name (str): The member name - - Returns: - list: A list of types/subtypes - """ - _size, vol_obj = vol_type.vol.members[member_name] - type_name = vol_obj.type_name - type_basename = type_name.split(constants.BANG)[1] - member_type = [type_basename] - cur_type = vol_obj - while hasattr(cur_type, "subtype"): - subtype_name = cur_type.subtype.type_name - subtype_basename = subtype_name.split(constants.BANG)[1] - member_type.append(subtype_basename) - cur_type = cur_type.subtype - - return member_type - - -class NetfilterImp_to_4_3(AbstractNetfilter): - """At this point, Netfilter hooks were implemented as a linked list of struct - 'nf_hook_ops' type. One linked list per protocol per hook type. - It was like that until 4.2.8. - - struct list_head nf_hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_symbol("nf_hooks") - - def get_net_namespaces(self): - # In kernels <= 4.2.8 netfilter hooks are not implemented per namespaces - netns, net = renderers.NotAvailableValue(), renderers.NotAvailableValue() - yield netns, net - - def get_hooks_container(self, net, proto_name, hook_name): - nf_hooks = self.vmlinux.object_from_symbol("nf_hooks") - if not nf_hooks: - return - - yield nf_hooks - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - -class NetfilterImp_4_3_to_4_9(AbstractNetfilter): - """Netfilter hooks were added to network namepaces in 4.3. - It is still implemented as a linked list of 'struct nf_hook_ops' type but inside a - network namespace. One linked list per protocol per hook type. - - struct net { ... struct netns_nf nf; ... } - struct netns_nf { ... - struct list_head hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") - == ["array", "array", "list_head"] - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - -class NetfilterImp_4_9_to_4_14(AbstractNetfilter): - """In this range of kernel versions, the doubly-linked lists of netfilter hooks were - replaced by an array of arrays of 'nf_hook_entry' pointers in a singly-linked lists. - struct net { ... struct netns_nf nf; ... } - struct netns_nf { .. - struct nf_hook_entry __rcu *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - - Also in v4.10 the struct nf_hook_entry changed, a hook function pointer was added to - it. However, for simplicity of this design, we will still take the hook address from - the 'nf_hook_ops'. As per v5.0-rc2, the hook address is duplicated in both sides. - - v4.9: - struct nf_hook_entry { - struct nf_hook_entry *next; - struct nf_hook_ops ops; - const struct nf_hook_ops *orig_ops; }; - - v4.10: - struct nf_hook_entry { - struct nf_hook_entry *next; - nf_hookfn *hook; - void *priv; - const struct nf_hook_ops *orig_ops; }; - (*) Even though the hook address is in the struct 'nf_hook_entry', we use the - original 'nf_hook_ops' hook address value, the one which was filled by the user, to - make it uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["array", "array", "pointer", "nf_hook_entry"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type - ) - - def _get_hook_ops(self, hook_container, proto_idx, hook_idx): - list_head = hook_container[proto_idx][hook_idx] - nf_hooks_ops_name = self.get_symbol_fullname("nf_hook_ops") - return list_head.to_list(nf_hooks_ops_name, "list") - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entry_list = hook_container[proto_idx][hook_idx] - while nf_hook_entry_list: - yield nf_hook_entry_list.orig_ops - nf_hook_entry_list = nf_hook_entry_list.next - - -class NetfilterImp_4_14_to_4_16(AbstractNetfilter): - """'nf_hook_ops' was removed from struct 'nf_hook_entry'. Instead, it was stored - adjacent in memory to the 'nf_hook_entry' array, in the new struct 'nf_hook_entries' - However, 'orig_ops' is not part of the 'nf_hook_entries' struct definition. So, we - have to craft it by hand. - - struct net { ... struct netns_nf nf; ... } - struct netns_nf { - struct nf_hook_entries *hooks[NFPROTO_NUMPROTO][NF_MAX_HOOKS]; ... } - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - struct nf_hook_entry { - nf_hookfn *hook; - void *priv; } - - (*) Even though the hook address is in the struct 'nf_hook_entry', we use the - original 'nf_hook_ops' hook address value, the one which was filled by the user, to - make it uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["array", "array", "pointer", "nf_hook_entries"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks") - and cls.get_member_type(vmlinux.get_type("netns_nf"), "hooks") == hooks_type - ) - - def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): - """This allows to support different hook array implementations from this version - on. For instance, in kernels >= 4.16 this multi-dimensional array is split in - one-dimensional array of pointers to 'nf_hooks_entries' per each protocol.""" - return nf_hooks_addr[proto_idx][hook_idx] - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entries = self.get_nf_hook_entries(hook_container, proto_idx, hook_idx) - if not nf_hook_entries: - return - - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) - if not nf_hook_ops_ptr_arr: - return - - for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: - nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) - yield nf_hook_ops - - -class NetfilterImp_4_16_to_latest(NetfilterImp_4_14_to_4_16): - """The multidimensional array of nf_hook_entries was split in a one-dimensional - array per each protocol. - - struct net { - struct netns_nf nf; ... } - struct netns_nf { - struct nf_hook_entries * hooks_ipv4[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_ipv6[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_arp[NF_ARP_NUMHOOKS]; - struct nf_hook_entries * hooks_bridge[NF_INET_NUMHOOKS]; - struct nf_hook_entries * hooks_decnet[NF_DN_NUMHOOKS]; ... } - struct nf_hook_entries { - u16 num_hook_entries; /* plus padding */ - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - struct nf_hook_entry { - nf_hookfn *hook; - void *priv; } - - (*) Even though the hook address is in the struct nf_hook_entry, we use the original - nf_hook_ops hook address value, the one which was filled by the user, to make it - uniform to all the implementations. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("netns_nf") - and vmlinux.get_type("netns_nf").has_member("hooks_ipv4") - ) - - def get_hooks_container(self, net, proto_name, hook_name): - try: - if proto_name == "IPV4": - net_nf_hooks = net.nf.hooks_ipv4 - elif proto_name == "ARP": - net_nf_hooks = net.nf.hooks_arp - elif proto_name == "BRIDGE": - net_nf_hooks = net.nf.hooks_bridge - elif proto_name == "IPV6": - net_nf_hooks = net.nf.hooks_ipv6 - elif proto_name == "DECNET": - net_nf_hooks = net.nf.hooks_decnet - else: - return - - yield net_nf_hooks - - except AttributeError: - # Protocol family disabled at kernel compilation - # CONFIG_NETFILTER_FAMILY_ARP=n || - # CONFIG_NETFILTER_FAMILY_BRIDGE=n || - # CONFIG_DECNET=n - pass - - def _get_nf_hook_entries_ptr(self, nf_hooks_addr, proto_idx, hook_idx): - nf_hook_entries_ptr = nf_hooks_addr[hook_idx] - return nf_hook_entries_ptr - - def get_nf_hook_entries(self, nf_hooks_addr, proto_idx, hook_idx): - return nf_hooks_addr[hook_idx] - - -class AbstractNetfilterNetDev(AbstractNetfilter): - """Base class to handle the Netfilter NetDev hooks. - It won't be executed. It has some common functions to all Netfilter NetDev hook - implementions. - - Netfilter NetDev hooks are set per network device which belongs to a network - namespace. - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - return False - - def subscribed_protocols(self): - return ("NETDEV",) - - def get_hooks_container(self, net, proto_name, hook_name): - net_device_type = self.vmlinux.get_type("net_device") - net_device_name = self.get_symbol_fullname("net_device") - for net_device in net.dev_base_head.to_list(net_device_name, "dev_list"): - if hook_name == "INGRESS": - if net_device_type.has_member("nf_hooks_ingress"): - # CONFIG_NETFILTER_INGRESS=y - yield net_device.nf_hooks_ingress - - elif hook_name == "EGRESS": - if net_device_type.has_member("nf_hooks_egress"): - # CONFIG_NETFILTER_EGRESS=y - yield net_device.nf_hooks_egress - - -class NetfilterNetDevImp_4_2_to_4_9(AbstractNetfilterNetDev): - """This is the first version of Netfilter Ingress hooks which was implemented using - a doubly-linked list of 'nf_hook_ops'. - struct list_head nf_hooks_ingress; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["list_head"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hooks_ingress = hook_container - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - return nf_hooks_ingress.to_list(nf_hook_ops_name, "list") - - -class NetfilterNetDevImp_4_9_to_4_14(AbstractNetfilterNetDev): - """In 4.9 it was changed to a simple singly-linked list. - struct nf_hook_entry * nf_hooks_ingress; - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["pointer", "nf_hook_entry"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hooks_ingress_ptr = hook_container - if not nf_hooks_ingress_ptr: - return - - while nf_hooks_ingress_ptr: - nf_hook_entry = nf_hooks_ingress_ptr.dereference() - orig_ops = nf_hook_entry.orig_ops.dereference() - yield orig_ops - nf_hooks_ingress_ptr = nf_hooks_ingress_ptr.next - - -class NetfilterNetDevImp_4_14_to_latest(AbstractNetfilterNetDev): - """In 4.14 the hook list was converted to an array of pointers inside the struct - 'nf_hook_entries': - struct nf_hook_entries * nf_hooks_ingress; - struct nf_hook_entries { - u16 num_hook_entries; - struct nf_hook_entry hooks[]; - //const struct nf_hook_ops *orig_ops[]; } - """ - - @classmethod - def symtab_checks(cls, vmlinux) -> bool: - hooks_type = ["pointer", "nf_hook_entries"] - return ( - vmlinux.has_symbol("net_namespace_list") - and vmlinux.has_type("net_device") - and vmlinux.get_type("net_device").has_member("nf_hooks_ingress") - and cls.get_member_type(vmlinux.get_type("net_device"), "nf_hooks_ingress") - == hooks_type - ) - - def get_hook_ops(self, hook_container, proto_idx, hook_idx): - nf_hook_entries = hook_container - if not nf_hook_entries: - return - - nf_hook_ops_name = self.get_symbol_fullname("nf_hook_ops") - nf_hook_ops_ptr_arr = self.build_nf_hook_ops_array(nf_hook_entries) - if not nf_hook_ops_ptr_arr: - return - - for nf_hook_ops_ptr in nf_hook_ops_ptr_arr: - nf_hook_ops = nf_hook_ops_ptr.dereference().cast(nf_hook_ops_name) - yield nf_hook_ops - - -class Netfilter(interfaces.plugins.PluginInterface): - """Lists Netfilter hooks.""" - - _required_framework_version = (2, 22, 0) +class Netfilter( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=netfilter.Netfilter, + removal_date="2026-06-07", +): + """Lists Netfilter hooks (deprecated).""" _version = (2, 0, 0) - - _required_linux_utilities_modules_version = (3, 0, 0) - _required_linuxutils_gatherers_version = (1, 0, 0) - _required_linuxnet_version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherers", - component=linux_utilities_modules.ModuleGatherers, - version=cls._required_linuxutils_gatherers_version, - ), - requirements.VersionRequirement( - name="linuxnet", - component=network.NetSymbols, - version=cls._required_linuxnet_version, - ), - ] - - def _format_fields(self, fields): - ( - netns, - proto_name, - hook_name, - priority, - hook_func, - module_info, - symbol_name, - hooked, - ) = fields - - if module_info: - module_name = module_info.name - else: - module_name = renderers.NotAvailableValue() - - return ( - netns, - proto_name, - hook_name, - priority, - format_hints.Hex(hook_func), - module_name, - symbol_name or renderers.NotAvailableValue(), - str(hooked), - ) - - def _generator(self): - kernel_module_name = self.config["kernel"] - for fields in AbstractNetfilter.run_all( - context=self.context, kernel_module_name=kernel_module_name - ): - yield (0, self._format_fields(fields)) - - def run(self): - headers = [ - ("Net NS", int), - ("Proto", str), - ("Hook", str), - ("Priority", int), - ("Handler", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ("Is Hooked", str), - ] - return renderers.TreeGrid(headers, self._generator()) + _required_framework_version = (2, 22, 0) diff --git a/volatility3/framework/plugins/linux/pagecache.py b/volatility3/framework/plugins/linux/pagecache.py index 1fd96d5d2..2d20a2fb1 100644 --- a/volatility3/framework/plugins/linux/pagecache.py +++ b/volatility3/framework/plugins/linux/pagecache.py @@ -386,7 +386,11 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface): inode_out = inode_in.to_user(vmlinux_layer) description = f"Cached Inode for {inode_out.path}" yield description, timeliner.TimeLinerType.ACCESSED, inode_out.access_time - yield description, timeliner.TimeLinerType.MODIFIED, inode_out.modification_time + yield ( + description, + timeliner.TimeLinerType.MODIFIED, + inode_out.modification_time, + ) yield description, timeliner.TimeLinerType.CHANGED, inode_out.change_time @classmethod @@ -813,7 +817,6 @@ class RecoverFs(plugins.PluginInterface): visited_paths = seen_prefixes = set() for inode_in in inodes_iter: - # Code is slightly duplicated here with the if-block below. # However this prevents unneeded tar manipulation if fifo # or sock inodes come through for example. diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index 9b31976ec..71caa0853 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -225,18 +225,21 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): task_euid = self._format_cred(task_fields.euid) task_egid = self._format_cred(task_fields.egid) - yield 0, ( - format_hints.Hex(task_fields.offset), - task_fields.user_pid, - task_fields.user_tid, - task_fields.user_ppid, - task_fields.name, - task_uid, - task_gid, - task_euid, - task_egid, - task_fields.creation_time or renderers.NotAvailableValue(), - file_output, + yield ( + 0, + ( + format_hints.Hex(task_fields.offset), + task_fields.user_pid, + task_fields.user_tid, + task_fields.user_ppid, + task_fields.name, + task_uid, + task_gid, + task_euid, + task_egid, + task_fields.creation_time or renderers.NotAvailableValue(), + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py index a6acf825b..a74e84f92 100644 --- a/volatility3/framework/plugins/linux/sockstat.py +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -475,12 +475,6 @@ class Sockstat(plugins.PluginInterface): requirements.VersionRequirement( name="linux_net", component=network.NetSymbols, version=(1, 0, 0) ), - requirements.BooleanRequirement( - name="unix", - description=("Show UNIX domain Sockets only"), - default=False, - optional=True, - ), requirements.ListRequirement( name="pids", description="Filter results by process IDs. " diff --git a/volatility3/framework/plugins/linux/tracing/ftrace.py b/volatility3/framework/plugins/linux/tracing/ftrace.py index afcc71784..4b7c2ebb3 100644 --- a/volatility3/framework/plugins/linux/tracing/ftrace.py +++ b/volatility3/framework/plugins/linux/tracing/ftrace.py @@ -48,7 +48,7 @@ class FtraceOpsFlags(Enum): @dataclass class ParsedFtraceOps: """Parsed ftrace_ops struct representation, containing a selection of forensics valuable - informations.""" + information.""" ftrace_ops_offset: int callback_symbol: str diff --git a/volatility3/framework/plugins/linux/tracing/perf_events.py b/volatility3/framework/plugins/linux/tracing/perf_events.py index 3e0a40579..ff922784d 100644 --- a/volatility3/framework/plugins/linux/tracing/perf_events.py +++ b/volatility3/framework/plugins/linux/tracing/perf_events.py @@ -18,7 +18,7 @@ class PerfEvents(plugins.PluginInterface): """Lists performance events for each process.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -28,8 +28,8 @@ class PerfEvents(plugins.PluginInterface): description="Linux kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="pslist", plugin=pslist.PsList, version=(4, 0, 0) + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(4, 0, 0) ), ] @@ -70,7 +70,6 @@ class PerfEvents(plugins.PluginInterface): for task in pslist.PsList.list_tasks( context, vmlinux_module_name, include_threads=True ): - # walk the list of perf_event entries for this process for event in task.perf_event_list.to_list( vmlinux.symbol_table_name + constants.BANG + "perf_event", "owner_entry" diff --git a/volatility3/framework/plugins/linux/tracing/tracepoints.py b/volatility3/framework/plugins/linux/tracing/tracepoints.py index 25c87b664..8666ffd6a 100644 --- a/volatility3/framework/plugins/linux/tracing/tracepoints.py +++ b/volatility3/framework/plugins/linux/tracing/tracepoints.py @@ -21,7 +21,7 @@ vollog = logging.getLogger(__name__) @dataclass class ParsedTracepointFunc: """Parsed tracepoint_func struct, containing a selection of forensics valuable - informations.""" + information.""" tracepoint_name: str tracepoint_address: int diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index 7d30b84ee..36bfb1b5a 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -1,118 +1,20 @@ -# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List - -import volatility3.framework.symbols.linux.utilities.modules as linux_utilities_modules -from volatility3.framework import interfaces, renderers, exceptions, constants -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import linux +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.linux.malware import tty_check as ttycheck vollog = logging.getLogger(__name__) -class tty_check(plugins.PluginInterface): - """Checks tty devices for hooks""" +class tty_check( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=ttycheck.Tty_Check, + removal_date="2026-06-07", +): + """Checks tty devices for hooks (deprecated).""" _required_framework_version = (2, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="linux_utilities_modules", - component=linux_utilities_modules.Modules, - version=(3, 0, 0), - ), - requirements.VersionRequirement( - name="linux_utilities_module_gatherers", - component=linux_utilities_modules.ModuleGatherers, - version=(1, 0, 0), - ), - requirements.VersionRequirement( - name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) - ), - ] - - def _generator(self): - vmlinux = self.context.modules[self.config["kernel"]] - - try: - tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") - except exceptions.SymbolError: - tty_drivers = None - - if not tty_drivers: - raise TypeError( - "This plugin requires the tty_drivers structure." - "This structure is not present in the supplied symbol table." - "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." - ) - - known_modules = linux_utilities_modules.Modules.run_modules_scanners( - context=self.context, - kernel_module_name=self.config["kernel"], - caller_wanted_gatherers=linux_utilities_modules.ModuleGatherers.all_gatherers_identifier, - ) - - for tty in tty_drivers.to_list( - vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" - ): - try: - ttys = utility.array_of_pointers( - tty.ttys.dereference(), - count=tty.num, - subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct", - context=self.context, - ) - except exceptions.PagedInvalidAddressException: - continue - - for tty_dev in ttys: - if tty_dev == 0: - continue - - try: - name = utility.array_to_string(tty_dev.name) - recv_buf = tty_dev.ldisc.ops.receive_buf - except exceptions.InvalidAddressException: - continue - - module_info, symbol_name = ( - linux_utilities_modules.Modules.module_lookup_by_address( - self.context, vmlinux.name, known_modules, recv_buf - ) - ) - - if module_info: - module_name = module_info.name - else: - module_name = renderers.NotAvailableValue() - - yield 0, ( - name, - format_hints.Hex(recv_buf), - module_name, - symbol_name or renderers.NotAvailableValue(), - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Name", str), - ("Address", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ], - self._generator(), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/linux/vmaregexscan.py b/volatility3/framework/plugins/linux/vmaregexscan.py index 37a1a5940..2fde3aadc 100644 --- a/volatility3/framework/plugins/linux/vmaregexscan.py +++ b/volatility3/framework/plugins/linux/vmaregexscan.py @@ -64,7 +64,6 @@ class VmaRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for task in tasks: - if not task.mm: continue name = utility.array_to_string(task.comm) @@ -91,7 +90,7 @@ class VmaRegExScan(plugins.PluginInterface): ): result_data = proc_layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) - # reapply the regex in order to extact just the match + # reapply the regex in order to extract just the match regex_result = re.match(regex_pattern, result_data) if regex_result: @@ -106,12 +105,15 @@ class VmaRegExScan(plugins.PluginInterface): bytes_result = result_data user_pid = task.tgid - yield 0, ( - user_pid, - name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + user_pid, + name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py index 64f5827c1..d9466f512 100644 --- a/volatility3/framework/plugins/linux/vmayarascan.py +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -103,12 +103,15 @@ class VmaYaraScan(interfaces.plugins.PluginInterface): layer_name=proc_layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.tgid, - rule_name, - name, - layer_data, + yield ( + 0, + ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + layer_data, + ), ) @classmethod diff --git a/volatility3/framework/plugins/mac/lsmod.py b/volatility3/framework/plugins/mac/lsmod.py index c6f57f889..05a0ee72f 100644 --- a/volatility3/framework/plugins/mac/lsmod.py +++ b/volatility3/framework/plugins/mac/lsmod.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's lsmod command.""" + from typing import Set from volatility3.framework import renderers, interfaces, exceptions diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index 0f3aa745c..3d9dcd916 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -3,6 +3,7 @@ # """A module containing a collection of plugins that produce data typically found in Mac's mount command.""" + from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins diff --git a/volatility3/framework/plugins/mac/psaux.py b/volatility3/framework/plugins/mac/psaux.py index ba9b7b5f6..bdcfb1466 100644 --- a/volatility3/framework/plugins/mac/psaux.py +++ b/volatility3/framework/plugins/mac/psaux.py @@ -2,6 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # """In-memory artifacts from OSX systems.""" + from typing import Iterator, Tuple, Any, Generator, List from volatility3.framework import exceptions, renderers, interfaces diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index 19f4516ac..904e4e201 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -56,7 +56,7 @@ class PsList(interfaces.plugins.PluginInterface): """Returns the list_tasks method based on the selector Args: - method: Must be one fo the available methods in get_task_choices + method: Must be one of the available methods in get_task_choices Returns: list_tasks method for listing tasks diff --git a/volatility3/framework/plugins/mac/timers.py b/volatility3/framework/plugins/mac/timers.py index 8a267bd55..aef8c5e1c 100644 --- a/volatility3/framework/plugins/mac/timers.py +++ b/volatility3/framework/plugins/mac/timers.py @@ -31,8 +31,8 @@ class Timers(plugins.PluginInterface): requirements.VersionRequirement( name="macutils", component=mac.MacUtilities, version=(1, 3, 0) ), - requirements.PluginRequirement( - name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + requirements.VersionRequirement( + name="lsmod", component=lsmod.Lsmod, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/regexscan.py b/volatility3/framework/plugins/regexscan.py index 343753e92..2c188ba4c 100644 --- a/volatility3/framework/plugins/regexscan.py +++ b/volatility3/framework/plugins/regexscan.py @@ -46,38 +46,52 @@ class RegExScan(plugins.PluginInterface): ), ] - def _generator(self, regex_pattern): - regex_pattern = bytes(regex_pattern, "UTF-8") - vollog.debug(f"RegEx Pattern: {regex_pattern}") + def _generator(self, context, layer_name, pattern, maxsize): + layer = self.context.layers[layer_name] + vollog.debug(f"RegEx Pattern: {pattern}") + + # Convert string pattern to bytes for RegExScanner + pattern_bytes = pattern.encode("utf-8") + + # Compile the pattern here to ensure consistency + try: + compiled_pattern = re.compile(pattern_bytes) + except re.error as e: + vollog.error(f"Invalid regex pattern: {e}") + raise ValueError(f"Invalid regex pattern: {e}") - layer = self.context.layers[self.config["primary"]] for offset in layer.scan( - context=self.context, scanner=scanners.RegExScanner(regex_pattern) + context=context, scanner=scanners.RegExScanner(pattern_bytes) ): - result_data = layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) + result_data = layer.read(offset, maxsize, pad=True) - # reapply the regex in order to extact just the match - regex_result = re.match(regex_pattern, result_data) + # reapply the regex in order to extract just the match + regex_result = compiled_pattern.search(result_data) if regex_result: - # the match is within the results_data (e.g. it fits within MAXSIZE_DEFAULT) + # the match is within the results_data (e.g. it fits within maxsize) # extract just the match itself regex_match = regex_result.group(0) text_result = str(regex_match, encoding="UTF-8", errors="replace") bytes_result = regex_match else: - # the match is not with the results_data (e.g. it doesn't fit within MAXSIZE_DEFAULT) + # the match is not with the results_data (e.g. it doesn't fit within maxsize) text_result = str(result_data, encoding="UTF-8", errors="replace") bytes_result = result_data yield 0, (format_hints.Hex(offset), text_result, bytes_result) def run(self): + pattern = self.config.get("pattern") + maxsize = self.config.get("maxsize", self.MAXSIZE_DEFAULT) + layer_name = self.config["primary"] + context = self.context + return renderers.TreeGrid( [ ("Offset", format_hints.Hex), ("Text", str), ("Hex", bytes), ], - self._generator(self.config.get("pattern")), + self._generator(context, layer_name, pattern, maxsize), ) diff --git a/volatility3/framework/plugins/renderers/parquet_renderer.py b/volatility3/framework/plugins/renderers/parquet_renderer.py new file mode 100644 index 000000000..f4eaf9c9b --- /dev/null +++ b/volatility3/framework/plugins/renderers/parquet_renderer.py @@ -0,0 +1,218 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +import datetime +import logging +import sys +from typing import ( + Any, + Dict, + List, + Optional, + Tuple, + TextIO, +) +from volatility3.framework import interfaces, renderers +from volatility3.framework.renderers import format_hints +from volatility3.cli import text_renderer + +vollog = logging.getLogger(__name__) + +ARROW_PRESENT = False +try: + import pyarrow as pa + import pyarrow.parquet as pq + + ARROW_PRESENT = True +except ImportError: + vollog.debug("Arrow/Parquet libraries not found") + + +class ArrowRenderer(text_renderer.CLIRenderer): + """Renderer that outputs Arrow IPC format data.""" + + name = "arrow" + structured_output = True + _version = (1, 0, 0) + + def __init__( + self, options: Optional[List[interfaces.renderers.RenderOption]] = None + ) -> None: + super().__init__(options) + + if not ARROW_PRESENT: + raise RuntimeError("Arrow output format requires the pyarrow package") + + self._to_arrow_type = { + renderers.Disassembly: pa.utf8, + bool: pa.bool_, + int: pa.int64, + float: pa.float64, + str: pa.utf8, + datetime.datetime: lambda: pa.timestamp("ms"), + format_hints.Bin: pa.uint64, + format_hints.Hex: pa.uint64, + format_hints.MultiTypeData: pa.utf8, + format_hints.HexBytes: pa.binary, + renderers.LayerData: pa.binary, + bytes: pa.binary, + } + + # indicates if the output from the plugin is nested, e.g., pstree + # which would then need to be flattened + self._is_tree_result = False + self._node_id_counter = 0 + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def to_arrow_schema(self, grid: interfaces.renderers.TreeGrid) -> "pa.Schema": + fields = [] + for column in grid.columns: + arrow_type = self._to_arrow_type[column.type] + fields.append(pa.field(column.name, arrow_type())) + + # if the output is nested, e.g., windows.pstree + if self._is_tree_result: + fields.append(pa.field("_vol_id", pa.uint64())) + fields.append(pa.field("_vol_parent_id", pa.uint64())) + + return pa.schema(fields) + + def _flatten_tree_structure(self, nested: List[Dict]) -> List[Dict]: + """ + Flattens a list of nested dicts using the `__children` key. + + Each node gets a `_vol_id` and a `_vol_parent_id` to preserve + the original tree structure in a flat format suitable for tabular output. + + Args: + nested: A list of dicts with optional `__children` lists (tree nodes). + + Returns: + A flat list of dicts with `_vol_id` and `_vol_parent_id`. + """ + rows = [] + self._node_id_counter = 0 + + def _process_node(node: Dict, parent_id: Optional[int]): + current_id = self._node_id_counter + self._node_id_counter += 1 + + entry = {k: v for k, v in node.items() if k != "__children"} + entry["_vol_id"] = current_id + entry["_vol_parent_id"] = parent_id + rows.append(entry) + + for child in node.get("__children", []): + _process_node(child, current_id) + + for root in nested: + _process_node(root, None) + + return rows + + def output_result(self, schema: "pa.Schema", outfd: TextIO, result): + """Outputs the JSON data to a file in a particular format""" + + if self._is_tree_result: + result = self._flatten_tree_structure(result) + + t = pa.Table.from_pylist(result, schema=schema) + self.write_table(t, outfd) + + def write_table(self, t: "pa.Table", outfd: TextIO) -> None: + buf = pa.BufferOutputStream() + + writer = pa.ipc.new_stream(buf, t.schema) + writer.write_table(t) + writer.close() + + # Get the buffer bytes and write to output + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) + + def render(self, grid: interfaces.renderers.TreeGrid): + outfd = sys.stdout + final_output: Tuple[ + Dict[str, List[interfaces.renderers.TreeNode]], + List[interfaces.renderers.TreeNode], + ] = ({}, []) + + ignore_columns = self.ignored_columns(grid) + + def visitor( + node: interfaces.renderers.TreeNode, + accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], + ) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]: + # Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case + acc_map, final_tree = accumulator + node_dict: Dict[str, Any] = {"__children": []} + line = [] + for column_index, column in enumerate(grid.columns): + if column in ignore_columns: + continue + + data = list(node.values)[column_index] + + if isinstance(data, interfaces.renderers.BaseAbsentValue): + data = None + + if isinstance(data, renderers.Disassembly): + data = text_renderer.display_disassembly(data) + + if isinstance(data, renderers.LayerData): + data = text_renderer.LayerDataRenderer().render_bytes(data)[0] + + node_dict[column.name] = data + line.append(data) + + if self.filter and self.filter.filter(line): + return accumulator + + if node.parent: + acc_map[node.parent.path]["__children"].append(node_dict) + self._is_tree_result = True + else: + final_tree.append(node_dict) + acc_map[node.path] = node_dict + + return (acc_map, final_tree) + + if not grid.populated: + grid.populate(visitor, final_output) + else: + grid.visit(node=None, function=visitor, initial_accumulator=final_output) + + schema = self.to_arrow_schema(grid) + self.output_result(schema, outfd, final_output[1]) + + +class ParquetRenderer(ArrowRenderer): + """Renderer that outputs Parquet format data.""" + + name = "parquet" + structured_output = True + _version = (1, 0, 0) + + def get_render_options(self) -> List[interfaces.renderers.RenderOption]: + return [] + + def write_table(self, table: "pa.Table", outfd: TextIO) -> None: + """ + Writes a table to stdout using the Parquet format. + + Args: + t: The Arrow table to write + outfd: The output file descriptor + + Returns: + Nothing + """ + # Write DataFrame to a temporary file-like object + buf = pa.BufferOutputStream() + pq.write_table(table, buf, compression="snappy") + + # Get the buffer as a bytes object + buf_bytes = buf.getvalue().to_pybytes() + outfd.buffer.write(buf_bytes) diff --git a/volatility3/framework/plugins/vmscan.py b/volatility3/framework/plugins/vmscan.py index 5322456b5..19d997605 100644 --- a/volatility3/framework/plugins/vmscan.py +++ b/volatility3/framework/plugins/vmscan.py @@ -54,7 +54,7 @@ class PageStartScanner(interfaces.layers.ScannerInterface): class Vmscan(plugins.PluginInterface): - """Scans for Intel VT-d structues and generates VM volatility configs for them""" + """Scans for Intel VT-d structures and generates VM volatility configs for them""" _required_framework_version = (2, 2, 0) _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/amcache.py b/volatility3/framework/plugins/windows/amcache.py index be144be91..0d1450127 100644 --- a/volatility3/framework/plugins/windows/amcache.py +++ b/volatility3/framework/plugins/windows/amcache.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import amcache vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Amcache( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=amcache.Amcache, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Extract information on executed applications from the AmCache (deprecated).""" diff --git a/volatility3/framework/plugins/windows/cachedump.py b/volatility3/framework/plugins/windows/cachedump.py index 35127c6f3..3c474bc2c 100644 --- a/volatility3/framework/plugins/windows/cachedump.py +++ b/volatility3/framework/plugins/windows/cachedump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import cachedump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Cachedump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=cachedump.Cachedump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index b8c9fe751..2a65f76bb 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -78,7 +78,6 @@ class Callbacks(interfaces.plugins.PluginInterface): def _create_default_scan_constraints( context: interfaces.context.ContextInterface, symbol_table: str ) -> List[poolscanner.PoolConstraint]: - shutdown_packet_size = context.symbol_space.get_type( symbol_table + constants.BANG + "_SHUTDOWN_PACKET" ).size @@ -590,7 +589,11 @@ class Callbacks(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: component = renderers.UnreadableValue() - yield "KeBugCheckReasonCallbackListHead", callback.CallbackRoutine, component + yield ( + "KeBugCheckReasonCallbackListHead", + callback.CallbackRoutine, + component, + ) @classmethod def list_bugcheck_callbacks( diff --git a/volatility3/framework/plugins/windows/deskscan.py b/volatility3/framework/plugins/windows/deskscan.py index 35430be5d..202eaf330 100644 --- a/volatility3/framework/plugins/windows/deskscan.py +++ b/volatility3/framework/plugins/windows/deskscan.py @@ -77,6 +77,11 @@ class DeskScan(desktops.Desktops): continue for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), winsta_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + winsta_name, + session_id, + desktop_name, + process_name, + process_pid, + ) diff --git a/volatility3/framework/plugins/windows/desktops.py b/volatility3/framework/plugins/windows/desktops.py index c6557085e..f2a985c6b 100644 --- a/volatility3/framework/plugins/windows/desktops.py +++ b/volatility3/framework/plugins/windows/desktops.py @@ -63,9 +63,14 @@ class Desktops(interfaces.plugins.PluginInterface): for desktop, desktop_name in winsta.desktops(kernel.symbol_table_name): # for each desktop, walk its threads for _thread, process_name, process_pid in desktop.get_threads(): - yield format_hints.Hex( - desktop.vol.offset - ), station_name, session_id, desktop_name, process_name, process_pid + yield ( + format_hints.Hex(desktop.vol.offset), + station_name, + session_id, + desktop_name, + process_name, + process_pid, + ) def _generator(self): kernel_name = self.config["kernel"] diff --git a/volatility3/framework/plugins/windows/direct_system_calls.py b/volatility3/framework/plugins/windows/direct_system_calls.py index 60dbf728c..79d02fe67 100644 --- a/volatility3/framework/plugins/windows/direct_system_calls.py +++ b/volatility3/framework/plugins/windows/direct_system_calls.py @@ -1,28 +1,13 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging - +from volatility3.framework import interfaces, deprecation from collections import namedtuple -from typing import List, Tuple, Optional, Generator, Callable - -from volatility3.framework.objects import utility -from volatility3.framework import interfaces, renderers, symbols, exceptions -from volatility3.framework.configuration import requirements -from volatility3.plugins import yarascan -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist +from volatility3.plugins.windows.malware import direct_system_calls vollog = logging.getLogger(__name__) -try: - import capstone - - has_capstone = True -except ImportError: - has_capstone = False - # Full details on the techniques used in these plugins to detect EDR-evading malware # can be found in our 20 page whitepaper submitted to DEFCON along with the presentation # https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf @@ -38,19 +23,14 @@ syscall_finder_type = namedtuple( ], ) -syscall_finder_type.__doc__ = """ -This type is used to specify how malicious system call invocations should be found. -`get_syscall_target_address` is optionally used to extract the address containing the malicious 'syscall' instruction -`wants_syscall_inst` whether or not this method expects the 'syscall' instrunction directly within the malicious code block -`rule` the opcode string to search for the malicious syscall instructions -`invalid_ops` instructions that only appear in invalid code blocks. Stops processing of the code block when encountered. -`termination_ops` instructions that are expected to be present in the code block and that stop processing -""" - - -class DirectSystemCalls(interfaces.plugins.PluginInterface): - """Detects the Direct System Call technique used to bypass EDRs""" +class DirectSystemCalls( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=direct_system_calls.DirectSystemCalls, + removal_date="2026-06-07", +): + """Detects the Direct System Call technique used to bypass EDRs (deprecated).""" _required_framework_version = (2, 4, 0) @@ -81,392 +61,3 @@ class DirectSystemCalls(interfaces.plugins.PluginInterface): # the expected form is to end with a "ret" back to the calling code ["ret"], ) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # create a list of requirements for vadyarascan - vadyarascan_requirements = [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) - ), - requirements.VersionRequirement( - name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) - ), - ] - - # get base yarascan requirements for command line options - yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() - - # return the combined requirements - return yarascan_requirements + vadyarascan_requirements - - @staticmethod - def _is_syscall_block( - disasm_func: Callable, - syscall_finder: syscall_finder_type, - data: bytes, - address: int, - ) -> Optional[Tuple[str, "capstone._cs_insn"]]: - """ - Determines if the bytes starting at `data` represent a valid syscall instrunction invocation block - - To maliciously invoke the system call instruction, malware must do each of the following: - - 1) update RAX to the system call number - 2) update R10 to the first parameter - 3) hit the 'termination' instruction set in `syscall_finder_type` - - We also track whether the 'syscall' instruction was encountered while parsing - - This function is reusable for every technique we found and studied during the DEFCON research timeframe - - Args: - disasm_func: capstone disassembly function gathered from `get_disasm_function` - syscall_finder: the method and constraints on the malicious system call blocks that the calling plugin knows how to find - data: the bytes from memory to search for malicious syscall invocations - address: the address from where `data` came from in the particular process - Returns: - Optional[Tuple[str, capstone._cs_insn]]: For valid blocks, the disassembled bytes in string from and the last (termination) instruction - """ - found_movr10 = False - found_movreax = False - found_syscall = False - found_end = False - end_inst = None - - disasm_bytes = "" - - for inst in disasm_func(data, address): - disasm_bytes += f"{inst.address:#x}: {inst.mnemonic} {inst.op_str}; " - - # an instruction of all 0x00 opcodes - if inst.opcode.count(0) == len(inst.opcode): - break - - op = inst.mnemonic - - # invalid op, bail - if op in syscall_finder.invalid_ops: - break - - # found the end instruction wanted by the caller - elif op in syscall_finder.termination_ops: - found_end = True - end_inst = inst - break - - # track this no matter what to make code more re-usable - elif op == "syscall": - found_syscall = True - - # if we hit a 'syscall' but RAX or R10 haven't been touched - # then we are in an invalid path, so bail - if not syscall_finder.wants_syscall_inst or ( - not (found_movr10 and found_movreax) - ): - break - - else: - # attempt to see if any other instruction type wrote to registers - try: - _, regs_written = inst.regs_access() - except capstone.CsError: - continue - - if regs_written: - for r in regs_written: - # track writes to eax/rax or R10 - reg = inst.reg_name(r) - if reg in ["eax", "rax"]: - found_movreax = True - - elif reg == "r10": - found_movr10 = True - - # if any of these are missing, the block is invalid regardless of - # the technique we are trying to detect now or in the future - if not (found_movr10 and found_movreax and found_end): - return None - - # if the finder requires a 'syscall' instruction then bail now if we didn't find one - if syscall_finder.wants_syscall_inst and not found_syscall: - return None - - return disasm_bytes, end_inst - - @classmethod - def get_disasm_function(cls, architecture: str) -> Callable: - """ - Returns the disassembly handler for the given architecture - .detail is used to get full instruction information - - Args: - architecture: the name of the architecture for the process being disassembled - Returns: - The disasm function from capstone for the given architecture - """ - disasm_types = { - "intel": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32), - "intel64": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64), - } - - disasm_type = disasm_types[architecture] - disasm_type.detail = True - return disasm_type.disasm - - @classmethod - def _is_valid_syscall( - cls, - syscall_finder: syscall_finder_type, - proc_layer: interfaces.layers.DataLayerInterface, - architecture: str, - vads: List[Tuple[int, int, str]], - address: int, - ) -> Optional[Tuple[int, str]]: - """ - Args: - syscall_finder: - proc_layer: the memory layer of the process being scanned - architecture: the name of the architecture for the process being disassembled - vads: the ranges of this process under 10MB - address: the starting address to check for malicious syscall code blocks - - Returns: - Optional[Tuple[int, str]]: For valid code blocks, the starting address of the block and the disassembly string - """ - # the number bytes behind the yara rule hit to scan - behind = 32 - - address = address - behind - - try: - data = proc_layer.read(address, behind * 2) - except exceptions.InvalidAddressException: - return None - - disasm_func = cls.get_disasm_function(architecture) - - # since Intel does not have fixed-size instructions, we have to scan - # each byte offset and re-disassemble the remaining block - for offset in range(behind): - # if this looks like a system call back (r10, rax, ret/jmp) - syscall_info = cls._is_syscall_block( - disasm_func, syscall_finder, data[offset:], address + offset - ) - if syscall_info: - disasm_bytes, end_inst = syscall_info - - # if we can recover (and require) a target address for this malware technique - if syscall_finder.get_syscall_target_address: - target_address = syscall_finder.get_syscall_target_address( - proc_layer, end_inst - ) - - # could not determine the address -> invalid basic block - if not target_address: - continue - - # we only care about calls to system call DLLs - path = cls.get_range_path(vads, target_address) - if not isinstance(path, str) or not path.lower().endswith( - cls.valid_syscall_handlers - ): - continue - - # return the address and disassembly string if all checks pass - return address + offset, disasm_bytes - - return None - - @classmethod - def get_vad_maps( - cls, - task: interfaces.objects.ObjectInterface, - ) -> List[Tuple[int, int, str]]: - """Creates a map of start/end addresses within a virtual address - descriptor tree. - - Args: - task: The EPROCESS object of which to traverse the vad tree - - Returns: - An iterable of tuples containing start and end addresses for each descriptor - """ - vads: List[Tuple[int, int, str]] = [] - - # scan regions under 10MB - scan_max = 10 * 1000 * 1000 - - vad_root = task.get_vad_root() - - for vad in vad_root.traverse(): - if vad.get_size() < scan_max: - vads.append((vad.get_start(), vad.get_size(), vad.get_file_name())) - - return vads - - @classmethod - def get_range_path( - cls, ranges: List[Tuple[int, int, str]], address: int - ) -> Optional[str]: - """ - Returns the path for the range holding `address`, if found - - Args: - ranges: VADs collected from `get_vad_maps` - address: the address to find - Returns: - The path holding the address, if any - """ - for start, size, path in ranges: - if start <= address < start + size: - return path - - return None - - @classmethod - def get_tasks_to_scan( - cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - ) -> Generator[ - Tuple[interfaces.objects.ObjectInterface, str, str, str], None, None - ]: - """ - Gathers active processes with the extra information needed - to detect malicious syscall instructions - - Returns: - Generator of the process object, name, memory layer, and architecture - """ - - # gather active processes - filter_func = pslist.PsList.create_active_process_filter() - - kernel = context.modules[kernel_module_name] - - is_32bit_arch = not symbols.symbol_table_is_64bit( - context=context, symbol_table_name=kernel.symbol_table_name - ) - - for proc in pslist.PsList.list_processes( - context=context, - kernel_module_name=kernel_module_name, - filter_func=filter_func, - ): - proc_name = utility.array_to_string(proc.ImageFileName) - - # skip Defender - if proc_name in ["MsMpEng.exe"]: - continue - - try: - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException: - continue - - if is_32bit_arch or proc.get_is_wow64(): - architecture = "intel" - else: - architecture = "intel64" - - yield proc, proc_name, proc_layer_name, architecture - - @classmethod - def _get_rule_hits( - cls, - context: interfaces.objects.ObjectInterface, - proc_layer: interfaces.layers.DataLayerInterface, - vads: List[Tuple[int, int, str]], - pattern: str, - ) -> Generator[Tuple[int, Optional[str]], None, None]: - """ - Runs the given opcode rule through Yara and returns the address and file path of hits - - Args: - context: - proc_layer: the layer to scan - vads: the ranges inside of the process being scanned - pattern: the opcodes rule from the plugin to detect a particular EDR-bypass technique - - Returns: - Generator of the address and file path of hits - """ - sections = [(vad[0], vad[1]) for vad in vads] - - rule = yarascan.YaraScanner.get_rule(pattern) - - for hit in proc_layer.scan( - context=context, - scanner=yarascan.YaraScanner(rules=rule), - sections=sections, - ): - address = hit[0] - - path = cls.get_range_path(vads, address) - - # ignore hits in the system call DLLs - if isinstance(path, str) and path.lower().endswith( - cls.valid_syscall_handlers - ): - continue - - yield address, path - - def _generator( - self, - ) -> Generator[Tuple[int, Tuple[str, int, Optional[str], int, str]], None, None]: - if not has_capstone: - vollog.warning( - "capstone is not installed. This plugin requires capstone to operate." - ) - return - - for proc, proc_name, proc_layer_name, architecture in self.get_tasks_to_scan( - self.context, self.config["kernel"] - ): - proc_layer = self.context.layers[proc_layer_name] - - vads = self.get_vad_maps(proc) - if not vads: - continue - - # for each valid process, look for malicious syscall invocations - for address, vad_path in self._get_rule_hits( - self.context, proc_layer, vads, self.syscall_finder.rule_str - ): - syscall_info = self._is_valid_syscall( - self.syscall_finder, proc_layer, architecture, vads, address - ) - if not syscall_info: - continue - - address, disasm_bytes = syscall_info - - yield 0, ( - proc_name, - proc.UniqueProcessId, - vad_path, - format_hints.Hex(address), - disasm_bytes, - ) - - def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid( - [ - ("Process", str), - ("PID", int), - ("Range", str), - ("Address", format_hints.Hex), - ("Disasm", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index b851cf7fd..cb9d3b8bf 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -22,7 +22,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Lists the loaded DLLs in a particular windows memory image.""" _required_framework_version = (2, 0, 0) - _version = (3, 0, 0) + _version = (3, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -173,6 +173,10 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except exceptions.InvalidAddressException: size_of_image = renderers.NotAvailableValue() + LoadCount = entry.get_load_count() + if LoadCount is None: + LoadCount = renderers.NotAvailableValue() + yield ( 0, ( @@ -186,6 +190,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): size_of_image, BaseDllName, FullDllName, + LoadCount, DllLoadTime, file_output, ), @@ -232,6 +237,7 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ("Size", format_hints.Hex), ("Name", str), ("Path", str), + ("LoadCount", int), ("LoadTime", datetime.datetime), ("File output", str), ], diff --git a/volatility3/framework/plugins/windows/drivermodule.py b/volatility3/framework/plugins/windows/drivermodule.py index c31fe2500..bf8f333f6 100644 --- a/volatility3/framework/plugins/windows/drivermodule.py +++ b/volatility3/framework/plugins/windows/drivermodule.py @@ -1,101 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Iterator, List, Tuple -from volatility3.framework import renderers, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import ssdt, driverscan, modules +import logging +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import drivermodule -# built in Windows-components that trigger false positives -KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] +vollog = logging.getLogger(__name__) -class DriverModule(interfaces.plugins.PluginInterface): - """Determines if any loaded drivers were hidden by a rootkit""" +class DriverModule( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=drivermodule.DriverModule, + removal_date="2026-06-07", +): + """Determines if any loaded drivers were hidden by a rootkit (deprecated).""" _required_framework_version = (2, 0, 0) _version = (1, 0, 0) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="modules", component=modules.Modules, version=(3, 0, 0) - ), - ] - - def _generator(self) -> Iterator[Tuple]: - """ - Attempt to match each driver's start code address to a known kernel module - A common rootkit technique is to register drivers from modules that are hidden, - which allows us to detect the disconnect between a malicious driver and its hidden module. - """ - collection = ssdt.SSDT.build_module_collection( - context=self.context, - kernel_module_name=self.config["kernel"], - ) - - kernel_space_start = modules.Modules.get_kernel_space_start( - self.context, self.config["kernel"] - ) - - for driver in driverscan.DriverScan.scan_drivers( - self.context, - self.config["kernel"], - ): - # We want starts of 0 as rootkits often set this value - # greater than 0 but less than the kernel space start is smear/terminated though - if 0 < driver.DriverStart < kernel_space_start: - continue - - # we do not care about actual symbol names, we just want to know if the driver points to a known module - module_symbols = list( - collection.get_module_symbols_by_absolute_location(driver.DriverStart) - ) - if not module_symbols: - ( - driver_name, - service_key, - name, - ) = driverscan.DriverScan.get_names_for_driver(driver) - - # drivers without any names will not produce useful output - if not driver_name and not service_key and not name: - continue - - known_exception = driver_name in KNOWN_DRIVERS - - yield ( - 0, - ( - format_hints.Hex(driver.vol.offset), - known_exception, - driver_name or renderers.NotAvailableValue(), - service_key or renderers.NotAvailableValue(), - name or renderers.NotAvailableValue(), - ), - ) - - def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Known Exception", bool), - ("Driver Name", str), - ("Service Key", str), - ("Alternative Name", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/hashdump.py b/volatility3/framework/plugins/windows/hashdump.py index e496e77a9..c4b99d86f 100644 --- a/volatility3/framework/plugins/windows/hashdump.py +++ b/volatility3/framework/plugins/windows/hashdump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import hashdump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Hashdump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=hashdump.Hashdump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps user hashes from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/hollowprocesses.py b/volatility3/framework/plugins/windows/hollowprocesses.py index af559bfbc..9949a223c 100644 --- a/volatility3/framework/plugins/windows/hollowprocesses.py +++ b/volatility3/framework/plugins/windows/hollowprocesses.py @@ -1,222 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import NamedTuple, Dict, Generator - -from volatility3.framework import interfaces, exceptions, constants -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import hollowprocesses vollog = logging.getLogger(__name__) -class VadData(NamedTuple): - protection: str - path: str - - -class DLLData(NamedTuple): - path: str - - -### Useful references on process hollowing -# https://cysinfo.com/detecting-deceptive-hollowing-techniques/ -# https://github.com/m0n0ph1/Process-Hollowing - - -class HollowProcesses(interfaces.plugins.PluginInterface): - """Lists hollowed processes""" +class HollowProcesses( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=hollowprocesses.HollowProcesses, + removal_date="2026-06-07", +): + """Lists hollowed processes (deprecated)""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - element_type=int, - description="Process IDs to include (all other processes are excluded)", - optional=True, - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - def _get_vads_data( - self, proc: interfaces.objects.ObjectInterface - ) -> Dict[int, VadData]: - """ - Returns a dictionary of: - base address -> (protection string, file name) - For each mapped VAD in the process. This is used - for quick lookups of data and matching the DLL - at the same base address as the VAD - """ - vads = {} - - kernel = self.context.modules[self.config["kernel"]] - - for vad in proc.get_vad_root().traverse(): - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, kernel.layer_name, kernel.symbol_table_name - ), - vadinfo.winnt_protections, - ) - - fn = vad.get_file_name() - if not fn or not isinstance(fn, str): - fn = "" - - vads[vad.get_start()] = VadData(protection_string, fn) - - return vads - - def _get_dlls_map( - self, proc: interfaces.objects.ObjectInterface - ) -> Dict[int, DLLData]: - """ - Returns a dictionary of: - base address -> path - for each DLL loaded in the process - - This is used to cross compare with - the corresponding VAD and to have a - backup path source in case of smear - in the VAD - """ - dlls = {} - - for entry in proc.load_order_modules(): - try: - base = entry.DllBase - except exceptions.InvalidAddressException: - continue - - try: - FullDllName = entry.FullDllName.get_string() - except exceptions.InvalidAddressException: - FullDllName = renderers.UnreadableValue() - - dlls[base] = DLLData(FullDllName) - - return dlls - - def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int: - """ - Uses the PEB to get the image base of the process - """ - kernel = self.context.modules[self.config["kernel"]] - - try: - proc_layer_name = proc.add_process_layer() - peb = self.context.object( - kernel.symbol_table_name + constants.BANG + "_PEB", - layer_name=proc_layer_name, - offset=proc.Peb, - ) - return peb.ImageBaseAddress - except exceptions.InvalidAddressException: - return None - - def _check_load_address(self, proc, _, __) -> Generator[str, None, None]: - """ - Detects when the image base in the PEB, which is writable by process malware, - does not match the section base address - whose value lives in kernel memory. - Many malware samples will manipulate their image base to fool AVs/EDRs and - as a necessary part of certain hollowing techniques - """ - image_base = self._get_image_base(proc) - if image_base is not None and image_base != proc.SectionBaseAddress: - yield f"The ImageBaseAddress reported from the PEB ({image_base:#x}) does not match the process SectionBaseAddress ({proc.SectionBaseAddress:#x})" - - def _check_exe_protection( - self, proc, vads: Dict[int, VadData], __ - ) -> Generator[str, None, None]: - """ - Legitimately mapped application executables and DLLs - will have a VAD present and its initial protection will be - PAGE_EXECUTE_WRITECOPY. - Many process hollowing and code injection techniques will - unmap the real executable and/or map in executables with - incorrect permissions. - This check verifies the VAD for the application exe. - `_check_dlls_protection` checks for DLLs mapped in the process. - """ - base = proc.SectionBaseAddress - - if base not in vads: - yield f"There is no VAD starting at the base address of the process executable ({base:#x})" - elif vads[base].protection != "PAGE_EXECUTE_WRITECOPY": - yield f"Unexpected protection ({vads[base].protection}) for VAD hosting the process executable ({base:#x}) with path {vads[base].path}" - - def _check_dlls_protection( - self, _, vads: Dict[int, VadData], dlls: Dict[int, DLLData] - ) -> Generator[str, None, None]: - for dll_base in dlls: - # could be malicious but triggers too many FPs from smear - if dll_base not in vads: - continue - - # PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files - if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY": - yield f"Unexpected protection ({vads[dll_base].protection}) for DLL in the PEB's load order list ({dll_base:#x}) with path {dlls[dll_base].path}" - - def _generator(self, procs): - checks = [ - self._check_load_address, - self._check_exe_protection, - self._check_dlls_protection, - ] - - for proc in procs: - # smear and/or terminated process - dlls = self._get_dlls_map(proc) - if len(dlls) < 3: - continue - - vads = self._get_vads_data(proc) - if len(vads) < 5: - continue - - proc_name = utility.array_to_string(proc.ImageFileName) - pid = proc.UniqueProcessId - - for check in checks: - for note in check(proc, vads, dlls): - yield 0, ( - pid, - proc_name, - note, - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Notes", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/indirect_system_calls.py b/volatility3/framework/plugins/windows/indirect_system_calls.py index 26216d2c3..65f5f8734 100644 --- a/volatility3/framework/plugins/windows/indirect_system_calls.py +++ b/volatility3/framework/plugins/windows/indirect_system_calls.py @@ -1,119 +1,21 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -import struct import logging -from typing import List, Optional - -from volatility3.framework import interfaces, exceptions -from volatility3.framework.configuration import requirements -from volatility3.plugins import yarascan -from volatility3.plugins.windows import direct_system_calls +from volatility3.framework import deprecation +from volatility3.plugins.windows.malware import indirect_system_calls +from volatility3.plugins.windows.malware import direct_system_calls vollog = logging.getLogger(__name__) -class IndirectSystemCalls(direct_system_calls.DirectSystemCalls): +class IndirectSystemCalls( + direct_system_calls.DirectSystemCalls, + deprecation.PluginRenameClass, + replacement_class=indirect_system_calls.IndirectSystemCalls, + removal_date="2026-06-07", +): + """Detects the Indirect System Call technique used to bypass EDRs (deprecated).""" + _required_framework_version = (2, 4, 0) _version = (1, 0, 0) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.syscall_finder = direct_system_calls.syscall_finder_type( - # gets the target address of a indirect jmp - self._indirect_syscall_block_target, - # we are looking for indirect system calls, so we don't want 'syscall' instructions in our code block - False, - # jmp [address]; ret - "/\\xff\\x25[^\\xc3]{,24}\\xc3/", - # any of these mean we aren't in a malicious indirect call - ["call", "leave", "int3", "ret"], - # stop at jmp, this should reference the system call instruction - ["jmp"], - ) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # create a list of requirements for vadyarascan - vadyarascan_requirements = [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) - ), - requirements.VersionRequirement( - name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="direct_system_calls", - component=direct_system_calls.DirectSystemCalls, - version=(2, 0, 0), - ), - ] - - # get base yarascan requirements for command line options - yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() - - # return the combined requirements - return yarascan_requirements + vadyarascan_requirements - - @staticmethod - def _indirect_syscall_block_target( - proc_layer: interfaces.layers.DataLayerInterface, inst - ) -> Optional[int]: - """ - This function determines the address of a jmp in the following form: - - jmp [address] - - To determine this, we must: - 1) Pull the 4 byte relative offset of 'address' inside the instruction - 2) Compute the full address of this relative offset - 3) Read from the address as it is being dereferenced - 4) Ensure the target address points to a 'syscall' instruction - - Args: - proc_layer: the layer of the potential syscall block - inst: the terminating instruction of the syscall block check - Returns: - The target address of the jump if it can be computed - """ - - try: - jmp_address_str = proc_layer.read(inst.address, 6) - except exceptions.InvalidAddressException: - return None - - # Should be an jmp... - if jmp_address_str[0:2] != b"\xff\x25": - return None - - # get the address of the 'jmp [address]' instruction - relative_offset = struct.unpack(" Iterator[Tuple]: - for kpcr, current_prcb in self.list_kpcrs(self.context, self.config["kernel"]): yield ( 0, diff --git a/volatility3/framework/plugins/windows/ldrmodules.py b/volatility3/framework/plugins/windows/ldrmodules.py index 32432c44e..efb62f8f6 100644 --- a/volatility3/framework/plugins/windows/ldrmodules.py +++ b/volatility3/framework/plugins/windows/ldrmodules.py @@ -1,128 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging - -from volatility3.framework import constants, exceptions, interfaces, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import ldrmodules vollog = logging.getLogger(__name__) -class LdrModules(interfaces.plugins.PluginInterface): +class LdrModules( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=ldrmodules.LdrModules, + removal_date="2026-06-07", +): """Lists the loaded modules in a particular windows memory image.""" _required_framework_version = (2, 0, 0) _version = (1, 0, 1) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - requirements.ListRequirement( - name="pid", - element_type=int, - description="Process IDs to include (all other processes are excluded)", - optional=True, - ), - ] - - def _generator(self, procs): - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - for proc in procs: - proc_layer_name = proc.add_process_layer() - - # Build dictionaries from different module lists, where the DllBase address is the key and value is the module object - load_order_mod = dict( - (mod.DllBase, mod) for mod in proc.load_order_modules() - ) - init_order_mod = dict( - (mod.DllBase, mod) for mod in proc.init_order_modules() - ) - mem_order_mod = dict((mod.DllBase, mod) for mod in proc.mem_order_modules()) - - # Build dictionary of mapped files, where the VAD start address is the key and value is the file name of the mapped file - mapped_files = {} - for vad in vadinfo.VadInfo.list_vads(proc): - dos_header = self.context.object( - pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset=vad.get_start(), - layer_name=proc_layer_name, - ) - try: - # Filter out VADs that do not start with a MZ header - if dos_header.e_magic != 0x5A4D: - continue - except exceptions.InvalidAddressException: - vollog.log( - constants.LOGLEVEL_VVVV, - f"Skipping vad at {hex(dos_header.vol.offset)} due to InvalidAddressException", - ) - continue - - mapped_files[vad.get_start()] = vad.get_file_name() - - for base in mapped_files.keys(): - # Does the base address exist in the PEB DLL lists? - load_mod = load_order_mod.get(base, None) - init_mod = init_order_mod.get(base, None) - mem_mod = mem_order_mod.get(base, None) - - yield ( - 0, - [ - int(proc.UniqueProcessId), - str( - proc.ImageFileName.cast( - "string", - max_length=proc.ImageFileName.vol.count, - errors="replace", - ) - ), - format_hints.Hex(base), - load_mod is not None, - init_mod is not None, - mem_mod is not None, - mapped_files[base], - ], - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("Pid", int), - ("Process", str), - ("Base", format_hints.Hex), - ("InLoad", bool), - ("InInit", bool), - ("InMem", bool), - ("MappedPath", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ) - ), - ) diff --git a/volatility3/framework/plugins/windows/lsadump.py b/volatility3/framework/plugins/windows/lsadump.py index 0b36ddef0..ab81f18df 100644 --- a/volatility3/framework/plugins/windows/lsadump.py +++ b/volatility3/framework/plugins/windows/lsadump.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import lsadump vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class Lsadump( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=lsadump.Lsadump, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Dumps lsa secrets from memory (deprecated)""" diff --git a/volatility3/framework/plugins/windows/malfind.py b/volatility3/framework/plugins/windows/malfind.py index d9861d9c8..a98ab5ec0 100644 --- a/volatility3/framework/plugins/windows/malfind.py +++ b/volatility3/framework/plugins/windows/malfind.py @@ -1,293 +1,20 @@ -# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from typing import Iterable, Generator, Tuple - -from volatility3.framework import interfaces, symbols, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import malfind vollog = logging.getLogger(__name__) -class Malfind(interfaces.plugins.PluginInterface): - """Lists process memory ranges that potentially contain injected code.""" +class Malfind( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=malfind.Malfind, + removal_date="2026-06-07", +): + """Lists process memory ranges that potentially contain injected code (deprecated).""" _required_framework_version = (2, 22, 0) _version = (1, 1, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - element_type=int, - description="Process IDs to include (all other processes are excluded)", - optional=True, - ), - requirements.BooleanRequirement( - name="dump", - description="Extract injected VADs", - default=False, - optional=True, - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - @classmethod - def is_vad_empty(cls, proc_layer, vad): - """Check if a VAD region is either entirely unavailable due to paging, - entirely consisting of zeros, or a combination of the two. This helps - ignore false positives whose VAD flags match task._injection_filter - requirements but there's no data and thus not worth reporting it. - - Args: - proc_layer: the process layer - vad: the MMVAD structure to test - - Returns: - A boolean indicating whether a vad is empty or not - """ - - CHUNK_SIZE = 0x1000 - all_zero_page = b"\x00" * CHUNK_SIZE - - offset = 0 - vad_length = vad.get_size() - - while offset < vad_length: - next_addr = vad.get_start() + offset - if ( - proc_layer.is_valid(next_addr, CHUNK_SIZE) - and proc_layer.read(next_addr, CHUNK_SIZE) != all_zero_page - ): - return False - offset += CHUNK_SIZE - - return True - - @classmethod - def list_injections( - cls, - context: interfaces.context.ContextInterface, - kernel_layer_name: str, - symbol_table: str, - proc: interfaces.objects.ObjectInterface, - ) -> Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: - for vad, data_object in cls.list_injection_sites( - context, kernel_layer_name, symbol_table, proc - ): - yield vad, data_object.context.layers[data_object.layer_name].read( - data_object.offset, data_object.length - ) - - @classmethod - def list_injection_sites( - cls, - context: interfaces.context.ContextInterface, - kernel_layer_name: str, - symbol_table: str, - proc: interfaces.objects.ObjectInterface, - ) -> Generator[ - Tuple[interfaces.objects.ObjectInterface, renderers.LayerData], - None, - None, - ]: - """Generate memory regions for a process that may contain injected - code. - - Args: - context: The context to retrieve required elements (layers, symbol tables) from - kernel_layer_name: The name of the kernel layer from which to read the VAD protections - symbol_table: The name of the table containing the kernel symbols - proc: an _EPROCESS instance - - Returns: - An iterable of VAD instances and the first 64 bytes of data containing in that region - """ - proc_id = "Unknown" - try: - proc_id = proc.UniqueProcessId - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException as excp: - vollog.debug( - f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" - ) - return None - - proc_layer = context.layers[proc_layer_name] - - for vad in proc.get_vad_root().traverse(): - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - context, kernel_layer_name, symbol_table - ), - vadinfo.winnt_protections, - ) - write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string - dirty_page = None - if not write_exec: - """ - # Inspect "PAGE_EXECUTE_READ" VAD pages to detect - # non writable memory regions having been injected - # using elevated WriteProcessMemory(). - """ - if "EXECUTE" in protection_string: - for page in range( - vad.get_start(), vad.get_end(), proc_layer.page_size - ): - try: - # If we have a dirty page in a non writable "EXECUTE" region, it is suspicious. - if proc_layer.is_dirty(page): - dirty_page = page - break - except exceptions.InvalidAddressException: - # Abort as it is likely that other addresses in the same range will also fail. - break - if dirty_page is None: - continue - else: - continue - - if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( - vad.get_private_memory() == 0 - and protection_string != "PAGE_EXECUTE_WRITECOPY" - ): - if cls.is_vad_empty(proc_layer, vad): - continue - - if dirty_page is not None: - # Useful information to investigate the page content with volshell afterwards. - vollog.warning( - f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}", - ) - start = vad.get_start() - length = 64 - data = renderers.LayerData( - context=context, - layer_name=proc_layer_name, - offset=start, - length=length, - no_surrounding=True, - ) - yield (vad, data) - - def _generator(self, procs): - # determine if we're on a 32 or 64 bit kernel - kernel = self.context.modules[self.config["kernel"]] - - # set refined criteria to know when to add to "Notes" column - refined_criteria = { - b"MZ": "MZ header", - b"\x55\x8b": "PE header", - b"\x55\x48": "Function prologue", - b"\x55\x89": "Function prologue", - } - - is_32bit_arch = not symbols.symbol_table_is_64bit( - context=self.context, symbol_table_name=kernel.symbol_table_name - ) - - for proc in procs: - # by default, "Notes" column will be set to N/A - process_name = utility.array_to_string(proc.ImageFileName) - - for vad, data_object in self.list_injection_sites( - self.context, kernel.layer_name, kernel.symbol_table_name, proc - ): - notes = renderers.NotApplicableValue() - # Check for unique headers and update "Notes" column if criteria is met - data = data_object.context.layers[data_object.layer_name].read( - data_object.offset, data_object.length, True - ) - if data[0:2] in refined_criteria: - notes = refined_criteria[data[0:2]] - - # if we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 - if is_32bit_arch or proc.get_is_wow64(): - architecture = "intel" - else: - architecture = "intel64" - - disasm = renderers.Disassembly(data, vad.get_start(), architecture) - - file_output = "Disabled" - if self.config["dump"]: - file_output = "Error outputting to file" - try: - file_handle = vadinfo.VadInfo.vad_dump( - self.context, proc, vad, self.open - ) - file_handle.close() - file_output = file_handle.preferred_filename - except (exceptions.InvalidAddressException, OverflowError) as excp: - vollog.debug( - f"Unable to dump PE with pid {proc.UniqueProcessId}.{vad.get_start():#x}: {excp}" - ) - - yield ( - 0, - ( - proc.UniqueProcessId, - process_name, - format_hints.Hex(vad.get_start()), - format_hints.Hex(vad.get_end()), - vad.get_tag(), - vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, - kernel.layer_name, - kernel.symbol_table_name, - ), - vadinfo.winnt_protections, - ), - vad.get_commit_charge(), - vad.get_private_memory(), - file_output, - notes, - data_object, - disasm, - ), - ) - - def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Start VPN", format_hints.Hex), - ("End VPN", format_hints.Hex), - ("Tag", str), - ("Protection", str), - ("CommitCharge", int), - ("PrivateMemory", int), - ("File output", str), - ("Notes", str), - ("Hexdump", renderers.LayerData), - ("Disasm", renderers.Disassembly), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ) - ), - ) diff --git a/volatility3/framework/plugins/windows/malware/__init__.py b/volatility3/framework/plugins/windows/malware/__init__.py new file mode 100644 index 000000000..2e2fec739 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/__init__.py @@ -0,0 +1,8 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""All core windows malware plugins. + +These modules should only be imported from volatility3.plugins NOT +volatility3.framework.plugins +""" diff --git a/volatility3/framework/plugins/windows/malware/direct_system_calls.py b/volatility3/framework/plugins/windows/malware/direct_system_calls.py new file mode 100644 index 000000000..f6b9e53bb --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/direct_system_calls.py @@ -0,0 +1,475 @@ +# 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 + +from collections import namedtuple +from typing import List, Tuple, Optional, Generator, Callable + +from volatility3.framework.objects import utility +from volatility3.framework import interfaces, renderers, symbols, exceptions +from volatility3.framework.configuration import requirements +from volatility3.plugins import yarascan +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +syscall_finder_type = namedtuple( + "syscall_finder_type", + [ + "get_syscall_target_address", + "wants_syscall_inst", + "rule_str", + "invalid_ops", + "termination_ops", + ], +) + +syscall_finder_type.__doc__ = """ +This type is used to specify how malicious system call invocations should be found. + +`get_syscall_target_address` is optionally used to extract the address containing the malicious 'syscall' instruction +`wants_syscall_inst` whether or not this method expects the 'syscall' instruction directly within the malicious code block +`rule` the opcode string to search for the malicious syscall instructions +`invalid_ops` instructions that only appear in invalid code blocks. Stops processing of the code block when encountered. +`termination_ops` instructions that are expected to be present in the code block and that stop processing +""" + + +class DirectSystemCalls(interfaces.plugins.PluginInterface): + """Detects the Direct System Call technique used to bypass EDRs""" + + _required_framework_version = (2, 4, 0) + + # 2.0.0 - changes signature of `get_tasks_to_scan` + _version = (2, 0, 0) + + # DLLs that are expected to host system call invocations + valid_syscall_handlers = ("ntdll.dll", "win32u.dll") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.syscall_finder = syscall_finder_type( + # for direct system calls, we find the `syscall` instruction directly, so we already know the address + None, + # yes, we want the syscall instruction present as it is what this technique looks for + True, + # regex to find "\x0f\x05" (syscall) followed later by "\xc3" (ret) + # we allow spacing in between to break naive anti-analysis forms (e.g., TarTarus Gate) + # Standard techniques, such as HellsGate, look like: + # mov r10, rcx + # mov eax, + # syscall + # ret + "/\\x0f\\x05[^\\xc3]{,24}\\xc3/", + # any of these will not be in a workable, malicious direct system call block + ["jmp", "call", "leave", "int3"], + # the expected form is to end with a "ret" back to the calling code + ["ret"], + ) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # create a list of requirements for vadyarascan + vadyarascan_requirements = [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), + ] + + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vadyarascan_requirements + + @staticmethod + def _is_syscall_block( + disasm_func: Callable, + syscall_finder: syscall_finder_type, + data: bytes, + address: int, + ) -> Optional[Tuple[str, "capstone._cs_insn"]]: + """ + Determines if the bytes starting at `data` represent a valid syscall instruction invocation block + + To maliciously invoke the system call instruction, malware must do each of the following: + + 1) update RAX to the system call number + 2) update R10 to the first parameter + 3) hit the 'termination' instruction set in `syscall_finder_type` + + We also track whether the 'syscall' instruction was encountered while parsing + + This function is reusable for every technique we found and studied during the DEFCON research timeframe + + Args: + disasm_func: capstone disassembly function gathered from `get_disasm_function` + syscall_finder: the method and constraints on the malicious system call blocks that the calling plugin knows how to find + data: the bytes from memory to search for malicious syscall invocations + address: the address from where `data` came from in the particular process + Returns: + Optional[Tuple[str, capstone._cs_insn]]: For valid blocks, the disassembled bytes in string from and the last (termination) instruction + """ + found_movr10 = False + found_movreax = False + found_syscall = False + found_end = False + end_inst = None + + disasm_bytes = "" + + for inst in disasm_func(data, address): + disasm_bytes += f"{inst.address:#x}: {inst.mnemonic} {inst.op_str}; " + + # an instruction of all 0x00 opcodes + if inst.opcode.count(0) == len(inst.opcode): + break + + op = inst.mnemonic + + # invalid op, bail + if op in syscall_finder.invalid_ops: + break + + # found the end instruction wanted by the caller + elif op in syscall_finder.termination_ops: + found_end = True + end_inst = inst + break + + # track this no matter what to make code more re-usable + elif op == "syscall": + found_syscall = True + + # if we hit a 'syscall' but RAX or R10 haven't been touched + # then we are in an invalid path, so bail + if not syscall_finder.wants_syscall_inst or ( + not (found_movr10 and found_movreax) + ): + break + + else: + # attempt to see if any other instruction type wrote to registers + try: + _, regs_written = inst.regs_access() + except capstone.CsError: + continue + + if regs_written: + for r in regs_written: + # track writes to eax/rax or R10 + reg = inst.reg_name(r) + if reg in ["eax", "rax"]: + found_movreax = True + + elif reg == "r10": + found_movr10 = True + + # if any of these are missing, the block is invalid regardless of + # the technique we are trying to detect now or in the future + if not (found_movr10 and found_movreax and found_end): + return None + + # if the finder requires a 'syscall' instruction then bail now if we didn't find one + if syscall_finder.wants_syscall_inst and not found_syscall: + return None + + return disasm_bytes, end_inst + + @classmethod + def get_disasm_function(cls, architecture: str) -> Callable: + """ + Returns the disassembly handler for the given architecture + .detail is used to get full instruction information + + Args: + architecture: the name of the architecture for the process being disassembled + Returns: + The disasm function from capstone for the given architecture + """ + disasm_types = { + "intel": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32), + "intel64": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64), + } + + disasm_type = disasm_types[architecture] + disasm_type.detail = True + return disasm_type.disasm + + @classmethod + def _is_valid_syscall( + cls, + syscall_finder: syscall_finder_type, + proc_layer: interfaces.layers.DataLayerInterface, + architecture: str, + vads: List[Tuple[int, int, str]], + address: int, + ) -> Optional[Tuple[int, str]]: + """ + Args: + syscall_finder: + proc_layer: the memory layer of the process being scanned + architecture: the name of the architecture for the process being disassembled + vads: the ranges of this process under 10MB + address: the starting address to check for malicious syscall code blocks + + Returns: + Optional[Tuple[int, str]]: For valid code blocks, the starting address of the block and the disassembly string + """ + # the number bytes behind the yara rule hit to scan + behind = 32 + + address = address - behind + + try: + data = proc_layer.read(address, behind * 2) + except exceptions.InvalidAddressException: + return None + + disasm_func = cls.get_disasm_function(architecture) + + # since Intel does not have fixed-size instructions, we have to scan + # each byte offset and re-disassemble the remaining block + for offset in range(behind): + # if this looks like a system call back (r10, rax, ret/jmp) + syscall_info = cls._is_syscall_block( + disasm_func, syscall_finder, data[offset:], address + offset + ) + if syscall_info: + disasm_bytes, end_inst = syscall_info + + # if we can recover (and require) a target address for this malware technique + if syscall_finder.get_syscall_target_address: + target_address = syscall_finder.get_syscall_target_address( + proc_layer, end_inst + ) + + # could not determine the address -> invalid basic block + if not target_address: + continue + + # we only care about calls to system call DLLs + path = cls.get_range_path(vads, target_address) + if not isinstance(path, str) or not path.lower().endswith( + cls.valid_syscall_handlers + ): + continue + + # return the address and disassembly string if all checks pass + return address + offset, disasm_bytes + + return None + + @classmethod + def get_vad_maps( + cls, + task: interfaces.objects.ObjectInterface, + ) -> List[Tuple[int, int, str]]: + """Creates a map of start/end addresses within a virtual address + descriptor tree. + + Args: + task: The EPROCESS object of which to traverse the vad tree + + Returns: + An iterable of tuples containing start and end addresses for each descriptor + """ + vads: List[Tuple[int, int, str]] = [] + + # scan regions under 10MB + scan_max = 10 * 1000 * 1000 + + vad_root = task.get_vad_root() + + for vad in vad_root.traverse(): + if vad.get_size() < scan_max: + vads.append((vad.get_start(), vad.get_size(), vad.get_file_name())) + + return vads + + @classmethod + def get_range_path( + cls, ranges: List[Tuple[int, int, str]], address: int + ) -> Optional[str]: + """ + Returns the path for the range holding `address`, if found + + Args: + ranges: VADs collected from `get_vad_maps` + address: the address to find + Returns: + The path holding the address, if any + """ + for start, size, path in ranges: + if start <= address < start + size: + return path + + return None + + @classmethod + def get_tasks_to_scan( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + ) -> Generator[ + Tuple[interfaces.objects.ObjectInterface, str, str, str], None, None + ]: + """ + Gathers active processes with the extra information needed + to detect malicious syscall instructions + + Returns: + Generator of the process object, name, memory layer, and architecture + """ + + # gather active processes + filter_func = pslist.PsList.create_active_process_filter() + + kernel = context.modules[kernel_module_name] + + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) + + for proc in pslist.PsList.list_processes( + context=context, + kernel_module_name=kernel_module_name, + filter_func=filter_func, + ): + proc_name = utility.array_to_string(proc.ImageFileName) + + # skip Defender + if proc_name in ["MsMpEng.exe"]: + continue + + try: + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + if is_32bit_arch or proc.get_is_wow64(): + architecture = "intel" + else: + architecture = "intel64" + + yield proc, proc_name, proc_layer_name, architecture + + @classmethod + def _get_rule_hits( + cls, + context: interfaces.objects.ObjectInterface, + proc_layer: interfaces.layers.DataLayerInterface, + vads: List[Tuple[int, int, str]], + pattern: str, + ) -> Generator[Tuple[int, Optional[str]], None, None]: + """ + Runs the given opcode rule through Yara and returns the address and file path of hits + + Args: + context: + proc_layer: the layer to scan + vads: the ranges inside of the process being scanned + pattern: the opcodes rule from the plugin to detect a particular EDR-bypass technique + + Returns: + Generator of the address and file path of hits + """ + sections = [(vad[0], vad[1]) for vad in vads] + + rule = yarascan.YaraScanner.get_rule(pattern) + + for hit in proc_layer.scan( + context=context, + scanner=yarascan.YaraScanner(rules=rule), + sections=sections, + ): + address = hit[0] + + path = cls.get_range_path(vads, address) + + # ignore hits in the system call DLLs + if isinstance(path, str) and path.lower().endswith( + cls.valid_syscall_handlers + ): + continue + + yield address, path + + def _generator( + self, + ) -> Generator[Tuple[int, Tuple[str, int, Optional[str], int, str]], None, None]: + if not has_capstone: + vollog.warning( + "capstone is not installed. This plugin requires capstone to operate." + ) + return + + for proc, proc_name, proc_layer_name, architecture in self.get_tasks_to_scan( + self.context, self.config["kernel"] + ): + proc_layer = self.context.layers[proc_layer_name] + + vads = self.get_vad_maps(proc) + if not vads: + continue + + # for each valid process, look for malicious syscall invocations + for address, vad_path in self._get_rule_hits( + self.context, proc_layer, vads, self.syscall_finder.rule_str + ): + syscall_info = self._is_valid_syscall( + self.syscall_finder, proc_layer, architecture, vads, address + ) + if not syscall_info: + continue + + address, disasm_bytes = syscall_info + + yield ( + 0, + ( + proc_name, + proc.UniqueProcessId, + vad_path, + format_hints.Hex(address), + disasm_bytes, + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("Range", str), + ("Address", format_hints.Hex), + ("Disasm", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/drivermodule.py b/volatility3/framework/plugins/windows/malware/drivermodule.py new file mode 100644 index 000000000..c31fe2500 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/drivermodule.py @@ -0,0 +1,101 @@ +# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +from typing import Iterator, List, Tuple +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import ssdt, driverscan, modules + +# built in Windows-components that trigger false positives +KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] + + +class DriverModule(interfaces.plugins.PluginInterface): + """Determines if any loaded drivers were hidden by a rootkit""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="driverscan", component=driverscan.DriverScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(3, 0, 0) + ), + ] + + def _generator(self) -> Iterator[Tuple]: + """ + Attempt to match each driver's start code address to a known kernel module + A common rootkit technique is to register drivers from modules that are hidden, + which allows us to detect the disconnect between a malicious driver and its hidden module. + """ + collection = ssdt.SSDT.build_module_collection( + context=self.context, + kernel_module_name=self.config["kernel"], + ) + + kernel_space_start = modules.Modules.get_kernel_space_start( + self.context, self.config["kernel"] + ) + + for driver in driverscan.DriverScan.scan_drivers( + self.context, + self.config["kernel"], + ): + # We want starts of 0 as rootkits often set this value + # greater than 0 but less than the kernel space start is smear/terminated though + if 0 < driver.DriverStart < kernel_space_start: + continue + + # we do not care about actual symbol names, we just want to know if the driver points to a known module + module_symbols = list( + collection.get_module_symbols_by_absolute_location(driver.DriverStart) + ) + if not module_symbols: + ( + driver_name, + service_key, + name, + ) = driverscan.DriverScan.get_names_for_driver(driver) + + # drivers without any names will not produce useful output + if not driver_name and not service_key and not name: + continue + + known_exception = driver_name in KNOWN_DRIVERS + + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + known_exception, + driver_name or renderers.NotAvailableValue(), + service_key or renderers.NotAvailableValue(), + name or renderers.NotAvailableValue(), + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Known Exception", bool), + ("Driver Name", str), + ("Service Key", str), + ("Alternative Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/hollowprocesses.py b/volatility3/framework/plugins/windows/malware/hollowprocesses.py new file mode 100644 index 000000000..1ea46b540 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/hollowprocesses.py @@ -0,0 +1,226 @@ +# 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 +from typing import NamedTuple, Dict, Generator + +from volatility3.framework import interfaces, exceptions, constants +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class VadData(NamedTuple): + protection: str + path: str + + +class DLLData(NamedTuple): + path: str + + +### Useful references on process hollowing +# https://cysinfo.com/detecting-deceptive-hollowing-techniques/ +# https://github.com/m0n0ph1/Process-Hollowing + + +class HollowProcesses(interfaces.plugins.PluginInterface): + """Lists hollowed processes""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + ] + + def _get_vads_data( + self, proc: interfaces.objects.ObjectInterface + ) -> Dict[int, VadData]: + """ + Returns a dictionary of: + base address -> (protection string, file name) + For each mapped VAD in the process. This is used + for quick lookups of data and matching the DLL + at the same base address as the VAD + """ + vads = {} + + kernel = self.context.modules[self.config["kernel"]] + + for vad in proc.get_vad_root().traverse(): + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, kernel.layer_name, kernel.symbol_table_name + ), + vadinfo.winnt_protections, + ) + + fn = vad.get_file_name() + if not fn or not isinstance(fn, str): + fn = "" + + vads[vad.get_start()] = VadData(protection_string, fn) + + return vads + + def _get_dlls_map( + self, proc: interfaces.objects.ObjectInterface + ) -> Dict[int, DLLData]: + """ + Returns a dictionary of: + base address -> path + for each DLL loaded in the process + + This is used to cross compare with + the corresponding VAD and to have a + backup path source in case of smear + in the VAD + """ + dlls = {} + + for entry in proc.load_order_modules(): + try: + base = entry.DllBase + except exceptions.InvalidAddressException: + continue + + try: + FullDllName = entry.FullDllName.get_string() + except exceptions.InvalidAddressException: + FullDllName = renderers.UnreadableValue() + + dlls[base] = DLLData(FullDllName) + + return dlls + + def _get_image_base(self, proc: interfaces.objects.ObjectInterface) -> int: + """ + Uses the PEB to get the image base of the process + """ + kernel = self.context.modules[self.config["kernel"]] + + try: + proc_layer_name = proc.add_process_layer() + peb = self.context.object( + kernel.symbol_table_name + constants.BANG + "_PEB", + layer_name=proc_layer_name, + offset=proc.Peb, + ) + return peb.ImageBaseAddress + except exceptions.InvalidAddressException: + return None + + def _check_load_address(self, proc, _, __) -> Generator[str, None, None]: + """ + Detects when the image base in the PEB, which is writable by process malware, + does not match the section base address - whose value lives in kernel memory. + Many malware samples will manipulate their image base to fool AVs/EDRs and + as a necessary part of certain hollowing techniques + """ + image_base = self._get_image_base(proc) + if image_base is not None and image_base != proc.SectionBaseAddress: + yield f"The ImageBaseAddress reported from the PEB ({image_base:#x}) does not match the process SectionBaseAddress ({proc.SectionBaseAddress:#x})" + + def _check_exe_protection( + self, proc, vads: Dict[int, VadData], __ + ) -> Generator[str, None, None]: + """ + Legitimately mapped application executables and DLLs + will have a VAD present and its initial protection will be + PAGE_EXECUTE_WRITECOPY. + Many process hollowing and code injection techniques will + unmap the real executable and/or map in executables with + incorrect permissions. + This check verifies the VAD for the application exe. + `_check_dlls_protection` checks for DLLs mapped in the process. + """ + base = proc.SectionBaseAddress + + if base not in vads: + yield f"There is no VAD starting at the base address of the process executable ({base:#x})" + elif vads[base].protection != "PAGE_EXECUTE_WRITECOPY": + yield f"Unexpected protection ({vads[base].protection}) for VAD hosting the process executable ({base:#x}) with path {vads[base].path}" + + def _check_dlls_protection( + self, _, vads: Dict[int, VadData], dlls: Dict[int, DLLData] + ) -> Generator[str, None, None]: + for dll_base in dlls: + # could be malicious but triggers too many FPs from smear + if dll_base not in vads: + continue + + # PAGE_EXECUTE_WRITECOPY is the only valid permission for mapped DLLs and .exe files + if vads[dll_base].protection != "PAGE_EXECUTE_WRITECOPY": + yield f"Unexpected protection ({vads[dll_base].protection}) for DLL in the PEB's load order list ({dll_base:#x}) with path {dlls[dll_base].path}" + + def _generator(self, procs): + checks = [ + self._check_load_address, + self._check_exe_protection, + self._check_dlls_protection, + ] + + for proc in procs: + # smear and/or terminated process + dlls = self._get_dlls_map(proc) + if len(dlls) < 3: + continue + + vads = self._get_vads_data(proc) + if len(vads) < 5: + continue + + proc_name = utility.array_to_string(proc.ImageFileName) + pid = proc.UniqueProcessId + + for check in checks: + for note in check(proc, vads, dlls): + yield ( + 0, + ( + pid, + proc_name, + note, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Notes", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/indirect_system_calls.py b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py new file mode 100644 index 000000000..ba34eb110 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/indirect_system_calls.py @@ -0,0 +1,121 @@ +# 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 struct +import logging +from typing import List, Optional + +from volatility3.framework import interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.plugins import yarascan +from volatility3.plugins.windows.malware import direct_system_calls + +vollog = logging.getLogger(__name__) + + +class IndirectSystemCalls(direct_system_calls.DirectSystemCalls): + """Detects the Indirect System Call technique used to bypass EDRs.""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.syscall_finder = direct_system_calls.syscall_finder_type( + # gets the target address of a indirect jmp + self._indirect_syscall_block_target, + # we are looking for indirect system calls, so we don't want 'syscall' instructions in our code block + False, + # jmp [address]; ret + "/\\xff\\x25[^\\xc3]{,24}\\xc3/", + # any of these mean we aren't in a malicious indirect call + ["call", "leave", "int3", "ret"], + # stop at jmp, this should reference the system call instruction + ["jmp"], + ) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # create a list of requirements for vadyarascan + vadyarascan_requirements = [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 1, 0) + ), + requirements.VersionRequirement( + name="yarascan", component=yarascan.YaraScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="direct_system_calls", + component=direct_system_calls.DirectSystemCalls, + version=(2, 0, 0), + ), + ] + + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vadyarascan_requirements + + @staticmethod + def _indirect_syscall_block_target( + proc_layer: interfaces.layers.DataLayerInterface, inst + ) -> Optional[int]: + """ + This function determines the address of a jmp in the following form: + + jmp [address] + + To determine this, we must: + 1) Pull the 4 byte relative offset of 'address' inside the instruction + 2) Compute the full address of this relative offset + 3) Read from the address as it is being dereferenced + 4) Ensure the target address points to a 'syscall' instruction + + Args: + proc_layer: the layer of the potential syscall block + inst: the terminating instruction of the syscall block check + Returns: + The target address of the jump if it can be computed + """ + + try: + jmp_address_str = proc_layer.read(inst.address, 6) + except exceptions.InvalidAddressException: + return None + + # Should be an jmp... + if jmp_address_str[0:2] != b"\xff\x25": + return None + + # get the address of the 'jmp [address]' instruction + relative_offset = struct.unpack(" Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: + for vad, data_object in cls.list_injection_sites( + context, kernel_layer_name, symbol_table, proc + ): + yield ( + vad, + data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length + ), + ) + + @classmethod + def list_injection_sites( + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + symbol_table: str, + proc: interfaces.objects.ObjectInterface, + ) -> Generator[ + Tuple[interfaces.objects.ObjectInterface, renderers.LayerData], + None, + None, + ]: + """Generate memory regions for a process that may contain injected + code. + + Args: + context: The context from which to retrieve required elements (layers, symbol tables) + kernel_layer_name: The name of the kernel layer from which to read the VAD protections + symbol_table: The name of the table containing the kernel symbols + proc: an _EPROCESS instance + + Returns: + An iterable of VAD instances and the first 64 bytes of data contained in that region + """ + proc_id = "Unknown" + try: + proc_id = proc.UniqueProcessId + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException as excp: + vollog.debug( + f"Process {proc_id}: invalid address {excp.invalid_address} in layer {excp.layer_name}" + ) + return None + + proc_layer = context.layers[proc_layer_name] + + for vad in proc.get_vad_root().traverse(): + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + context, kernel_layer_name, symbol_table + ), + vadinfo.winnt_protections, + ) + write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string + dirty_page = None + if not write_exec: + """ + # Inspect "PAGE_EXECUTE_READ" VAD pages to detect + # non-writable memory regions having been injected + # using elevated WriteProcessMemory(). + """ + if "EXECUTE" in protection_string: + for page in range( + vad.get_start(), vad.get_end(), proc_layer.page_size + ): + try: + # If we have a dirty page in a non-writable "EXECUTE" region, it is suspicious. + if proc_layer.is_dirty(page): + dirty_page = page + break + except exceptions.InvalidAddressException: + # Abort as it is likely that other addresses in the same range will also fail. + break + if dirty_page is None: + continue + else: + continue + + if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( + vad.get_private_memory() == 0 + and protection_string != "PAGE_EXECUTE_WRITECOPY" + ): + if cls.is_vad_empty(proc_layer, vad): + continue + + if dirty_page is not None: + # Useful information to investigate the page content with volshell afterwards. + vollog.debug( + f"[proc_id {proc_id}] Found suspicious DIRTY + {protection_string} page at {hex(dirty_page)}", + ) + start = vad.get_start() + length = 64 + data = renderers.LayerData( + context=context, + layer_name=proc_layer_name, + offset=start, + length=length, + no_surrounding=True, + ) + yield (vad, data) + + def _generator(self, procs): + # Determine if we're on a 32 or 64 bit kernel + kernel = self.context.modules[self.config["kernel"]] + + # Set refined criteria to know when to add to "Notes" column + refined_criteria = { + b"MZ": "MZ header", + b"\x55\x8b": "PE header", + b"\x55\x48": "Function prologue", + b"\x55\x89": "Function prologue", + } + + is_32bit_arch = not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ) + + for proc in procs: + # By default, "Notes" column will be set to N/A + process_name = utility.array_to_string(proc.ImageFileName) + + for vad, data_object in self.list_injection_sites( + self.context, kernel.layer_name, kernel.symbol_table_name, proc + ): + notes = renderers.NotApplicableValue() + # Check for unique headers and update "Notes" column if criteria is met + data = data_object.context.layers[data_object.layer_name].read( + data_object.offset, data_object.length, True + ) + if data[:2] in refined_criteria: + notes = refined_criteria[data[:2]] + + # If we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 + if is_32bit_arch or proc.get_is_wow64(): + architecture = "intel" + else: + architecture = "intel64" + + disasm = renderers.Disassembly(data, vad.get_start(), architecture) + + file_output = "Disabled" + if self.config["dump"]: + file_output = "Error outputting to file" + try: + file_handle = vadinfo.VadInfo.vad_dump( + self.context, proc, vad, self.open + ) + file_handle.close() + file_output = file_handle.preferred_filename + except (exceptions.InvalidAddressException, OverflowError) as excp: + vollog.debug( + f"Unable to dump PE with pid {proc.UniqueProcessId}.{vad.get_start():#x}: {excp}" + ) + + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(vad.get_start()), + format_hints.Hex(vad.get_end()), + vad.get_tag(), + vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + ), + vadinfo.winnt_protections, + ), + vad.get_commit_charge(), + vad.get_private_memory(), + file_output, + notes, + data_object, + disasm, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start VPN", format_hints.Hex), + ("End VPN", format_hints.Hex), + ("Tag", str), + ("Protection", str), + ("CommitCharge", int), + ("PrivateMemory", int), + ("File output", str), + ("Notes", str), + ("Hexdump", renderers.LayerData), + ("Disasm", renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/pebmasquerade.py b/volatility3/framework/plugins/windows/malware/pebmasquerade.py new file mode 100644 index 000000000..cd898239f --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/pebmasquerade.py @@ -0,0 +1,233 @@ +import logging +from typing import List, Union, Tuple + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + + +# https://www.ired.team/offensive-security/defense-evasion/masquerading-processes-in-userland-through-_peb +# https://github.com/FuzzySecurity/PowerShell-Suite/blob/master/Masquerade-PEB.ps1 +class PebMasquerade(interfaces.plugins.PluginInterface): + """Detects potential process name spoofing by comparing EPROCESS and PEB data.""" + + _version = (1, 0, 0) + _required_framework_version = (2, 27, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + ] + + @classmethod + def get_process_names(cls, proc: interfaces.objects.ObjectInterface) -> Tuple[ + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + Union[str, renderers.NotAvailableValue], + ]: + """Extract process names and related information from various sources (EPROCESS and PEB). + + Args: + proc: The process object + + Returns: + tuple: (eprocess_imagefilename, eprocess_seaudit_imagefilename, peb_imagefilepath, peb_cmdline) + """ + eprocess_imagefilename = renderers.NotAvailableValue() + eprocess_seaudit_imagefilename = renderers.NotAvailableValue() + peb_imagefilepath = renderers.NotAvailableValue() + peb_cmdline = renderers.NotAvailableValue() + + try: + eprocess_imagefilename = utility.array_to_string(proc.ImageFileName) + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read EPROCESS.ImageFileName for PID %d", proc.UniqueProcessId + ) + except Exception as e: + vollog.warning( + "Error reading EPROCESS.ImageFileName for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + audit = proc.SeAuditProcessCreationInfo.ImageFileName.Name + audit_string = audit.get_string() + if audit_string: + eprocess_seaudit_imagefilename = audit_string + except exceptions.InvalidAddressException: + vollog.debug( + "Unable to read SeAuditProcessCreationInfo.ImageFileName for PID %d", + proc.UniqueProcessId, + ) + except AttributeError: + vollog.debug( + "SeAuditProcessCreationInfo structure not available for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading SeAuditProcessCreationInfo for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + peb = proc.get_peb() + if peb and peb.ProcessParameters: + # Get ImagePathName + try: + image_path_str = peb.ProcessParameters.ImagePathName.get_string() + if image_path_str: + peb_imagefilepath = image_path_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ImagePathName for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ImagePathName for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + + try: + cmdline_str = peb.ProcessParameters.CommandLine.get_string() + if cmdline_str: + peb_cmdline = cmdline_str + except (AttributeError, exceptions.InvalidAddressException): + vollog.debug( + "Unable to read PEB.ProcessParameters.CommandLine for PID %d", + proc.UniqueProcessId, + ) + except Exception as e: + vollog.warning( + "Error reading PEB.ProcessParameters.CommandLine for PID %d: %s", + proc.UniqueProcessId, + str(e), + ) + except (AttributeError, exceptions.InvalidAddressException): + # Important for cases where PEB does not exist or is inaccessible (e.g SYSTEM process) + vollog.debug("Unable to access PEB for PID %d", proc.UniqueProcessId) + except Exception as e: + vollog.warning( + "Error accessing PEB for PID %d: %s", proc.UniqueProcessId, str(e) + ) + + return ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) + + def _generator(self, pids, context, kernel_module_name): + pid_filter = pslist.PsList.create_pid_filter(pids) + + for proc in pslist.PsList.list_processes( + context=context, + kernel_module_name=kernel_module_name, + filter_func=pid_filter, + ): + proc_id = proc.UniqueProcessId + try: + peb = proc.get_peb() + except (exceptions.InvalidAddressException, AttributeError): + vollog.debug( + "Unable to access PEB for PID %d, skipping process", proc_id + ) + peb_imagefilepath_length_check = False + peb_cmdline_length_check = False + ( + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline, + ) = PebMasquerade.get_process_names(proc) + + if isinstance(peb_imagefilepath, str) and peb: + try: + # Length values are of type USHORT + peb_imagefilepath_length = ( + peb.ProcessParameters.ImagePathName.Length // 2 + ) + peb_imagefilepath_maxlength = ( + peb.ProcessParameters.ImagePathName.MaximumLength // 2 - 1 + ) + + if (peb_imagefilepath_length != len(peb_imagefilepath)) or ( + peb_imagefilepath_maxlength != len(peb_imagefilepath) + ): + peb_imagefilepath_length_check = True + except Exception as e: + vollog.warning( + "PEB.ImagePathName Length comparison error for PID %d: %s", + proc_id, + str(e), + ) + + if isinstance(peb_cmdline, str) and peb: + try: + # Length values are of type USHORT + peb_cmdline_length = peb.ProcessParameters.CommandLine.Length // 2 + peb_cmdline_maxlength = ( + peb.ProcessParameters.CommandLine.MaximumLength // 2 - 1 + ) + + if (peb_cmdline_length != len(peb_cmdline)) or ( + peb_cmdline_maxlength != len(peb_cmdline) + ): + peb_cmdline_length_check = True + except Exception as e: + vollog.warning( + "PEB.CommandLine Length comparison error for PID %d: %s", + proc_id, + str(e), + ) + yield ( + 0, + ( + proc_id, + eprocess_imagefilename, + eprocess_seaudit_imagefilename, + peb_imagefilepath, + peb_cmdline_length_check, + peb_imagefilepath_length_check, + ), + ) + + def run(self): + pids = self.config.get("pid", None) + context = self.context + kernel_module_name = self.config["kernel"] + return renderers.TreeGrid( + [ + ("PID", int), + ("EPROCESS_ImageFileName", str), + ("EPROCESS_SeAudit_ImageFileName", str), + ("PEB_ImageFilePath", str), + ("PEB_ImageFilePath_Spoofed", bool), + ("PEB_CommandLine_Spoofed", bool), + ], + self._generator(pids, context, kernel_module_name), + ) diff --git a/volatility3/framework/plugins/windows/malware/processghosting.py b/volatility3/framework/plugins/windows/malware/processghosting.py new file mode 100644 index 000000000..fb8ec527d --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/processghosting.py @@ -0,0 +1,226 @@ +# 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 + +from typing import Optional, Tuple, Generator, Dict + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class ProcessGhosting(interfaces.plugins.PluginInterface): + """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose""" + + _version = (1, 0, 0) + _required_framework_version = (2, 4, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 1) + ), + ] + + @classmethod + def _process_checks( + cls, + proc: interfaces.objects.ObjectInterface, + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], + ) -> Generator[ + Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None + ]: + """ + Checks the EPROCESS for signs of ghosting + """ + if not proc.has_member("ImageFilePointer"): + return + + delete_pending = None + + # if it is 0 then its a side effect of process ghosting + if proc.ImageFilePointer.vol.offset != 0: + try: + file_object = proc.ImageFilePointer + delete_pending = file_object.DeletePending + file_object = file_object.dereference().vol.offset + except exceptions.InvalidAddressException: + file_object = 0 + + # ImageFilePointer equal to 0 means process ghosting or similar techniques were used + else: + file_object = 0 + + # delete_pending besides 0 or 1 = smear + if isinstance(delete_pending, int) and delete_pending not in [0, 1]: + vollog.debug( + f"Invalid delete_pending value {delete_pending} found for process {proc.UniqueProcessId}" + ) + delete_pending = None + + if file_object == 0 or delete_pending == 1: + yield file_object, delete_pending, None, proc.SectionBaseAddress + + @classmethod + def _vad_checks( + cls, control_area: interfaces.objects.ObjectInterface, vad_path: str + ) -> Generator[Tuple[int, Optional[int], Optional[int]], None, None]: + """ + Checks the control area for delete on close or delete pending being set + """ + try: + file_object = control_area.FilePointer.dereference().cast("_FILE_OBJECT") + except exceptions.InvalidAddressException: + return + + try: + delete_on_close = control_area.u.Flags.DeleteOnClose + except exceptions.InvalidAddressException: + delete_on_close = None + + if delete_on_close and vad_path.lower().endswith((".exe", ".dll")): + yield file_object.vol.offset, None, delete_on_close + + try: + delete_pending = file_object.DeletePending + except exceptions.InvalidAddressException: + delete_pending = None + + if delete_pending == 1: + yield file_object.vol.offset, delete_pending, None + + @classmethod + def check_for_ghosting( + cls, + proc: interfaces.objects.ObjectInterface, + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], + ) -> Generator[ + Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None + ]: + """ + Returns process or vad info for ghosting files + + Args: + proc: + mapped_files: A dictionary mapping vad base addresses to the path and vad instance for the process + + Return: + A Generator of tuples of the file object address, the delete pending state, delete on close state, base address of the VAD, and the path + """ + # check the direct file object of the process + yield from cls._process_checks(proc, mapped_files) + + # walk each vad, check if it is pending delete or has its delete on close bit set + for vad_base, (path, vad) in mapped_files.items(): + # these checks have no meaning for private memory areas + if vad.get_private_memory() == 1: + continue + + try: + if vad.has_member("ControlArea"): + control_area = vad.ControlArea + elif vad.has_member("Subsection"): + control_area = vad.Subsection.ControlArea + # We got here from a short vad, likely smear + else: + continue + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to get control area for vad at base {vad_base:#x} for process with pid {proc.UniqueProcessId}" + ) + continue + + for file_object_address, delete_pending, delete_on_close in cls._vad_checks( + control_area, path + ): + yield ( + format_hints.Hex(file_object_address), + delete_pending, + delete_on_close, + vad_base, + ) + + def _generator(self, procs): + kernel = self.context.modules[self.config["kernel"]] + + has_imagefilepointer = kernel.get_type("_EPROCESS").has_member( + "ImageFilePointer" + ) + if not has_imagefilepointer: + vollog.warning( + "ImageFilePointer checks are only supported on Windows 10+ builds when the ImageFilePointer member of _EPROCESS is present" + ) + + for proc in procs: + process_name = utility.array_to_string(proc.ImageFileName) + pid = proc.UniqueProcessId + + # base address -> (file path, VAD instance) + mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]] = {} + for vad in vadinfo.VadInfo.list_vads(proc): + path = vad.get_file_name() + if isinstance(path, str): + mapped_files[vad.get_start()] = (path, vad) + + for ( + file_object_address, + delete_pending, + delete_on_close, + base_address, + ) in self.check_for_ghosting(proc, mapped_files): + vad_info = mapped_files.get(base_address) + if vad_info: + path = vad_info[0] + else: + path = renderers.NotAvailableValue() + + yield ( + 0, + ( + pid, + process_name, + format_hints.Hex(base_address), + format_hints.Hex(file_object_address), + delete_pending or renderers.NotApplicableValue(), + delete_on_close or renderers.NotApplicableValue(), + path, + ), + ) + + def run(self): + filter_func = pslist.PsList.create_active_process_filter() + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Base", format_hints.Hex), + ("FILE_OBJECT", format_hints.Hex), + ("DeletePending", int), + ("DeleteOnClose", int), + ("Path", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/psxview.py b/volatility3/framework/plugins/windows/malware/psxview.py new file mode 100644 index 000000000..51616a22c --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/psxview.py @@ -0,0 +1,241 @@ +import datetime +import logging +import string +from itertools import chain +from typing import Dict, Iterable, List + +from volatility3.framework import constants, exceptions, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows import extensions +from volatility3.plugins.windows import handles, pslist, psscan, thrdscan + +vollog = logging.getLogger(__name__) + + +class PsXView(plugins.PluginInterface): + """Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \ +identify processes that are trying to hide themselves. + +We recommend using -r pretty if you are looking at this plugin's output in a terminal.""" + + # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality + # which the original plugin used to do it. + + # The sessions method is omitted because it begins with the list of processes found by Pslist anyway. + + # Lastly, I've omitted the pspcid method because I could not for the life of me get it to work. I saved the + # code I do have from it, and will happily share it if anyone else wants to add it. + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + valid_proc_name_chars = set( + string.ascii_lowercase + string.ascii_uppercase + "." + " " + ) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="psscan", component=psscan.PsScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(4, 0, 0) + ), + requirements.BooleanRequirement( + name="physical-offsets", + description="List processes with physical offsets instead of virtual offsets.", + optional=True, + ), + ] + + def _proc_name_to_string(self, proc): + return proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ) + + def _is_valid_proc_name(self, string: str) -> bool: + return all(c in self.valid_proc_name_chars for c in string) + + def _filter_garbage_procs( + self, proc_list: Iterable[extensions.EPROCESS] + ) -> List[extensions.EPROCESS]: + return [ + p + for p in proc_list + if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) + ] + + def _translate_offset(self, offset: int) -> int: + if not self.config["physical-offsets"]: + return offset + + kernel = self.context.modules[self.config["kernel"]] + layer_name = kernel.layer_name + + try: + _original_offset, _original_length, offset, _length, _layer_name = list( + self.context.layers[layer_name].mapping(offset=offset, length=0) + )[0] + except exceptions.PagedInvalidAddressException: + vollog.debug(f"Page fault: unable to translate {offset:0x}") + + return offset + + def _proc_list_to_dict( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + tasks = self._filter_garbage_procs(tasks) + return {self._translate_offset(proc.vol.offset): proc for proc in tasks} + + def _check_pslist(self, tasks): + return self._proc_list_to_dict(tasks) + + def _check_psscan( + self, + ) -> Dict[int, extensions.EPROCESS]: + res = psscan.PsScan.scan_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + return self._proc_list_to_dict(res) + + def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: + ret = [] + + for ethread in thrdscan.ThrdScan.scan_threads( + self.context, module_name="kernel" + ): + process = None + try: + process = ethread.owning_process() + if not process.is_valid(): + continue + + ret.append(process) + except AttributeError: + vollog.log( + constants.LOGLEVEL_VVV, + "Unable to find the owning process of ethread", + ) + + return self._proc_list_to_dict(ret) + + def _check_csrss_handles( + self, tasks: Iterable[extensions.EPROCESS] + ) -> Dict[int, extensions.EPROCESS]: + ret: List[extensions.EPROCESS] = [] + + type_map = handles.Handles.get_type_map( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + cookie = handles.Handles.find_cookie( + context=self.context, kernel_module_name=self.config["kernel"] + ) + + for p in tasks: + name = self._proc_name_to_string(p) + if name != "csrss.exe": + continue + + try: + ret += [ + handle.Body.cast("_EPROCESS") + for handle in handles.Handles.handles( + context=self.context, + kernel_module_name=self.config["kernel"], + handle_table=p.ObjectTable, + ) + if handle.get_object_type(type_map, cookie) == "Process" + ] + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVV, "Cannot access eprocess object table" + ) + + return self._proc_list_to_dict(ret) + + def _generator(self): + kdbg_list_processes = list( + pslist.PsList.list_processes( + context=self.context, kernel_module_name=self.config["kernel"] + ) + ) + + # get processes from each source + processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} + + processes["pslist"] = self._check_pslist(kdbg_list_processes) + processes["psscan"] = self._check_psscan() + processes["thrdscan"] = self._check_thrdscan() + processes["csrss"] = self._check_csrss_handles(kdbg_list_processes) + + # Unique set of all offsets from all sources + offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) + + for offset in offsets: + # We know there will be at least one process mapped to each offset + proc: extensions.EPROCESS = next( + mapping[offset] for mapping in processes.values() if offset in mapping + ) + + in_sources = {src: False for src in processes} + + for source, process_mapping in processes.items(): + if offset in process_mapping: + in_sources[source] = True + + pid = proc.UniqueProcessId + name = self._proc_name_to_string(proc) + + exit_time = proc.get_exit_time() + if type(exit_time) is not datetime.datetime: + exit_time = "" + else: + exit_time = str(exit_time) + + yield ( + 0, + ( + format_hints.Hex(offset), + name, + pid, + in_sources["pslist"], + in_sources["psscan"], + in_sources["thrdscan"], + in_sources["csrss"], + exit_time, + ), + ) + + def run(self): + offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" + offset_str = "Offset" + offset_type + + return renderers.TreeGrid( + [ + (offset_str, format_hints.Hex), + ("Name", str), + ("PID", int), + ("pslist", bool), + ("psscan", bool), + ("thrdscan", bool), + ("csrss", bool), + ("Exit Time", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/skeleton_key_check.py b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py new file mode 100644 index 000000000..10c6222bc --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/skeleton_key_check.py @@ -0,0 +1,689 @@ +# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +# This module attempts to locate skeleton-key like function hooks. +# It does this by locating the CSystems array through a variety of methods, +# and then validating the entry for RC4 HMAC (0x17 / 23) +# +# For a thorough walkthrough on how the R&D was performed to develop this plugin, +# please see our blogpost here: +# +# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html + +import logging +from typing import Iterable, Tuple, List, Optional + +import pefile + +from volatility3.framework import interfaces, symbols, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.layers import scanners +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows import pdbutil +from volatility3.framework.symbols.windows.extensions import pe +from volatility3.plugins.windows import pslist, vadinfo, pe_symbols + +try: + import capstone + + has_capstone = True +except ImportError: + has_capstone = False + +vollog = logging.getLogger(__name__) + + +class Skeleton_Key_Check(interfaces.plugins.PluginInterface): + """Looks for signs of Skeleton Key malware""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="bytes_scanner", + component=scanners.BytesScanner, + version=(1, 0, 0), + ), + ] + + def _check_for_skeleton_key_vad( + self, + csystem: interfaces.objects.ObjectInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> bool: + """ + Checks if Initialize and/or Decrypt is hooked by determining if + these function pointers reference addresses inside of the cryptdll VAD + + Args: + csystem: The RC4HMAC KERB_ECRYPT instance + cryptdll_base: Base address of the cryptdll.dll VAD + cryptdll_size: Size of the VAD + Returns: + bool: if a skeleton key hook is present + """ + return not ( + (cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) + and (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size) + ) + + def _check_for_skeleton_key_symbols( + self, + csystem: interfaces.objects.ObjectInterface, + rc4HmacInitialize: int, + rc4HmacDecrypt: int, + ) -> bool: + """ + Uses the PDB information to specifically check if the csystem for RC4HMAC + has an initialization pointer to rc4HmacInitialize and a decryption pointer + to rc4HmacDecrypt. + + Args: + csystem: The RC4HMAC KERB_ECRYPT instance + rc4HmacInitialize: The expected address of csystem Initialization function + rc4HmacDecrypt: The expected address of the csystem Decryption function + + Returns: + bool: if a skeleton key hook was found + """ + return ( + csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt + ) + + def _construct_ecrypt_array( + self, + array_start: int, + count: int, + cryptdll_types: interfaces.context.ModuleInterface, + ) -> interfaces.context.ModuleInterface: + """ + Attempts to construct an array of _KERB_ECRYPT structures + + Args: + array_start: starting virtual address of the array + count: how many elements are in the array + cryptdll_types: the reverse engineered types + + Returns: + The instantiated array + """ + + try: + array = cryptdll_types.object( + object_type="array", + offset=array_start, + subtype=cryptdll_types.get_type("_KERB_ECRYPT"), + count=count, + absolute=True, + ) + + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to construct cSystems array at given offset: {array_start:x}" + ) + array = None + + return array + + def _find_array_with_pdb_symbols( + self, + cryptdll_symbols: str, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + cryptdll_base: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: + """ + Finds the CSystems array through use of PDB symbols + + Args: + cryptdll_symbols: The symbols table from the PDB file + cryptdll_types: The types from cryptdll binary analysis + proc_layer_name: The lsass.exe process layer name + cryptdll_base: Base address of cryptdll.dll inside of lsass.exe + + Returns: + Tuple of: + array: The cSystems array + rc4HmacInitialize: The runtime address of the expected initialization function + rc4HmacDecrypt: The runtime address of the expected decryption function + """ + cryptdll_module = self.context.module( + cryptdll_symbols, layer_name=proc_layer_name, offset=cryptdll_base + ) + + rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address( + "rc4HmacInitialize" + ) + + rc4HmacDecrypt = cryptdll_module.get_absolute_symbol_address("rc4HmacDecrypt") + + count_address = cryptdll_module.get_symbol("cCSystems").address + + # we do not want to fail just because the count is not in memory + # 16 was the size on samples I tested, so I chose it as the default + try: + count = cryptdll_types.object( + object_type="unsigned long", offset=count_address + ) + except exceptions.InvalidAddressException: + count = 16 + + array_start = cryptdll_module.get_absolute_symbol_address("CSystems") + + array = self._construct_ecrypt_array(array_start, count, cryptdll_types) + + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB based analysis." + ) + + return array, rc4HmacInitialize, rc4HmacDecrypt + + def _get_cryptdll_types( + self, + context: interfaces.context.ContextInterface, + config, + config_path: str, + proc_layer_name: str, + cryptdll_base: int, + ): + """ + Builds a symbol table from the cryptdll types generated after binary analysis + + Args: + context: the context to operate upon + config: + config_path: + proc_layer_name: name of the lsass.exe process layer + cryptdll_base: base address of cryptdll.dll inside of lsass.exe + """ + kernel = self.context.modules[self.config["kernel"]] + table_mapping = {"nt_symbols": kernel.symbol_table_name} + + cryptdll_symbol_table = intermed.IntermediateSymbolTable.create( + context=context, + config_path=config_path, + sub_path="windows", + filename="kerb_ecrypt", + table_mapping=table_mapping, + ) + + return context.module( + cryptdll_symbol_table, proc_layer_name, offset=cryptdll_base + ) + + def _find_lsass_proc( + self, proc_list: Iterable + ) -> Tuple[interfaces.context.ContextInterface, str]: + """ + Walks the process list and returns the first valid lsass instances. + There should be only one lsass process, but malware will often use the + process name to try and blend in. + + Args: + proc_list: The process list generator + + Return: + The process object for lsass + """ + + for proc in proc_list: + try: + proc_layer_name = proc.add_process_layer() + + return proc, proc_layer_name + + except exceptions.InvalidAddressException as excp: + vollog.debug( + f"Invalid address {excp.invalid_address} in layer {excp.layer_name}" + ) + + return None, None + + def _find_cryptdll( + self, lsass_proc: interfaces.context.ContextInterface + ) -> Tuple[int, int]: + """ + Finds the base address of cryptdll.dll inside of lsass.exe + + Args: + lsass_proc: the process object for lsass.exe + + Returns: + A tuple of: + cryptdll_base: the base address of cryptdll.dll + crytpdll_size: the size of the VAD for cryptdll.dll + """ + for vad in lsass_proc.get_vad_root().traverse(): + filename = vad.get_file_name() + + if isinstance(filename, str) and filename.lower().endswith("cryptdll.dll"): + base = vad.get_start() + return base, vad.get_size() + + return None, None + + def _find_csystems_with_symbols( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int]: + """ + Attempts to find CSystems and the expected address of the handlers. + Relies on downloading and parsing of the cryptdll PDB file. + + Args: + proc_layer_name: the name of the lsass.exe process layer + cryptdll_types: The types from cryptdll binary analysis + cryptdll_base: the base address of cryptdll.dll + crytpdll_size: the size of the VAD for cryptdll.dll + + Returns: + A tuple of: + array: An initialized Volatility array of _KERB_ECRYPT structures + rc4HmacInitialize: The expected address of csystem Initialization function + rc4HmacDecrypt: The expected address of the csystem Decryption function + """ + try: + cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( + self.context, + interfaces.configuration.path_join(self.config_path, "cryptdll"), + proc_layer_name, + "cryptdll.pdb", + cryptdll_base, + cryptdll_size, + ) + except exceptions.VolatilityException: + vollog.debug( + "Unable to use the cryptdll PDB. Stopping PDB symbols based analysis." + ) + return None, None, None + + array, rc4HmacInitialize, rc4HmacDecrypt = self._find_array_with_pdb_symbols( + cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base + ) + + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB symbols based analysis." + ) + + return array, rc4HmacInitialize, rc4HmacDecrypt + + def _get_rip_relative_target(self, inst) -> int: + """ + Returns the target address of a RIP-relative instruction. + + These instructions contain the offset of a target address + relative to the current instruction pointer. + + Args: + inst: A capstone instruction instance + + Returns: + None or the target address of the instruction + """ + try: + opnd = inst.operands[1] + except capstone.CsError: + return None + + if opnd.type != capstone.x86.X86_OP_MEM: + return None + + if inst.reg_name(opnd.mem.base) != "rip": + return None + + return inst.address + inst.size + opnd.mem.disp + + def _analyze_cdlocatecsystem( + self, + function_bytes: bytes, + function_start: int, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + ) -> Optional[interfaces.objects.ObjectInterface]: + """ + Performs static analysis on CDLocateCSystem to find the instructions that + reference CSystems as well as cCsystems + + Args: + function_bytes: the instruction bytes of CDLocateCSystem + function_start: the address of CDLocateCSystem + proc_layer_name: the name of the lsass.exe process layer + + Return: + The cSystems array of ecrypt instances + """ + found_count = False + array_start = None + count = None + + ## we only support 64bit disassembly analysis + md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) + md.detail = True + + for inst in md.disasm(function_bytes, function_start): + # we should not reach debug traps + if inst.mnemonic == "int3": + break + + # cCsystems is referenced by a mov instruction + elif inst.mnemonic == "mov": + if not found_count: + target_address = self._get_rip_relative_target(inst) + + # we do not want to fail just because the count is not in memory + # 16 was the size on samples I tested, so I chose it as the default + count = 16 + + if target_address: + try: + count = int.from_bytes( + self.context.layers[proc_layer_name].read( + target_address, 4 + ), + "little", + ) + except exceptions.InvalidAddressException: + vollog.debug( + "Unable to read `cCsystems`. Defaulting to 16." + ) + + found_count = True + + elif inst.mnemonic == "lea": + target_address = self._get_rip_relative_target(inst) + + if target_address: + array_start = target_address + + # we find the count before, so we can terminate the static analysis here + break + + if array_start and count: + array = self._construct_ecrypt_array(array_start, count, cryptdll_types) + else: + array = None + + return array + + def _find_csystems_with_export( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + _, + ) -> Optional[interfaces.objects.ObjectInterface]: + """ + Uses export table analysis to locate CDLocateCsystem + This function references CSystems and cCsystems + + Args: + proc_layer_name: The lsass.exe process layer name + cryptdll_types: The types from cryptdll binary analysis + cryptdll_base: Base address of cryptdll.dll inside of lsass.exe + _: unused in this source + Returns: + The cSystems array + """ + + if not has_capstone: + vollog.debug( + "capstone is not installed so cannot fall back to export table analysis." + ) + return None + + vollog.debug( + "Unable to perform analysis using PDB symbols, falling back to export table analysis." + ) + + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + + cryptdll = pe_symbols.PESymbols.get_pefile_obj( + self.context, pe_table_name, proc_layer_name, cryptdll_base + ) + if not cryptdll: + return None + + cryptdll.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + if not hasattr(cryptdll, "DIRECTORY_ENTRY_EXPORT"): + return None + + # find the location of CDLocateCSystem and then perform static analysis + for export in cryptdll.DIRECTORY_ENTRY_EXPORT.symbols: + if export.name != b"CDLocateCSystem": + continue + + function_start = cryptdll_base + export.address + + try: + function_bytes = self.context.layers[proc_layer_name].read( + function_start, 0x50 + ) + except exceptions.InvalidAddressException: + vollog.debug( + "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis." + ) + break + + array = self._analyze_cdlocatecsystem( + function_bytes, function_start, cryptdll_types, proc_layer_name + ) + if array is None: + vollog.debug( + "The CSystem array is not present in memory. Stopping export based analysis." + ) + + return array + + return None + + def _find_csystems_with_scanning( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> List[interfaces.context.ModuleInterface]: + """ + Performs scanning to find potential RC4 HMAC csystem instances + + This function may return several values as it cannot validate which is the active one + + Args: + proc_layer_name: the lsass.exe process layer name + cryptdll_types: the types from cryptdll binary analysis + cryptdll_base: base address of cryptdll.dll inside of lsass.exe + cryptdll_size: size of the VAD + Returns: + A list of csystem instances + """ + + csystems = [] + + cryptdll_end = cryptdll_base + cryptdll_size + + proc_layer = self.context.layers[proc_layer_name] + + ecrypt_size = cryptdll_types.get_type("_KERB_ECRYPT").size + + # scan for potential instances of RC4 HMAC + # the signature is based on the type being 0x17 + # and the block size member being 1 in all test samples + for address in proc_layer.scan( + self.context, + scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), + sections=[(cryptdll_base, cryptdll_size)], + ): + # this occurs across page boundaries + if not proc_layer.is_valid(address, ecrypt_size): + continue + + kerb = cryptdll_types.object("_KERB_ECRYPT", offset=address, absolute=True) + + # ensure the Encrypt and Finish pointers are inside the VAD + # these are not manipulated in the attack + if (cryptdll_base < kerb.Encrypt < cryptdll_end) and ( + cryptdll_base < kerb.Finish < cryptdll_end + ): + csystems.append(kerb) + + return csystems + + def _generator(self, procs): + """ + Finds instances of the RC4 HMAC CSystem structure + + Returns whether the instances are hooked as well as the function handler addresses + + Args: + procs: the process list filtered to lsass.exe instances + """ + kernel = self.context.modules[self.config["kernel"]] + + if not symbols.symbol_table_is_64bit( + context=self.context, symbol_table_name=kernel.symbol_table_name + ): + vollog.info("This plugin only supports 64bit Windows memory samples") + return None + + lsass_proc, proc_layer_name = self._find_lsass_proc(procs) + if not lsass_proc: + vollog.info( + "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed." + ) + return None + + cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc) + if not cryptdll_base: + vollog.info( + "Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed." + ) + return None + + # the custom type information from binary analysis + cryptdll_types = self._get_cryptdll_types( + self.context, self.config, self.config_path, proc_layer_name, cryptdll_base + ) + + # attempt to find the array and symbols directly from the PDB + csystems, rc4HmacInitialize, rc4HmacDecrypt = self._find_csystems_with_symbols( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) + + # if we can't find cSystems through the PDB then + # we fall back to export analysis and scanning + # we keep the address of the rc4 functions from the PDB + # though as its our only source to get them + if csystems is None: + fallback_sources = [ + self._find_csystems_with_export, + self._find_csystems_with_scanning, + ] + + for source in fallback_sources: + csystems = source( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) + + if csystems is not None: + break + + if csystems is None: + vollog.info( + "Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed." + ) + return None + + for csystem in csystems: + if not self.context.layers[proc_layer_name].is_valid( + csystem.vol.offset, csystem.vol.size + ): + continue + + # filter for RC4 HMAC + if csystem.EncryptionType != 0x17: + continue + + # use the specific symbols if present, otherwise use the vad start and size + if rc4HmacInitialize and rc4HmacDecrypt: + skeleton_key_present = self._check_for_skeleton_key_symbols( + csystem, rc4HmacInitialize, rc4HmacDecrypt + ) + else: + skeleton_key_present = self._check_for_skeleton_key_vad( + csystem, cryptdll_base, cryptdll_size + ) + + yield ( + 0, + ( + lsass_proc.UniqueProcessId, + "lsass.exe", + skeleton_key_present, + format_hints.Hex(csystem.Initialize), + format_hints.Hex(csystem.Decrypt), + ), + ) + + def _lsass_proc_filter(self, proc): + """ + Used to filter to only lsass.exe processes + + There should only be one of these, but malware can/does make lsass.exe + named processes to blend in or uses lsass.exe as a process hollowing target + """ + process_name = utility.array_to_string(proc.ImageFileName) + + return process_name != "lsass.exe" + + def run(self): + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Skeleton Key Found", bool), + ("rc4HmacInitialize", format_hints.Hex), + ("rc4HmacDecrypt", format_hints.Hex), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=self._lsass_proc_filter, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/malware/suspicious_threads.py b/volatility3/framework/plugins/windows/malware/suspicious_threads.py new file mode 100644 index 000000000..803a0b04b --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/suspicious_threads.py @@ -0,0 +1,224 @@ +# 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 +from typing import List, Dict, Tuple, Generator +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan + +vollog = logging.getLogger(__name__) + + +class SuspiciousThreads(interfaces.plugins.PluginInterface): + """Lists suspicious userland process threads""" + + _required_framework_version = (2, 4, 0) + _version = (2, 0, 1) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.VersionRequirement( + name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="threads", component=threads.Threads, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + ] + + def _get_ranges( + self, + kernel: interfaces.context.ModuleInterface, + all_ranges: Dict[int, List[Tuple[int, int, str, str]]], + proc, + ) -> Tuple[int, int, str, str]: + """ + Maintains a hash table so each process' VADs + are only enumerated once per plugin run + """ + key = proc.vol.offset + + if key not in all_ranges: + all_ranges[key] = [] + + for vad in proc.get_vad_root().traverse(): + fn = vad.get_file_name() + if not isinstance(fn, str) or not fn: + fn = None + + protection_string = vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, kernel.layer_name, kernel.symbol_table_name + ), + vadinfo.winnt_protections, + ) + + all_ranges[key].append( + (vad.get_start(), vad.get_end(), protection_string, fn) + ) + + return all_ranges[key] + + def _get_range( + self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int + ) -> Tuple[int, str, str]: + """ + Walks a process' VADs looking for the one + containing `address` + + Returns its base address, protection string, and mapped file, if any + """ + for start, end, protection_string, fn in ranges: + if start <= address < end: + return start, protection_string, fn + + return None, None, None + + def _check_thread_address( + self, exe_path: str, ranges, thread_address: int + ) -> Generator[Tuple[str, str], None, None]: + vad_base, prot, vad_path = self._get_range(ranges, thread_address) + + # threads outside of a VAD means either smear from this thread or this process' VAD tree + if vad_base is None: + return + + if vad_path is None: + # set this so checks after report the non file backed region in the path column + vad_path = "" + + yield ( + vad_path, + f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", + ) + + # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions + if prot != "PAGE_EXECUTE_WRITECOPY": + yield ( + vad_path, + f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", + ) + + # check for process hollowing type techniques that mapped in a second, malicious exe file + if ( + exe_path + and vad_path.lower().endswith(".exe") + and (vad_path.lower() != exe_path.lower()) + ): + yield ( + vad_path, + "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process executable", + ) + + def _enumerate_processes( + self, kernel: interfaces.context.ModuleInterface, all_ranges + ): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + for proc in pslist.PsList.list_processes( + context=self.context, + kernel_module_name=self.config["kernel"], + filter_func=filter_func, + ): + ranges = self._get_ranges(kernel, all_ranges, proc) + + # smeared vads or process is terminating + if len(all_ranges[proc.vol.offset]) < 5: + continue + + pid = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + + _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) + if not isinstance(exe_path, str): + exe_path = None + + yield proc, pid, proc_name, exe_path, ranges + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + + all_ranges = {} + + for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( + kernel, all_ranges + ): + # processes often create multiple threads at the same address + # there is no benefit to checking the same address more than once per process + checked = set() + + for thread in threads.Threads.list_threads( + self.context, self.config["kernel"], proc + ): + # do not process if a thread is exited or terminated (4 = Terminated) + if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: + continue + + # bail if accessing the threads members causes a page fault + info = thrdscan.ThrdScan.gather_thread_info(thread) + if not info: + continue + + _, _, tid, start_address, _, win32_start_address, _, _, _ = info + + addresses = [ + (start_address, "Start"), + (win32_start_address, "Win32Start"), + ] + + for address, context in addresses: + if address in checked: + continue + checked.add(address) + + for vad_path, note in self._check_thread_address( + exe_path, ranges, address + ): + yield ( + 0, + ( + proc_name, + pid, + tid, + context, + format_hints.Hex(address), + vad_path, + note, + ), + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Process", str), + ("PID", int), + ("TID", int), + ("Context", str), + ("Address", format_hints.Hex), + ("VAD Path", str), + ("Note", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/malware/svcdiff.py b/volatility3/framework/plugins/windows/malware/svcdiff.py new file mode 100644 index 000000000..78b61eb67 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/svcdiff.py @@ -0,0 +1,102 @@ +# 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 +# +# This module compares services found through list walking versus scanning, +# with the aim of finding hidden services. +# +# For background of hidden services and a real-world example of the use of this plugin, +# please see our blogpost: +# +# https://volatilityfoundation.org/memory-forensics-rd-illustrated-detecting-hidden-windows-services/ + +import logging + +from volatility3.framework import symbols, interfaces +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import svclist, svcscan +from volatility3.framework.symbols.windows import versions + +vollog = logging.getLogger(__name__) + + +class SvcDiff(svcscan.SvcScan): + """Compares services found through list walking versus scanning to find rootkits""" + + _required_framework_version = (2, 4, 0) + + _version = (2, 0, 0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._enumeration_method = self.service_diff + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="svclist", component=svclist.SvcList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="svcscan", component=svcscan.SvcScan, version=(4, 0, 0) + ), + ] + + @classmethod + def service_diff( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + service_table_name: str, + service_binary_dll_map, + filter_func, + ): + """ + On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list + and scan for services then report differences + """ + kernel = context.modules[kernel_module_name] + + if not symbols.symbol_table_is_64bit( + context=context, symbol_table_name=kernel.symbol_table_name + ) or not versions.is_win10_15063_or_later( + context=context, symbol_table=kernel.symbol_table_name + ): + vollog.warning( + "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" + ) + return + + from_scan = set() + from_list = set() + records = {} + + # collect unique service names from scanning + for service in svcscan.SvcScan.service_scan( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_scan.add(service[6]) + records[service[6]] = service + + # collect services from listing walking + for service in svclist.SvcList.service_list( + context, + kernel_module_name, + service_table_name, + service_binary_dll_map, + filter_func, + ): + from_list.add(service[6]) + + # report services found from scanning but not list walking + for hidden_service in from_scan - from_list: + yield records[hidden_service] diff --git a/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py new file mode 100644 index 000000000..71dc20021 --- /dev/null +++ b/volatility3/framework/plugins/windows/malware/unhooked_system_calls.py @@ -0,0 +1,202 @@ +# 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 + +# Full details on the techniques used in these plugins to detect EDR-evading malware +# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation +# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf + +import logging + +from typing import Dict, Tuple, List, Generator + +from volatility3.framework import interfaces, exceptions +from volatility3.framework import renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.plugins.windows import pslist, pe_symbols + +vollog = logging.getLogger(__name__) + + +class UnhookedSystemCalls(interfaces.plugins.PluginInterface): + """Detects hooked ntdll.dll stub functions in Windows processes.""" + + _required_framework_version = (2, 4, 0) + _version = (2, 0, 0) + + system_calls = { + "ntdll.dll": { + pe_symbols.wanted_names_identifier: [ + "NtCreateThread", + "NtProtectVirtualMemory", + "NtReadVirtualMemory", + "NtOpenProcess", + "NtWriteFile", + "NtQueryVirtualMemory", + "NtAllocateVirtualMemory", + "NtWorkerFactoryWorkerReady", + "NtAcceptConnectPort", + "NtAddDriverEntry", + "NtAdjustPrivilegesToken", + "NtAlpcCreatePort", + "NtClose", + "NtCreateFile", + "NtCreateMutant", + "NtOpenFile", + "NtOpenIoCompletion", + "NtOpenJobObject", + "NtOpenKey", + "NtOpenKeyEx", + "NtOpenThread", + "NtOpenThreadToken", + "NtOpenThreadTokenEx", + "NtWriteVirtualMemory", + "NtTraceEvent", + "NtTranslateFilePath", + "NtUmsThreadYield", + "NtUnloadDriver", + "NtUnloadKey", + "NtUnloadKey2", + "NtUnloadKeyEx", + "NtCreateKey", + "NtCreateSection", + "NtDeleteKey", + "NtDeleteValueKey", + "NtDuplicateObject", + "NtQueryValueKey", + "NtReplaceKey", + "NtRequestWaitReplyPort", + "NtRestoreKey", + "NtSetContextThread", + "NtSetSecurityObject", + "NtSetValueKey", + "NtSystemDebugControl", + "NtTerminateProcess", + ] + } + } + + # This data structure is used to track unique implementations of functions across processes + # The outer dictionary holds the module name (e.g., ntdll.dll) + # The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module + # The innermost dictionary holds the unique implementation (bytes) of a function across processes + # Each implementation is tracked along with the process(es) that host it + # For systems without malware, all functions should have the same implementation + # When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations + _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] + + @classmethod + def get_requirements(cls) -> List: + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) + ), + ] + + def _gather_code_bytes( + self, + kernel_module_name: str, + found_symbols: pe_symbols.found_symbols_type, + ) -> _code_bytes_type: + """ + Enumerates the desired DLLs and function implementations in each process + Groups based on unique implementations of each DLLs' functions + The purpose is to detect when a function has different implementations (code) + in different processes. + This very effectively detects code injection. + """ + code_bytes: UnhookedSystemCalls._code_bytes_type = {} + + procs = pslist.PsList.list_processes(self.context, kernel_module_name) + + for proc in procs: + try: + proc_id = proc.UniqueProcessId + proc_name = utility.array_to_string(proc.ImageFileName) + proc_layer_name = proc.add_process_layer() + except exceptions.InvalidAddressException: + continue + + for dll_name, functions in found_symbols.items(): + for func_name, func_addr in functions: + try: + fbytes = self.context.layers[proc_layer_name].read( + func_addr, 0x20 + ) + except exceptions.InvalidAddressException: + continue + + # see the definition of _code_bytes_type for details of this data structure + if dll_name not in code_bytes: + code_bytes[dll_name] = {} + + if func_name not in code_bytes[dll_name]: + code_bytes[dll_name][func_name] = {} + + if fbytes not in code_bytes[dll_name][func_name]: + code_bytes[dll_name][func_name][fbytes] = [] + + code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name)) + + return code_bytes + + def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: + found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( + context=self.context, + config_path=self.config_path, + kernel_module_name=self.config["kernel"], + symbols=UnhookedSystemCalls.system_calls, + ) + + # code_bytes[dll_name][func_name][func_bytes] + code_bytes = self._gather_code_bytes(self.config["kernel"], found_symbols) + + # walk the functions that were evaluated + for functions in code_bytes.values(): + # cbb is the distinct groups of bytes (instructions) + # for this function across processes + for func_name, cbb in functions.items(): + # the dict key here is the raw instructions, which is not helpful to look at + # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) + cb = list(cbb.values()) + + # if all processes map to the same implementation, then no malware is present + if len(cb) == 1: + yield 0, (func_name, "", len(cb[0])) + else: + # if there are differing implementations then it means + # that malware has overwritten system call(s) in infected processes + # max_idx and small_idx find which implementation of a system call has the least processes + # as all observed malware and open source projects only infected a few targets, leaving the + # rest with the original EDR hooks in place + max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 + small_idx = (~max_idx) & 1 + + ps = [] + + # gather processes on small_idx since these are the malware infected ones + for pid, pname in cb[small_idx]: + ps.append(f"{pid:d}:{pname}") + + proc_names = ", ".join(ps) + + yield 0, (func_name, proc_names, len(cb[max_idx])) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Function", str), + ("Distinct Implementations", str), + ("Total Implementations", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index f0be3cf7f..2dfecb93b 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -132,19 +132,22 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # There should only be one STANDARD_INFORMATION attribute, but we # do this just in case. for std_information in mft_record.standard_information_entries(): - yield 0, cls.MFTScanResult( - format_hints.Hex(std_information.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - renderers.NotApplicableValue(), - "STANDARD_INFORMATION", - conversion.wintime_to_datetime(std_information.CreationTime), - conversion.wintime_to_datetime(std_information.ModifiedTime), - conversion.wintime_to_datetime(std_information.UpdatedTime), - conversion.wintime_to_datetime(std_information.AccessedTime), - renderers.NotApplicableValue(), + yield ( + 0, + cls.MFTScanResult( + format_hints.Hex(std_information.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + renderers.NotApplicableValue(), + "STANDARD_INFORMATION", + conversion.wintime_to_datetime(std_information.CreationTime), + conversion.wintime_to_datetime(std_information.ModifiedTime), + conversion.wintime_to_datetime(std_information.UpdatedTime), + conversion.wintime_to_datetime(std_information.AccessedTime), + renderers.NotApplicableValue(), + ), ) except exceptions.InvalidAddressException: pass @@ -163,26 +166,28 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # File Name Attribute try: for filename_info in mft_record.filename_entries(): - # If we don't have a valid enum, coerce to hex so we can keep the record try: permissions = filename_info.Flags.lookup() except ValueError: permissions = hex(filename_info.Flags) - yield 1, cls.MFTScanResult( - format_hints.Hex(filename_info.vol.offset), - str(mft_record.get_signature()), - mft_record.RecordNumber, - mft_record.LinkCount, - mft_flag, - permissions, - "FILE_NAME", - conversion.wintime_to_datetime(filename_info.CreationTime), - conversion.wintime_to_datetime(filename_info.ModifiedTime), - conversion.wintime_to_datetime(filename_info.UpdatedTime), - conversion.wintime_to_datetime(filename_info.AccessedTime), - filename_info.get_full_name(), + yield ( + 1, + cls.MFTScanResult( + format_hints.Hex(filename_info.vol.offset), + str(mft_record.get_signature()), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + permissions, + "FILE_NAME", + conversion.wintime_to_datetime(filename_info.CreationTime), + conversion.wintime_to_datetime(filename_info.ModifiedTime), + conversion.wintime_to_datetime(filename_info.UpdatedTime), + conversion.wintime_to_datetime(filename_info.AccessedTime), + filename_info.get_full_name(), + ), ) except exceptions.InvalidAddressException: return @@ -214,22 +219,25 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield level, ( - record.offset, - record.record_type, - int(record.record_number), - int(record.link_count), - record.mft_type, - record.permissions, - record.attribute_type, - record.created, - record.modified, - record.updated, - record.accessed, + yield ( + level, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + record.record_type, + int(record.record_number), + int(record.link_count), + record.mft_type, + record.permissions, + record.attribute_type, + record.created, + record.modified, + record.updated, + record.accessed, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), ), ) @@ -344,22 +352,25 @@ class ADS(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - record.offset, - str(record.signature), - int(record.record_number), - record.attribute_type, + yield ( + 0, ( - str(record.filename) - if isinstance(record.filename, objects.String) - else record.filename + record.offset, + str(record.signature), + int(record.record_number), + record.attribute_type, + ( + str(record.filename) + if isinstance(record.filename, objects.String) + else record.filename + ), + ( + str(record.stream_name) + if isinstance(record.stream_name, objects.String) + else record.stream_name + ), + record.content, ), - ( - str(record.stream_name) - if isinstance(record.stream_name, objects.String) - else record.stream_name - ), - record.content, ) def run(self): @@ -454,13 +465,16 @@ class ResidentData(interfaces.plugins.PluginInterface): # but in this case memory usage is so extreme due to the number of # records that it becomes necessary. The rich types are still # exposed through classmethods. - yield 0, ( - resident_data_entry.offset, - str(resident_data_entry.signature), - int(resident_data_entry.record_number), - resident_data_entry.attribute_type, - str(resident_data_entry.filename), - resident_data_entry.content, + yield ( + 0, + ( + resident_data_entry.offset, + str(resident_data_entry.signature), + int(resident_data_entry.record_number), + resident_data_entry.attribute_type, + str(resident_data_entry.filename), + resident_data_entry.content, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index 1ec965737..18a7b5919 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -119,13 +119,16 @@ class Modules(interfaces.plugins.PluginInterface): if self.config["dump"]: file_output = self.dump_module(session_layers, pe_table_name, mod) - yield 0, ( - format_hints.Hex(mod.vol.offset), - format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), - BaseDllName, - FullDllName, - file_output, + yield ( + 0, + ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output, + ), ) @classmethod diff --git a/volatility3/framework/plugins/windows/orphan_kernel_threads.py b/volatility3/framework/plugins/windows/orphan_kernel_threads.py index b4dec0fc5..16a25503e 100644 --- a/volatility3/framework/plugins/windows/orphan_kernel_threads.py +++ b/volatility3/framework/plugins/windows/orphan_kernel_threads.py @@ -85,7 +85,7 @@ class Threads(thrdscan.ThrdScan): # previous methods for determining if a thread was a kernel thread # such as bit fields and flags are not stable in Win10+ # so we check if the thread is from the kernel itself or one its child - # kernel processes (MemCompression, Regsitry, ...) + # kernel processes (MemCompression, Registry, ...) if pid != 4 and ppid != 4: continue diff --git a/volatility3/framework/plugins/windows/pe_symbols.py b/volatility3/framework/plugins/windows/pe_symbols.py index e3af0c28a..b21e39a8c 100644 --- a/volatility3/framework/plugins/windows/pe_symbols.py +++ b/volatility3/framework/plugins/windows/pe_symbols.py @@ -37,7 +37,7 @@ filter_modules_type = Dict[str, filter_module_info] found_symbols_module = List[Tuple[str, int]] found_symbols_type = Dict[str, found_symbols_module] -# used to hold informatin about a range (VAD or kernel module) +# used to hold information about a range (VAD or kernel module) # (start address, size, file path) range_type = Tuple[int, int, str] ranges_type = List[range_type] @@ -243,7 +243,7 @@ class PESymbols(interfaces.plugins.PluginInterface): _required_framework_version = (2, 7, 0) # 2.0.0 - changed signature of get_kernel_modules, get_all_vads_with_file_paths, addresses_for_process_symbols, get_process_modules - # 3.0.0 - find_symbols wil now throw a ValueError if the provided wanted symbol information does not follow the spec + # 3.0.0 - find_symbols will now throw a ValueError if the provided wanted symbol information does not follow the spec _version = (3, 0, 0) # used for special handling of the kernel PDB file. See later notes @@ -649,7 +649,7 @@ class PESymbols(interfaces.plugins.PluginInterface): and wanted_addresses_identifier not in wanted_symbols ): vollog.warning( - "Invalid `wanted_symbols` sent to `find_symbols`. addresses and names keys both misssing." + "Invalid `wanted_symbols` sent to `find_symbols`. addresses and names keys both missing." ) return @@ -671,7 +671,7 @@ class PESymbols(interfaces.plugins.PluginInterface): for value_index, wanted_value in enumerate(all_wanted): symbol_value = symbol_getter(wanted_value) if symbol_value: - # yield out deleteion key, deletion index, symbol name, symbol address + # yield out deletion key, deletion index, symbol name, symbol address if symbol_key == wanted_names_identifier: yield symbol_key, wanted_value, symbol_value else: @@ -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/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index 157282ce1..43dcd0482 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -436,7 +436,6 @@ class PoolScanner(plugins.PluginInterface): constraints, alignment=alignment, ): - mem_objects = header.get_object( constraint=constraint, use_top_down=is_windows_8_or_later, diff --git a/volatility3/framework/plugins/windows/processghosting.py b/volatility3/framework/plugins/windows/processghosting.py index 7e7f6d3cc..24eb6fa9a 100644 --- a/volatility3/framework/plugins/windows/processghosting.py +++ b/volatility3/framework/plugins/windows/processghosting.py @@ -1,220 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging - -from typing import Optional, Tuple, Generator, Dict - -from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, vadinfo +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import processghosting vollog = logging.getLogger(__name__) -class ProcessGhosting(interfaces.plugins.PluginInterface): - """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose""" +class ProcessGhosting( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=processghosting.ProcessGhosting, + removal_date="2026-06-07", +): + """Lists processes whose DeletePending bit is set or whose FILE_OBJECT is set to 0 or Vads that are DeleteOnClose (deprecated).""" - _version = (1, 0, 0) _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 1) - ), - ] - - @classmethod - def _process_checks( - cls, - proc: interfaces.objects.ObjectInterface, - mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], - ) -> Generator[ - Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None - ]: - """ - Checks the EPROCESS for signs of ghosting - """ - if not proc.has_member("ImageFilePointer"): - return - - delete_pending = None - - # if it is 0 then its a side effect of process ghosting - if proc.ImageFilePointer.vol.offset != 0: - try: - file_object = proc.ImageFilePointer - delete_pending = file_object.DeletePending - file_object = file_object.dereference().vol.offset - except exceptions.InvalidAddressException: - file_object = 0 - - # ImageFilePointer equal to 0 means process ghosting or similar techniques were used - else: - file_object = 0 - - # delete_pending besides 0 or 1 = smear - if isinstance(delete_pending, int) and delete_pending not in [0, 1]: - vollog.debug( - f"Invalid delete_pending value {delete_pending} found for process {proc.UniqueProcessId}" - ) - delete_pending = None - - if file_object == 0 or delete_pending == 1: - yield file_object, delete_pending, None, proc.SectionBaseAddress - - @classmethod - def _vad_checks( - cls, control_area: interfaces.objects.ObjectInterface, vad_path: str - ) -> Generator[Tuple[int, Optional[int], Optional[int]], None, None]: - """ - Checks the control area for delete on close or delete pending being set - """ - try: - file_object = control_area.FilePointer.dereference().cast("_FILE_OBJECT") - except exceptions.InvalidAddressException: - return - - try: - delete_on_close = control_area.u.Flags.DeleteOnClose - except exceptions.InvalidAddressException: - delete_on_close = None - - if delete_on_close and vad_path.lower().endswith((".exe", ".dll")): - yield file_object.vol.offset, None, delete_on_close - - try: - delete_pending = file_object.DeletePending - except exceptions.InvalidAddressException: - delete_pending = None - - if delete_pending == 1: - yield file_object.vol.offset, delete_pending, None - - @classmethod - def check_for_ghosting( - cls, - proc: interfaces.objects.ObjectInterface, - mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]], - ) -> Generator[ - Tuple[int, Optional[int], Optional[int], int, Optional[str]], None, None - ]: - """ - Returns process or vad info for ghosting files - - Args: - proc: - mapped_files: A dictionary mapping vad base addreses to the path and vad instance for the process - - Return: - A Generator of tuples of the file object address, the delete pending state, delete on close state, base address of the VAD, and the path - """ - # check the direct file object of the process - yield from cls._process_checks(proc, mapped_files) - - # walk each vad, check if it is pending delete or has its delete on close bit set - for vad_base, (path, vad) in mapped_files.items(): - # these checks have no meaning for private memory areas - if vad.get_private_memory() == 1: - continue - - try: - if vad.has_member("ControlArea"): - control_area = vad.ControlArea - elif vad.has_member("Subsection"): - control_area = vad.Subsection.ControlArea - # We got here from a short vad, likely smear - else: - continue - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to get control area for vad at base {vad_base:#x} for process with pid {proc.UniqueProcessId}" - ) - continue - - for file_object_address, delete_pending, delete_on_close in cls._vad_checks( - control_area, path - ): - yield format_hints.Hex( - file_object_address - ), delete_pending, delete_on_close, vad_base - - def _generator(self, procs): - kernel = self.context.modules[self.config["kernel"]] - - has_imagefilepointer = kernel.get_type("_EPROCESS").has_member( - "ImageFilePointer" - ) - if not has_imagefilepointer: - vollog.warning( - "ImageFilePointer checks are only supported on Windows 10+ builds when the ImageFilePointer member of _EPROCESS is present" - ) - - for proc in procs: - process_name = utility.array_to_string(proc.ImageFileName) - pid = proc.UniqueProcessId - - # base address -> (file path, VAD instance) - mapped_files: Dict[int, Tuple[str, interfaces.objects.ObjectInterface]] = {} - for vad in vadinfo.VadInfo.list_vads(proc): - path = vad.get_file_name() - if isinstance(path, str): - mapped_files[vad.get_start()] = (path, vad) - - for ( - file_object_address, - delete_pending, - delete_on_close, - base_address, - ) in self.check_for_ghosting(proc, mapped_files): - vad_info = mapped_files.get(base_address) - if vad_info: - path = vad_info[0] - else: - path = renderers.NotAvailableValue() - - yield 0, ( - pid, - process_name, - format_hints.Hex(base_address), - format_hints.Hex(file_object_address), - delete_pending or renderers.NotApplicableValue(), - delete_on_close or renderers.NotApplicableValue(), - path, - ) - - def run(self): - filter_func = pslist.PsList.create_active_process_filter() - - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Base", format_hints.Hex), - ("FILE_OBJECT", format_hints.Hex), - ("DeletePending", int), - ("DeleteOnClose", int), - ("Path", str), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/psxview.py b/volatility3/framework/plugins/windows/psxview.py index 51616a22c..625f02387 100644 --- a/volatility3/framework/plugins/windows/psxview.py +++ b/volatility3/framework/plugins/windows/psxview.py @@ -1,24 +1,24 @@ -import datetime +# 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 -import string -from itertools import chain -from typing import Dict, Iterable, List - -from volatility3.framework import constants, exceptions, renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols.windows import extensions -from volatility3.plugins.windows import handles, pslist, psscan, thrdscan +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import psxview vollog = logging.getLogger(__name__) -class PsXView(plugins.PluginInterface): +class PsXView( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=psxview.PsXView, + removal_date="2026-06-07", +): """Lists all processes found via four of the methods described in \"The Art of Memory Forensics\" which may help \ -identify processes that are trying to hide themselves. + identify processes that are trying to hide themselves. -We recommend using -r pretty if you are looking at this plugin's output in a terminal.""" + We recommend using -r pretty if you are looking at this plugin's output in a terminal. + deprecated.""" # I've omitted the desktop thread scanning method because Volatility3 doesn't appear to have the functionality # which the original plugin used to do it. @@ -30,212 +30,3 @@ We recommend using -r pretty if you are looking at this plugin's output in a ter _required_framework_version = (2, 0, 0) _version = (1, 0, 0) - - valid_proc_name_chars = set( - string.ascii_lowercase + string.ascii_uppercase + "." + " " - ) - - @classmethod - def get_requirements(cls): - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="psscan", component=psscan.PsScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="handles", component=handles.Handles, version=(4, 0, 0) - ), - requirements.BooleanRequirement( - name="physical-offsets", - description="List processes with physical offsets instead of virtual offsets.", - optional=True, - ), - ] - - def _proc_name_to_string(self, proc): - return proc.ImageFileName.cast( - "string", max_length=proc.ImageFileName.vol.count, errors="replace" - ) - - def _is_valid_proc_name(self, string: str) -> bool: - return all(c in self.valid_proc_name_chars for c in string) - - def _filter_garbage_procs( - self, proc_list: Iterable[extensions.EPROCESS] - ) -> List[extensions.EPROCESS]: - return [ - p - for p in proc_list - if p.is_valid() and self._is_valid_proc_name(self._proc_name_to_string(p)) - ] - - def _translate_offset(self, offset: int) -> int: - if not self.config["physical-offsets"]: - return offset - - kernel = self.context.modules[self.config["kernel"]] - layer_name = kernel.layer_name - - try: - _original_offset, _original_length, offset, _length, _layer_name = list( - self.context.layers[layer_name].mapping(offset=offset, length=0) - )[0] - except exceptions.PagedInvalidAddressException: - vollog.debug(f"Page fault: unable to translate {offset:0x}") - - return offset - - def _proc_list_to_dict( - self, tasks: Iterable[extensions.EPROCESS] - ) -> Dict[int, extensions.EPROCESS]: - tasks = self._filter_garbage_procs(tasks) - return {self._translate_offset(proc.vol.offset): proc for proc in tasks} - - def _check_pslist(self, tasks): - return self._proc_list_to_dict(tasks) - - def _check_psscan( - self, - ) -> Dict[int, extensions.EPROCESS]: - res = psscan.PsScan.scan_processes( - context=self.context, kernel_module_name=self.config["kernel"] - ) - - return self._proc_list_to_dict(res) - - def _check_thrdscan(self) -> Dict[int, extensions.EPROCESS]: - ret = [] - - for ethread in thrdscan.ThrdScan.scan_threads( - self.context, module_name="kernel" - ): - process = None - try: - process = ethread.owning_process() - if not process.is_valid(): - continue - - ret.append(process) - except AttributeError: - vollog.log( - constants.LOGLEVEL_VVV, - "Unable to find the owning process of ethread", - ) - - return self._proc_list_to_dict(ret) - - def _check_csrss_handles( - self, tasks: Iterable[extensions.EPROCESS] - ) -> Dict[int, extensions.EPROCESS]: - ret: List[extensions.EPROCESS] = [] - - type_map = handles.Handles.get_type_map( - context=self.context, kernel_module_name=self.config["kernel"] - ) - - cookie = handles.Handles.find_cookie( - context=self.context, kernel_module_name=self.config["kernel"] - ) - - for p in tasks: - name = self._proc_name_to_string(p) - if name != "csrss.exe": - continue - - try: - ret += [ - handle.Body.cast("_EPROCESS") - for handle in handles.Handles.handles( - context=self.context, - kernel_module_name=self.config["kernel"], - handle_table=p.ObjectTable, - ) - if handle.get_object_type(type_map, cookie) == "Process" - ] - except exceptions.InvalidAddressException: - vollog.log( - constants.LOGLEVEL_VVV, "Cannot access eprocess object table" - ) - - return self._proc_list_to_dict(ret) - - def _generator(self): - kdbg_list_processes = list( - pslist.PsList.list_processes( - context=self.context, kernel_module_name=self.config["kernel"] - ) - ) - - # get processes from each source - processes: Dict[str, Dict[int, extensions.EPROCESS]] = {} - - processes["pslist"] = self._check_pslist(kdbg_list_processes) - processes["psscan"] = self._check_psscan() - processes["thrdscan"] = self._check_thrdscan() - processes["csrss"] = self._check_csrss_handles(kdbg_list_processes) - - # Unique set of all offsets from all sources - offsets = set(chain(*(mapping.keys() for mapping in processes.values()))) - - for offset in offsets: - # We know there will be at least one process mapped to each offset - proc: extensions.EPROCESS = next( - mapping[offset] for mapping in processes.values() if offset in mapping - ) - - in_sources = {src: False for src in processes} - - for source, process_mapping in processes.items(): - if offset in process_mapping: - in_sources[source] = True - - pid = proc.UniqueProcessId - name = self._proc_name_to_string(proc) - - exit_time = proc.get_exit_time() - if type(exit_time) is not datetime.datetime: - exit_time = "" - else: - exit_time = str(exit_time) - - yield ( - 0, - ( - format_hints.Hex(offset), - name, - pid, - in_sources["pslist"], - in_sources["psscan"], - in_sources["thrdscan"], - in_sources["csrss"], - exit_time, - ), - ) - - def run(self): - offset_type = "(Physical)" if self.config["physical-offsets"] else "(Virtual)" - offset_str = "Offset" + offset_type - - return renderers.TreeGrid( - [ - (offset_str, format_hints.Hex), - ("Name", str), - ("PID", int), - ("pslist", bool), - ("psscan", bool), - ("thrdscan", bool), - ("csrss", bool), - ("Exit Time", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/registry/amcache.py b/volatility3/framework/plugins/windows/registry/amcache.py index a6ad1848e..ed078993c 100644 --- a/volatility3/framework/plugins/windows/registry/amcache.py +++ b/volatility3/framework/plugins/windows/registry/amcache.py @@ -246,13 +246,29 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, entry in self._generator(): if isinstance(entry.last_modify_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} registry key modified", timeliner.TimeLinerType.MODIFIED, entry.last_modify_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} registry key modified", + timeliner.TimeLinerType.MODIFIED, + entry.last_modify_time, + ) if isinstance(entry.last_modify_time_2, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", timeliner.TimeLinerType.CREATED, entry.last_modify_time_2 + yield ( + f"Amcache: {entry.entry_type} {entry.path} STANDARD_INFORMATION create time", + timeliner.TimeLinerType.CREATED, + entry.last_modify_time_2, + ) if isinstance(entry.install_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} installed", timeliner.TimeLinerType.CREATED, entry.install_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} installed", + timeliner.TimeLinerType.CREATED, + entry.install_time, + ) if isinstance(entry.compile_time, datetime.datetime): - yield f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", timeliner.TimeLinerType.MODIFIED, entry.compile_time + yield ( + f"Amcache: {entry.entry_type} {entry.path} compiled (PE metadata)", + timeliner.TimeLinerType.MODIFIED, + entry.compile_time, + ) @classmethod def get_amcache_hive( @@ -319,20 +335,23 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): vollog.debug(f"Found sha1hash {sha1_hash}") product_name = _get_string_value(values, val_enum.Product.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=company, - last_modify_time=last_mod_time, - last_modify_time_2=last_mod_time_2, - install_time=install_time, - compile_time=compile_time, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=company, + last_modify_time=last_mod_time, + last_modify_time_2=last_mod_time_2, + install_time=install_time, + compile_time=compile_time, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=product_name, ), - product_name=product_name, ) @classmethod @@ -365,15 +384,18 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) version = _get_string_value(values, val_enum.Version.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.Program.name, - company=company, - last_modify_time=conversion.wintime_to_datetime( - program_key.LastWriteTime.QuadPart + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.Program.name, + company=company, + last_modify_time=conversion.wintime_to_datetime( + program_key.LastWriteTime.QuadPart + ), + install_time=install_time, + product_name=product, + product_version=version, ), - install_time=install_time, - product_name=product, - product_version=version, ) @classmethod @@ -411,14 +433,17 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): product: str = name if isinstance(name, str) else "UNKNOWN" # type: ignore - yield program_id.strip().strip("\u0000"), _AmcacheEntry( - AmcacheEntryType.Program.name, - path=path, - last_modify_time=last_mod, - install_time=install_date, - product_name=product, - company=publisher, - product_version=version, + yield ( + program_id.strip().strip("\u0000"), + _AmcacheEntry( + AmcacheEntryType.Program.name, + path=path, + last_modify_time=last_mod, + install_time=install_date, + product_name=product, + company=publisher, + product_version=version, + ), ) @classmethod @@ -456,19 +481,22 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): prod_ver = _get_string_value(values, val_enum.ProductVersion.value) program_id = _get_string_value(values, val_enum.ProgramID.value) - yield program_id, _AmcacheEntry( - AmcacheEntryType.File.name, - path=path, - company=publisher, - last_modify_time=last_mod, - compile_time=linkdate, - sha1_hash=( - sha1_hash.lstrip("0000") - if isinstance(sha1_hash, str) - else sha1_hash + yield ( + program_id, + _AmcacheEntry( + AmcacheEntryType.File.name, + path=path, + company=publisher, + last_modify_time=last_mod, + compile_time=linkdate, + sha1_hash=( + sha1_hash.lstrip("0000") + if isinstance(sha1_hash, str) + else sha1_hash + ), + product_name=prod_name, + product_version=prod_ver, ), - product_name=prod_name, - product_version=prod_ver, ) @classmethod @@ -485,7 +513,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): wanted_values = [key.value for key in val_enum] for binary_key in driver_binary_key.get_subkeys(): - values = { str(value.get_name()): value for value in binary_key.get_values() @@ -636,7 +663,6 @@ class Amcache(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield 0, empty_program def run(self): - return renderers.TreeGrid( [ ("EntryType", str), diff --git a/volatility3/framework/plugins/windows/registry/hashdump.py b/volatility3/framework/plugins/windows/registry/hashdump.py index 256f15d61..19bd60e81 100644 --- a/volatility3/framework/plugins/windows/registry/hashdump.py +++ b/volatility3/framework/plugins/windows/registry/hashdump.py @@ -355,7 +355,7 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_bootkey(cls, syshive: registry_layer.RegistryHive) -> Optional[bytes]: """ - Returns the scrambled bootkey necesary to decrypt hashes + Returns the scrambled bootkey necessary to decrypt hashes """ cs = 1 lsa_base = f"ControlSet{cs:03}" + "\\Control\\Lsa" diff --git a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py index a2789e5df..a3e5fabe2 100644 --- a/volatility3/framework/plugins/windows/registry/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/registry/scheduled_tasks.py @@ -185,7 +185,6 @@ NULL = "\u0000" class _ScheduledTasksReader(io.BytesIO): - def read_task_scheduler_time(self) -> Optional[datetime.datetime]: _ = bool(self.read_aligned_u1()) # is_localized filetime = self.decode_filetime() @@ -393,7 +392,6 @@ class TaskAction: num_attachment_filenames = reader.read_u4() if num_attachment_filenames is not None: - attachment_filenames = [ reader.read_bstring() for _ in range(num_attachment_filenames) ] @@ -602,7 +600,7 @@ def decode_sid(data: bytes) -> Optional[str]: Decodes a windows SID from variable-length raw bytes Returns the string representation of the SID if decoding was successful, or None - if the data could not be parsed due to an insufficent number of bytes. + if the data could not be parsed due to an insufficient number of bytes. """ try: revision, subid_count, id_authority = struct.unpack( @@ -817,7 +815,7 @@ class TaskTrigger: _ = reader.read_u4() # timeout seconds repetition_interval_secs = reader.read_u4() - _ = reader.read_u4() # reptition duration seconds + _ = reader.read_u4() # repetition duration seconds _ = reader.read_u4() # repetition duration seconds 2 _ = reader.read_bool() # stop at duration end @@ -1138,11 +1136,23 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime.datetime]]: for _, task in self._generator(): if isinstance(task.last_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", timeliner.TimeLinerType.ACCESSED, task.last_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran", + timeliner.TimeLinerType.ACCESSED, + task.last_run_time, + ) if isinstance(task.last_successful_run_time, datetime.datetime): - yield f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", timeliner.TimeLinerType.ACCESSED, task.last_successful_run_time + yield ( + f"ScheduledTasks: task action {task.action_description} with trigger {task.trigger_description} ran successfully", + timeliner.TimeLinerType.ACCESSED, + task.last_successful_run_time, + ) if isinstance(task.creation_time, datetime.datetime): - yield f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", timeliner.TimeLinerType.CREATED, task.creation_time + yield ( + f"ScheduledTasks: Creation Time for task {task.guid} with trigger {task.trigger_description or ''}", + timeliner.TimeLinerType.CREATED, + task.creation_time, + ) @classmethod def get_software_hive( @@ -1203,7 +1213,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte def parse_dynamic_info_value( cls, dyn_info_value: reg_extensions.CM_KEY_VALUE ) -> Optional[DynamicInfo]: - try: data = dyn_info_value.decode_data() except exceptions.InvalidAddressException: @@ -1318,7 +1327,6 @@ class ScheduledTasks(interfaces.plugins.PluginInterface, timeliner.TimeLinerInte all_actions = action_set.actions or [None] if action_set is not None else [None] for action, trigger in itertools.product(all_actions, all_triggers): - if action is not None: if action.action_type in ( ActionType.Exe, diff --git a/volatility3/framework/plugins/windows/scheduled_tasks.py b/volatility3/framework/plugins/windows/scheduled_tasks.py index 62d8e3b88..104d062b2 100644 --- a/volatility3/framework/plugins/windows/scheduled_tasks.py +++ b/volatility3/framework/plugins/windows/scheduled_tasks.py @@ -2,7 +2,8 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # import logging -from volatility3.framework import interfaces, deprecation + +from volatility3.framework import deprecation, interfaces from volatility3.plugins.windows.registry import scheduled_tasks vollog = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class ScheduledTasks( interfaces.plugins.PluginInterface, deprecation.PluginRenameClass, replacement_class=scheduled_tasks.ScheduledTasks, - removal_date="2025-09-25", + removal_date="2026-09-25", ): """Decodes scheduled task information from the Windows registry, including information about triggers, actions, run times, and creation times (deprecated).""" diff --git a/volatility3/framework/plugins/windows/sessions.py b/volatility3/framework/plugins/windows/sessions.py index 29d0b2104..158820529 100644 --- a/volatility3/framework/plugins/windows/sessions.py +++ b/volatility3/framework/plugins/windows/sessions.py @@ -95,13 +95,16 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # Group and yield each row for rows in sessions.values(): for row in rows: - yield 0, ( - row.get("session_id"), - row.get("session_type"), - row.get("process_id"), - row.get("process_name"), - row.get("user_name"), - row.get("process_start"), + yield ( + 0, + ( + row.get("session_id"), + row.get("session_type"), + row.get("process_id"), + row.get("process_name"), + row.get("user_name"), + row.get("process_start"), + ), ) def generate_timeline(self): diff --git a/volatility3/framework/plugins/windows/shimcachemem.py b/volatility3/framework/plugins/windows/shimcachemem.py index 5c0af7766..8935757d4 100644 --- a/volatility3/framework/plugins/windows/shimcachemem.py +++ b/volatility3/framework/plugins/windows/shimcachemem.py @@ -51,9 +51,17 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf ) -> Iterator[Tuple[str, timeliner.TimeLinerType, datetime]]: for _, (_, last_modified, last_update, _, _, file_path) in self._generator(): if isinstance(last_update, datetime): - yield f"Shimcache: File {file_path} executed", timeliner.TimeLinerType.ACCESSED, last_update + yield ( + f"Shimcache: File {file_path} executed", + timeliner.TimeLinerType.ACCESSED, + last_update, + ) if isinstance(last_modified, datetime): - yield f"Shimcache: File {file_path} modified", timeliner.TimeLinerType.MODIFIED, last_modified + yield ( + f"Shimcache: File {file_path} modified", + timeliner.TimeLinerType.MODIFIED, + last_modified, + ) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -161,7 +169,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf continue try: - if proc_layer.read(vad.get_start(), 4) != b"\xEF\xBE\xAD\xDE": + if proc_layer.read(vad.get_start(), 4) != b"\xef\xbe\xad\xde": if pid == 624: vollog.debug("VAD magic bytes don't match DEADBEEF") continue @@ -236,7 +244,7 @@ class ShimcacheMem(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterf 2) Iterate over every 4/8 bytes (depending on OS bitness) in the .data section and test for the following: a) offset represents a valid RTL_AVL_TABLE object - b) RTL_AVL_TABLE is preceeded by an ERESOURCE object + b) RTL_AVL_TABLE is preceded by an ERESOURCE object c) RTL_AVL_TABLE is followed by the beginning of the SHIM LRU list :param context: The context to retrieve required elements (layers, symbol tables) from diff --git a/volatility3/framework/plugins/windows/skeleton_key_check.py b/volatility3/framework/plugins/windows/skeleton_key_check.py index 6071a2a39..86c5cf1df 100644 --- a/volatility3/framework/plugins/windows/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/skeleton_key_check.py @@ -1,685 +1,20 @@ -# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - -# This module attempts to locate skeleton-key like function hooks. -# It does this by locating the CSystems array through a variety of methods, -# and then validating the entry for RC4 HMAC (0x17 / 23) -# -# For a thorough walkthrough on how the R&D was performed to develop this plugin, -# please see our blogpost here: -# -# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html - import logging -from typing import Iterable, Tuple, List, Optional - -import pefile - -from volatility3.framework import interfaces, symbols, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.layers import scanners -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.framework.symbols import intermed -from volatility3.framework.symbols.windows import pdbutil -from volatility3.framework.symbols.windows.extensions import pe -from volatility3.plugins.windows import pslist, vadinfo, pe_symbols - -try: - import capstone - - has_capstone = True -except ImportError: - has_capstone = False +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import skeleton_key_check vollog = logging.getLogger(__name__) -class Skeleton_Key_Check(interfaces.plugins.PluginInterface): +class Skeleton_Key_Check( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=skeleton_key_check.Skeleton_Key_Check, + removal_date="2026-06-07", +): """Looks for signs of Skeleton Key malware""" _required_framework_version = (2, 4, 0) - - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) - ), - requirements.VersionRequirement( - name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="bytes_scanner", - component=scanners.BytesScanner, - version=(1, 0, 0), - ), - ] - - def _check_for_skeleton_key_vad( - self, - csystem: interfaces.objects.ObjectInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> bool: - """ - Checks if Initialize and/or Decrypt is hooked by determining if - these function pointers reference addresses inside of the cryptdll VAD - - Args: - csystem: The RC4HMAC KERB_ECRYPT instance - cryptdll_base: Base address of the cryptdll.dll VAD - cryptdll_size: Size of the VAD - Returns: - bool: if a skeleton key hook is present - """ - return not ( - (cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) - and (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size) - ) - - def _check_for_skeleton_key_symbols( - self, - csystem: interfaces.objects.ObjectInterface, - rc4HmacInitialize: int, - rc4HmacDecrypt: int, - ) -> bool: - """ - Uses the PDB information to specifically check if the csystem for RC4HMAC - has an initialization pointer to rc4HmacInitialize and a decryption pointer - to rc4HmacDecrypt. - - Args: - csystem: The RC4HMAC KERB_ECRYPT instance - rc4HmacInitialize: The expected address of csystem Initialization function - rc4HmacDecrypt: The expected address of the csystem Decryption function - - Returns: - bool: if a skeleton key hook was found - """ - return ( - csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt - ) - - def _construct_ecrypt_array( - self, - array_start: int, - count: int, - cryptdll_types: interfaces.context.ModuleInterface, - ) -> interfaces.context.ModuleInterface: - """ - Attempts to construct an array of _KERB_ECRYPT structures - - Args: - array_start: starting virtual address of the array - count: how many elements are in the array - cryptdll_types: the reverse engineered types - - Returns: - The instantiated array - """ - - try: - array = cryptdll_types.object( - object_type="array", - offset=array_start, - subtype=cryptdll_types.get_type("_KERB_ECRYPT"), - count=count, - absolute=True, - ) - - except exceptions.InvalidAddressException: - vollog.debug( - f"Unable to construct cSystems array at given offset: {array_start:x}" - ) - array = None - - return array - - def _find_array_with_pdb_symbols( - self, - cryptdll_symbols: str, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str, - cryptdll_base: int, - ) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: - """ - Finds the CSystems array through use of PDB symbols - - Args: - cryptdll_symbols: The symbols table from the PDB file - cryptdll_types: The types from cryptdll binary analysis - proc_layer_name: The lsass.exe process layer name - cryptdll_base: Base address of cryptdll.dll inside of lsass.exe - - Returns: - Tuple of: - array: The cSystems array - rc4HmacInitialize: The runtime address of the expected initialization function - rc4HmacDecrypt: The runtime address of the expected decryption function - """ - cryptdll_module = self.context.module( - cryptdll_symbols, layer_name=proc_layer_name, offset=cryptdll_base - ) - - rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address( - "rc4HmacInitialize" - ) - - rc4HmacDecrypt = cryptdll_module.get_absolute_symbol_address("rc4HmacDecrypt") - - count_address = cryptdll_module.get_symbol("cCSystems").address - - # we do not want to fail just because the count is not in memory - # 16 was the size on samples I tested, so I chose it as the default - try: - count = cryptdll_types.object( - object_type="unsigned long", offset=count_address - ) - except exceptions.InvalidAddressException: - count = 16 - - array_start = cryptdll_module.get_absolute_symbol_address("CSystems") - - array = self._construct_ecrypt_array(array_start, count, cryptdll_types) - - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping PDB based analysis." - ) - - return array, rc4HmacInitialize, rc4HmacDecrypt - - def _get_cryptdll_types( - self, - context: interfaces.context.ContextInterface, - config, - config_path: str, - proc_layer_name: str, - cryptdll_base: int, - ): - """ - Builds a symbol table from the cryptdll types generated after binary analysis - - Args: - context: the context to operate upon - config: - config_path: - proc_layer_name: name of the lsass.exe process layer - cryptdll_base: base address of cryptdll.dll inside of lsass.exe - """ - kernel = self.context.modules[self.config["kernel"]] - table_mapping = {"nt_symbols": kernel.symbol_table_name} - - cryptdll_symbol_table = intermed.IntermediateSymbolTable.create( - context=context, - config_path=config_path, - sub_path="windows", - filename="kerb_ecrypt", - table_mapping=table_mapping, - ) - - return context.module( - cryptdll_symbol_table, proc_layer_name, offset=cryptdll_base - ) - - def _find_lsass_proc( - self, proc_list: Iterable - ) -> Tuple[interfaces.context.ContextInterface, str]: - """ - Walks the process list and returns the first valid lsass instances. - There should be only one lsass process, but malware will often use the - process name to try and blend in. - - Args: - proc_list: The process list generator - - Return: - The process object for lsass - """ - - for proc in proc_list: - try: - proc_layer_name = proc.add_process_layer() - - return proc, proc_layer_name - - except exceptions.InvalidAddressException as excp: - vollog.debug( - f"Invalid address {excp.invalid_address} in layer {excp.layer_name}" - ) - - return None, None - - def _find_cryptdll( - self, lsass_proc: interfaces.context.ContextInterface - ) -> Tuple[int, int]: - """ - Finds the base address of cryptdll.dll inside of lsass.exe - - Args: - lsass_proc: the process object for lsass.exe - - Returns: - A tuple of: - cryptdll_base: the base address of cryptdll.dll - crytpdll_size: the size of the VAD for cryptdll.dll - """ - for vad in lsass_proc.get_vad_root().traverse(): - filename = vad.get_file_name() - - if isinstance(filename, str) and filename.lower().endswith("cryptdll.dll"): - base = vad.get_start() - return base, vad.get_size() - - return None, None - - def _find_csystems_with_symbols( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> Tuple[interfaces.objects.ObjectInterface, int, int]: - """ - Attempts to find CSystems and the expected address of the handlers. - Relies on downloading and parsing of the cryptdll PDB file. - - Args: - proc_layer_name: the name of the lsass.exe process layer - cryptdll_types: The types from cryptdll binary analysis - cryptdll_base: the base address of cryptdll.dll - crytpdll_size: the size of the VAD for cryptdll.dll - - Returns: - A tuple of: - array: An initialized Volatility array of _KERB_ECRYPT structures - rc4HmacInitialize: The expected address of csystem Initialization function - rc4HmacDecrypt: The expected address of the csystem Decryption function - """ - try: - cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( - self.context, - interfaces.configuration.path_join(self.config_path, "cryptdll"), - proc_layer_name, - "cryptdll.pdb", - cryptdll_base, - cryptdll_size, - ) - except exceptions.VolatilityException: - vollog.debug( - "Unable to use the cryptdll PDB. Stopping PDB symbols based analysis." - ) - return None, None, None - - array, rc4HmacInitialize, rc4HmacDecrypt = self._find_array_with_pdb_symbols( - cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base - ) - - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping PDB symbols based analysis." - ) - - return array, rc4HmacInitialize, rc4HmacDecrypt - - def _get_rip_relative_target(self, inst) -> int: - """ - Returns the target address of a RIP-relative instruction. - - These instructions contain the offset of a target address - relative to the current instruction pointer. - - Args: - inst: A capstone instruction instance - - Returns: - None or the target address of the instruction - """ - try: - opnd = inst.operands[1] - except capstone.CsError: - return None - - if opnd.type != capstone.x86.X86_OP_MEM: - return None - - if inst.reg_name(opnd.mem.base) != "rip": - return None - - return inst.address + inst.size + opnd.mem.disp - - def _analyze_cdlocatecsystem( - self, - function_bytes: bytes, - function_start: int, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str, - ) -> Optional[interfaces.objects.ObjectInterface]: - """ - Performs static analysis on CDLocateCSystem to find the instructions that - reference CSystems as well as cCsystems - - Args: - function_bytes: the instruction bytes of CDLocateCSystem - function_start: the address of CDLocateCSystem - proc_layer_name: the name of the lsass.exe process layer - - Return: - The cSystems array of ecrypt instances - """ - found_count = False - array_start = None - count = None - - ## we only support 64bit disassembly analysis - md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) - md.detail = True - - for inst in md.disasm(function_bytes, function_start): - # we should not reach debug traps - if inst.mnemonic == "int3": - break - - # cCsystems is referenced by a mov instruction - elif inst.mnemonic == "mov": - if not found_count: - target_address = self._get_rip_relative_target(inst) - - # we do not want to fail just because the count is not in memory - # 16 was the size on samples I tested, so I chose it as the default - count = 16 - - if target_address: - try: - count = int.from_bytes( - self.context.layers[proc_layer_name].read( - target_address, 4 - ), - "little", - ) - except exceptions.InvalidAddressException: - vollog.debug( - "Unable to read `cCsystems`. Defaulting to 16." - ) - - found_count = True - - elif inst.mnemonic == "lea": - target_address = self._get_rip_relative_target(inst) - - if target_address: - array_start = target_address - - # we find the count before, so we can terminate the static analysis here - break - - if array_start and count: - array = self._construct_ecrypt_array(array_start, count, cryptdll_types) - else: - array = None - - return array - - def _find_csystems_with_export( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - _, - ) -> Optional[interfaces.objects.ObjectInterface]: - """ - Uses export table analysis to locate CDLocateCsystem - This function references CSystems and cCsystems - - Args: - proc_layer_name: The lsass.exe process layer name - cryptdll_types: The types from cryptdll binary analysis - cryptdll_base: Base address of cryptdll.dll inside of lsass.exe - _: unused in this source - Returns: - The cSystems array - """ - - if not has_capstone: - vollog.debug( - "capstone is not installed so cannot fall back to export table analysis." - ) - return None - - vollog.debug( - "Unable to perform analysis using PDB symbols, falling back to export table analysis." - ) - - pe_table_name = intermed.IntermediateSymbolTable.create( - self.context, self.config_path, "windows", "pe", class_types=pe.class_types - ) - - cryptdll = pe_symbols.PESymbols.get_pefile_obj( - self.context, pe_table_name, proc_layer_name, cryptdll_base - ) - if not cryptdll: - return None - - cryptdll.parse_data_directories( - directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] - ) - if not hasattr(cryptdll, "DIRECTORY_ENTRY_EXPORT"): - return None - - # find the location of CDLocateCSystem and then perform static analysis - for export in cryptdll.DIRECTORY_ENTRY_EXPORT.symbols: - if export.name != b"CDLocateCSystem": - continue - - function_start = cryptdll_base + export.address - - try: - function_bytes = self.context.layers[proc_layer_name].read( - function_start, 0x50 - ) - except exceptions.InvalidAddressException: - vollog.debug( - "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis." - ) - break - - array = self._analyze_cdlocatecsystem( - function_bytes, function_start, cryptdll_types, proc_layer_name - ) - if array is None: - vollog.debug( - "The CSystem array is not present in memory. Stopping export based analysis." - ) - - return array - - return None - - def _find_csystems_with_scanning( - self, - proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int, - ) -> List[interfaces.context.ModuleInterface]: - """ - Performs scanning to find potential RC4 HMAC csystem instances - - This function may return several values as it cannot validate which is the active one - - Args: - proc_layer_name: the lsass.exe process layer name - cryptdll_types: the types from cryptdll binary analysis - cryptdll_base: base address of cryptdll.dll inside of lsass.exe - cryptdll_size: size of the VAD - Returns: - A list of csystem instances - """ - - csystems = [] - - cryptdll_end = cryptdll_base + cryptdll_size - - proc_layer = self.context.layers[proc_layer_name] - - ecrypt_size = cryptdll_types.get_type("_KERB_ECRYPT").size - - # scan for potential instances of RC4 HMAC - # the signature is based on the type being 0x17 - # and the block size member being 1 in all test samples - for address in proc_layer.scan( - self.context, - scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), - sections=[(cryptdll_base, cryptdll_size)], - ): - # this occurs across page boundaries - if not proc_layer.is_valid(address, ecrypt_size): - continue - - kerb = cryptdll_types.object("_KERB_ECRYPT", offset=address, absolute=True) - - # ensure the Encrypt and Finish pointers are inside the VAD - # these are not manipulated in the attack - if (cryptdll_base < kerb.Encrypt < cryptdll_end) and ( - cryptdll_base < kerb.Finish < cryptdll_end - ): - csystems.append(kerb) - - return csystems - - def _generator(self, procs): - """ - Finds instances of the RC4 HMAC CSystem structure - - Returns whether the instances are hooked as well as the function handler addresses - - Args: - procs: the process list filtered to lsass.exe instances - """ - kernel = self.context.modules[self.config["kernel"]] - - if not symbols.symbol_table_is_64bit( - context=self.context, symbol_table_name=kernel.symbol_table_name - ): - vollog.info("This plugin only supports 64bit Windows memory samples") - return None - - lsass_proc, proc_layer_name = self._find_lsass_proc(procs) - if not lsass_proc: - vollog.info( - "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed." - ) - return None - - cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc) - if not cryptdll_base: - vollog.info( - "Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed." - ) - return None - - # the custom type information from binary analysis - cryptdll_types = self._get_cryptdll_types( - self.context, self.config, self.config_path, proc_layer_name, cryptdll_base - ) - - # attempt to find the array and symbols directly from the PDB - csystems, rc4HmacInitialize, rc4HmacDecrypt = self._find_csystems_with_symbols( - proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size - ) - - # if we can't find cSystems through the PDB then - # we fall back to export analysis and scanning - # we keep the address of the rc4 functions from the PDB - # though as its our only source to get them - if csystems is None: - fallback_sources = [ - self._find_csystems_with_export, - self._find_csystems_with_scanning, - ] - - for source in fallback_sources: - csystems = source( - proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size - ) - - if csystems is not None: - break - - if csystems is None: - vollog.info( - "Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed." - ) - return None - - for csystem in csystems: - if not self.context.layers[proc_layer_name].is_valid( - csystem.vol.offset, csystem.vol.size - ): - continue - - # filter for RC4 HMAC - if csystem.EncryptionType != 0x17: - continue - - # use the specific symbols if present, otherwise use the vad start and size - if rc4HmacInitialize and rc4HmacDecrypt: - skeleton_key_present = self._check_for_skeleton_key_symbols( - csystem, rc4HmacInitialize, rc4HmacDecrypt - ) - else: - skeleton_key_present = self._check_for_skeleton_key_vad( - csystem, cryptdll_base, cryptdll_size - ) - - yield 0, ( - lsass_proc.UniqueProcessId, - "lsass.exe", - skeleton_key_present, - format_hints.Hex(csystem.Initialize), - format_hints.Hex(csystem.Decrypt), - ) - - def _lsass_proc_filter(self, proc): - """ - Used to filter to only lsass.exe processes - - There should only be one of these, but malware can/does make lsass.exe - named processes to blend in or uses lsass.exe as a process hollowing target - """ - process_name = utility.array_to_string(proc.ImageFileName) - - return process_name != "lsass.exe" - - def run(self): - return renderers.TreeGrid( - [ - ("PID", int), - ("Process", str), - ("Skeleton Key Found", bool), - ("rc4HmacInitialize", format_hints.Hex), - ("rc4HmacDecrypt", format_hints.Hex), - ], - self._generator( - pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=self._lsass_proc_filter, - ) - ), - ) + _version = (1, 0, 0) diff --git a/volatility3/framework/plugins/windows/suspicious_threads.py b/volatility3/framework/plugins/windows/suspicious_threads.py index eabc637c8..068bdccaf 100644 --- a/volatility3/framework/plugins/windows/suspicious_threads.py +++ b/volatility3/framework/plugins/windows/suspicious_threads.py @@ -1,221 +1,20 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # - import logging -from typing import List, Dict, Tuple, Generator -from volatility3.framework import renderers, interfaces -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.framework.renderers import format_hints -from volatility3.plugins.windows import pslist, threads, vadinfo, thrdscan +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import suspicious_threads vollog = logging.getLogger(__name__) -class SuspiciousThreads(interfaces.plugins.PluginInterface): - """Lists suspicious userland process threads""" +class SuspiciousThreads( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=suspicious_threads.SuspiciousThreads, + removal_date="2026-06-07", +): + """Lists suspicious userland process threads (deprecated).""" _required_framework_version = (2, 4, 0) _version = (2, 0, 1) - - @classmethod - def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.ListRequirement( - name="pid", - description="Filter on specific process IDs", - element_type=int, - optional=True, - ), - requirements.VersionRequirement( - name="thrdscan", component=thrdscan.ThrdScan, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="threads", component=threads.Threads, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) - ), - ] - - def _get_ranges( - self, - kernel: interfaces.context.ModuleInterface, - all_ranges: Dict[int, List[Tuple[int, int, str, str]]], - proc, - ) -> Tuple[int, int, str, str]: - """ - Maintains a hash table so each process' VADs - are only enumerated once per plugin run - """ - key = proc.vol.offset - - if key not in all_ranges: - all_ranges[key] = [] - - for vad in proc.get_vad_root().traverse(): - fn = vad.get_file_name() - if not isinstance(fn, str) or not fn: - fn = None - - protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values( - self.context, kernel.layer_name, kernel.symbol_table_name - ), - vadinfo.winnt_protections, - ) - - all_ranges[key].append( - (vad.get_start(), vad.get_end(), protection_string, fn) - ) - - return all_ranges[key] - - def _get_range( - self, ranges: Dict[int, List[Tuple[int, int, str, str]]], address: int - ) -> Tuple[int, str, str]: - """ - Walks a process' VADs looking for the one - containing `address` - - Returns its base address, protection string, and mapped file, if any - """ - for start, end, protection_string, fn in ranges: - if start <= address < end: - return start, protection_string, fn - - return None, None, None - - def _check_thread_address( - self, exe_path: str, ranges, thread_address: int - ) -> Generator[Tuple[str, str], None, None]: - vad_base, prot, vad_path = self._get_range(ranges, thread_address) - - # threads outside of a VAD means either smear from this thread or this process' VAD tree - if vad_base is None: - return - - if vad_path is None: - # set this so checks after report the non file backed region in the path column - vad_path = "" - - yield ( - vad_path, - f"This thread started execution in the VAD starting at base address ({vad_base:#x}), which is not backed by a file", - ) - - # All threads should point to PAGE_EXECUTE_WRITECOPY mapped regions - if prot != "PAGE_EXECUTE_WRITECOPY": - yield ( - vad_path, - f"VAD at base address ({vad_base:#x}) hosting this thread has an unexpected starting protection {prot}", - ) - - # check for process hollowing type techniques that mapped in a second, malicious exe file - if ( - exe_path - and vad_path.lower().endswith(".exe") - and (vad_path.lower() != exe_path.lower()) - ): - yield ( - vad_path, - "VAD at base address ({vad_base:#x}) hosting this thread maps an application executable that is not the process exectuable", - ) - - def _enumerate_processes( - self, kernel: interfaces.context.ModuleInterface, all_ranges - ): - filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - - for proc in pslist.PsList.list_processes( - context=self.context, - kernel_module_name=self.config["kernel"], - filter_func=filter_func, - ): - ranges = self._get_ranges(kernel, all_ranges, proc) - - # smeared vads or process is terminating - if len(all_ranges[proc.vol.offset]) < 5: - continue - - pid = proc.UniqueProcessId - proc_name = utility.array_to_string(proc.ImageFileName) - - _, __, exe_path = self._get_range(ranges, proc.SectionBaseAddress) - if not isinstance(exe_path, str): - exe_path = None - - yield proc, pid, proc_name, exe_path, ranges - - def _generator(self): - kernel = self.context.modules[self.config["kernel"]] - - all_ranges = {} - - for proc, pid, proc_name, exe_path, ranges in self._enumerate_processes( - kernel, all_ranges - ): - # processes often create multiple threads at the same address - # there is no benefit to checking the same address more than once per process - checked = set() - - for thread in threads.Threads.list_threads( - self.context, self.config["kernel"], proc - ): - # do not process if a thread is exited or terminated (4 = Terminated) - if thread.ExitTime.QuadPart > 0 or thread.Tcb.State == 4: - continue - - # bail if accessing the threads members causes a page fault - info = thrdscan.ThrdScan.gather_thread_info(thread) - if not info: - continue - - _, _, tid, start_address, _, win32_start_address, _, _, _ = info - - addresses = [ - (start_address, "Start"), - (win32_start_address, "Win32Start"), - ] - - for address, context in addresses: - if address in checked: - continue - checked.add(address) - - for vad_path, note in self._check_thread_address( - exe_path, ranges, address - ): - yield 0, ( - proc_name, - pid, - tid, - context, - format_hints.Hex(address), - vad_path, - note, - ) - - def run(self): - return renderers.TreeGrid( - [ - ("Process", str), - ("PID", int), - ("TID", int), - ("Context", str), - ("Address", format_hints.Hex), - ("VAD Path", str), - ("Note", str), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/svcdiff.py b/volatility3/framework/plugins/windows/svcdiff.py index 78b61eb67..24bc53e49 100644 --- a/volatility3/framework/plugins/windows/svcdiff.py +++ b/volatility3/framework/plugins/windows/svcdiff.py @@ -1,102 +1,26 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -# This module compares services found through list walking versus scanning, -# with the aim of finding hidden services. -# -# For background of hidden services and a real-world example of the use of this plugin, -# please see our blogpost: -# -# https://volatilityfoundation.org/memory-forensics-rd-illustrated-detecting-hidden-windows-services/ - import logging - -from volatility3.framework import symbols, interfaces -from volatility3.framework.configuration import requirements -from volatility3.plugins.windows import svclist, svcscan -from volatility3.framework.symbols.windows import versions +from volatility3.framework import deprecation +from volatility3.plugins.windows.malware import svcdiff +from volatility3.plugins.windows import svcscan vollog = logging.getLogger(__name__) -class SvcDiff(svcscan.SvcScan): - """Compares services found through list walking versus scanning to find rootkits""" - - _required_framework_version = (2, 4, 0) - - _version = (2, 0, 0) +class SvcDiff( + svcscan.SvcScan, + deprecation.PluginRenameClass, + replacement_class=svcdiff.SvcDiff, + removal_date="2026-06-07", +): + """Compares services found through list walking versus scanning to find rootkits (deprecated).""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._enumeration_method = self.service_diff - @classmethod - def get_requirements(cls): - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="svclist", component=svclist.SvcList, version=(2, 0, 0) - ), - requirements.VersionRequirement( - name="svcscan", component=svcscan.SvcScan, version=(4, 0, 0) - ), - ] + _required_framework_version = (2, 4, 0) - @classmethod - def service_diff( - cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - service_table_name: str, - service_binary_dll_map, - filter_func, - ): - """ - On Windows 10 version 15063+ 64bit Windows memory samples, walk the services list - and scan for services then report differences - """ - kernel = context.modules[kernel_module_name] - - if not symbols.symbol_table_is_64bit( - context=context, symbol_table_name=kernel.symbol_table_name - ) or not versions.is_win10_15063_or_later( - context=context, symbol_table=kernel.symbol_table_name - ): - vollog.warning( - "This plugin only supports Windows 10 version 15063+ 64bit Windows memory samples" - ) - return - - from_scan = set() - from_list = set() - records = {} - - # collect unique service names from scanning - for service in svcscan.SvcScan.service_scan( - context, - kernel_module_name, - service_table_name, - service_binary_dll_map, - filter_func, - ): - from_scan.add(service[6]) - records[service[6]] = service - - # collect services from listing walking - for service in svclist.SvcList.service_list( - context, - kernel_module_name, - service_table_name, - service_binary_dll_map, - filter_func, - ): - from_list.add(service[6]) - - # report services found from scanning but not list walking - for hidden_service in from_scan - from_list: - yield records[hidden_service] + _version = (2, 0, 0) diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index 5f0e4761e..1bfb98fda 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -150,7 +150,6 @@ class SvcScan(interfaces.plugins.PluginInterface): def _get_service_key( context, config_path: str, kernel_module_name: str ) -> Optional[objects.StructType]: - for hive in hivelist.HiveList.list_hives( context=context, base_config_path=interfaces.configuration.path_join( diff --git a/volatility3/framework/plugins/windows/thrdscan.py b/volatility3/framework/plugins/windows/thrdscan.py index d5a1a0b07..1588f292e 100644 --- a/volatility3/framework/plugins/windows/thrdscan.py +++ b/volatility3/framework/plugins/windows/thrdscan.py @@ -167,16 +167,19 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) info = self.gather_thread_info(ethread, vads_cache) if info: - yield 0, ( - format_hints.Hex(info.offset), - info.pid, - info.tid, - format_hints.Hex(info.start_addr), - info.start_path or renderers.NotAvailableValue(), - format_hints.Hex(info.win32_start_addr), - info.win32_start_path or renderers.NotAvailableValue(), - info.create_time, - info.exit_time, + yield ( + 0, + ( + format_hints.Hex(info.offset), + info.pid, + info.tid, + format_hints.Hex(info.start_addr), + info.start_path or renderers.NotAvailableValue(), + format_hints.Hex(info.win32_start_addr), + info.win32_start_path or renderers.NotAvailableValue(), + info.create_time, + info.exit_time, + ), ) def generate_timeline(self): @@ -190,6 +193,9 @@ class ThrdScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) row_dict["PID"], row_dict["TID"], row_dict["StartAddress"], + row_dict["StartPath"], + row_dict["Win32StartAddress"], + row_dict["Win32StartPath"], row_dict["CreateTime"], row_dict["ExitTime"], ) = row_data diff --git a/volatility3/framework/plugins/windows/timers.py b/volatility3/framework/plugins/windows/timers.py index f530a4c7b..c7364d5a5 100644 --- a/volatility3/framework/plugins/windows/timers.py +++ b/volatility3/framework/plugins/windows/timers.py @@ -35,11 +35,11 @@ class Timers(interfaces.plugins.PluginInterface): description="Windows kernel", architectures=["Intel32", "Intel64"], ), - requirements.PluginRequirement( - name="ssdt", plugin=ssdt.SSDT, version=(2, 0, 0) + requirements.VersionRequirement( + name="ssdt", component=ssdt.SSDT, version=(2, 0, 0) ), - requirements.PluginRequirement( - name="kpcrs", plugin=kpcrs.KPCRs, version=(2, 0, 0) + requirements.VersionRequirement( + name="kpcrs", component=kpcrs.KPCRs, version=(2, 0, 0) ), ] diff --git a/volatility3/framework/plugins/windows/unhooked_system_calls.py b/volatility3/framework/plugins/windows/unhooked_system_calls.py index 3ff0aa158..3827bbe6e 100644 --- a/volatility3/framework/plugins/windows/unhooked_system_calls.py +++ b/volatility3/framework/plugins/windows/unhooked_system_calls.py @@ -1,202 +1,22 @@ -# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0 +# This file is Copyright 2025 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 - -# Full details on the techniques used in these plugins to detect EDR-evading malware -# can be found in our 20 page whitepaper submitted to DEFCON along with the presentation -# https://www.volexity.com/wp-content/uploads/2024/08/Defcon24_EDR_Evasion_Detection_White-Paper_Andrew-Case.pdf - +# import logging - -from typing import Dict, Tuple, List, Generator - -from volatility3.framework import interfaces, exceptions -from volatility3.framework import renderers -from volatility3.framework.configuration import requirements -from volatility3.framework.objects import utility -from volatility3.plugins.windows import pslist, pe_symbols +from volatility3.framework import interfaces, deprecation +from volatility3.plugins.windows.malware import ( + unhooked_system_calls as unhooked_syscalls, +) vollog = logging.getLogger(__name__) -class unhooked_system_calls(interfaces.plugins.PluginInterface): - """Looks for signs of Skeleton Key malware""" +class unhooked_system_calls( + interfaces.plugins.PluginInterface, + deprecation.PluginRenameClass, + replacement_class=unhooked_syscalls.UnhookedSystemCalls, + removal_date="2026-06-07", +): + """Detects hooked ntdll.dll stub functions in Windows processes (deprecated).""" _required_framework_version = (2, 4, 0) _version = (2, 0, 0) - - system_calls = { - "ntdll.dll": { - pe_symbols.wanted_names_identifier: [ - "NtCreateThread", - "NtProtectVirtualMemory", - "NtReadVirtualMemory", - "NtOpenProcess", - "NtWriteFile", - "NtQueryVirtualMemory", - "NtAllocateVirtualMemory", - "NtWorkerFactoryWorkerReady", - "NtAcceptConnectPort", - "NtAddDriverEntry", - "NtAdjustPrivilegesToken", - "NtAlpcCreatePort", - "NtClose", - "NtCreateFile", - "NtCreateMutant", - "NtOpenFile", - "NtOpenIoCompletion", - "NtOpenJobObject", - "NtOpenKey", - "NtOpenKeyEx", - "NtOpenThread", - "NtOpenThreadToken", - "NtOpenThreadTokenEx", - "NtWriteVirtualMemory", - "NtTraceEvent", - "NtTranslateFilePath", - "NtUmsThreadYield", - "NtUnloadDriver", - "NtUnloadKey", - "NtUnloadKey2", - "NtUnloadKeyEx", - "NtCreateKey", - "NtCreateSection", - "NtDeleteKey", - "NtDeleteValueKey", - "NtDuplicateObject", - "NtQueryValueKey", - "NtReplaceKey", - "NtRequestWaitReplyPort", - "NtRestoreKey", - "NtSetContextThread", - "NtSetSecurityObject", - "NtSetValueKey", - "NtSystemDebugControl", - "NtTerminateProcess", - ] - } - } - - # This data structure is used to track unique implementations of functions across processes - # The outer dictionary holds the module name (e.g., ntdll.dll) - # The next dictionary holds the function names (NtTerminateProcess, NtSetValueKey, etc.) inside a module - # The innermost dictionary holds the unique implementation (bytes) of a function across processes - # Each implementation is tracked along with the process(es) that host it - # For systems without malware, all functions should have the same implementation - # When API hooking/module unhooking is done, the victim (infected) processes will have unique implementations - _code_bytes_type = Dict[str, Dict[str, Dict[bytes, List[Tuple[int, str]]]]] - - @classmethod - def get_requirements(cls) -> List: - # Since we're calling the plugin, make sure we have the plugin's requirements - return [ - requirements.ModuleRequirement( - name="kernel", - description="Windows kernel", - architectures=["Intel32", "Intel64"], - ), - requirements.VersionRequirement( - name="pslist", component=pslist.PsList, version=(3, 0, 0) - ), - requirements.VersionRequirement( - name="pe_symbols", component=pe_symbols.PESymbols, version=(3, 0, 0) - ), - ] - - def _gather_code_bytes( - self, - kernel_module_name: str, - found_symbols: pe_symbols.found_symbols_type, - ) -> _code_bytes_type: - """ - Enumerates the desired DLLs and function implementations in each process - Groups based on unique implementations of each DLLs' functions - The purpose is to detect when a function has different implementations (code) - in different processes. - This very effectively detects code injection. - """ - code_bytes: unhooked_system_calls._code_bytes_type = {} - - procs = pslist.PsList.list_processes(self.context, kernel_module_name) - - for proc in procs: - try: - proc_id = proc.UniqueProcessId - proc_name = utility.array_to_string(proc.ImageFileName) - proc_layer_name = proc.add_process_layer() - except exceptions.InvalidAddressException: - continue - - for dll_name, functions in found_symbols.items(): - for func_name, func_addr in functions: - try: - fbytes = self.context.layers[proc_layer_name].read( - func_addr, 0x20 - ) - except exceptions.InvalidAddressException: - continue - - # see the definition of _code_bytes_type for details of this data structure - if dll_name not in code_bytes: - code_bytes[dll_name] = {} - - if func_name not in code_bytes[dll_name]: - code_bytes[dll_name][func_name] = {} - - if fbytes not in code_bytes[dll_name][func_name]: - code_bytes[dll_name][func_name][fbytes] = [] - - code_bytes[dll_name][func_name][fbytes].append((proc_id, proc_name)) - - return code_bytes - - def _generator(self) -> Generator[Tuple[int, Tuple[str, str, int]], None, None]: - found_symbols = pe_symbols.PESymbols.addresses_for_process_symbols( - context=self.context, - config_path=self.config_path, - kernel_module_name=self.config["kernel"], - symbols=unhooked_system_calls.system_calls, - ) - - # code_bytes[dll_name][func_name][func_bytes] - code_bytes = self._gather_code_bytes(self.config["kernel"], found_symbols) - - # walk the functions that were evaluated - for functions in code_bytes.values(): - # cbb is the distinct groups of bytes (instructions) - # for this function across processes - for func_name, cbb in functions.items(): - # the dict key here is the raw instructions, which is not helpful to look at - # the values are the list of tuples for the (proc_id, proc_name) pairs for this set of bytes (instructions) - cb = list(cbb.values()) - - # if all processes map to the same implementation, then no malware is present - if len(cb) == 1: - yield 0, (func_name, "", len(cb[0])) - else: - # if there are differing implementations then it means - # that malware has overwritten system call(s) in infected processes - # max_idx and small_idx find which implementation of a system call has the least processes - # as all observed malware and open source projects only infected a few targets, leaving the - # rest with the original EDR hooks in place - max_idx = 0 if len(cb[0]) > len(cb[1]) else 1 - small_idx = (~max_idx) & 1 - - ps = [] - - # gather processes on small_idx since these are the malware infected ones - for pid, pname in cb[small_idx]: - ps.append(f"{pid:d}:{pname}") - - proc_names = ", ".join(ps) - - yield 0, (func_name, proc_names, len(cb[max_idx])) - - def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid( - [ - ("Function", str), - ("Distinct Implementations", str), - ("Total Implementations", int), - ], - self._generator(), - ) diff --git a/volatility3/framework/plugins/windows/vadregexscan.py b/volatility3/framework/plugins/windows/vadregexscan.py index 5ead4e453..6a3cc394f 100644 --- a/volatility3/framework/plugins/windows/vadregexscan.py +++ b/volatility3/framework/plugins/windows/vadregexscan.py @@ -62,7 +62,6 @@ class VadRegExScan(plugins.PluginInterface): vollog.debug(f"RegEx Pattern: {regex_pattern}") for proc in procs: - # attempt to create a process layer for each proc proc_layer_name = proc.add_process_layer() if not proc_layer_name: @@ -86,7 +85,7 @@ class VadRegExScan(plugins.PluginInterface): ): result_data = proc_layer.read(offset, self.MAXSIZE_DEFAULT, pad=True) - # reapply the regex in order to extact just the match + # reapply the regex in order to extract just the match regex_result = re.match(regex_pattern, result_data) if regex_result: @@ -106,12 +105,15 @@ class VadRegExScan(plugins.PluginInterface): max_length=proc.ImageFileName.vol.count, errors="replace", ) - yield 0, ( - proc_id, - process_name, - format_hints.Hex(offset), - text_result, - bytes_result, + yield ( + 0, + ( + proc_id, + process_name, + format_hints.Hex(offset), + text_result, + bytes_result, + ), ) def run(self): diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 04b9aadd9..9a38213fa 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -4,6 +4,7 @@ import logging from typing import Iterable, List, Tuple +import datetime from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements @@ -18,7 +19,7 @@ class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" _required_framework_version = (2, 22, 0) - _version = (1, 1, 3) + _version = (1, 1, 4) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: @@ -99,12 +100,24 @@ class VadYaraScan(interfaces.plugins.PluginInterface): layer_name=layer.name, length=len(value), ) - yield 0, ( - format_hints.Hex(offset), - task.UniqueProcessId, - rule_name, - name, - layer_data, + yield ( + 0, + ( + format_hints.Hex(offset), + task.UniqueProcessId, + task.get_create_time(), + task.InheritedFromUniqueProcessId, + task.ImageFileName.cast( + "string", + max_length=task.ImageFileName.vol.count, + errors="replace", + ), + task.get_session_id(), + task.ActiveThreads, + rule_name, + name, + layer_data, + ), ) @classmethod @@ -130,6 +143,11 @@ class VadYaraScan(interfaces.plugins.PluginInterface): [ ("Offset", format_hints.Hex), ("PID", int), + ("CreateTime", datetime.datetime), + ("PPID", int), + ("ImageFileName", str), + ("SessionId", int), + ("Threads", int), ("Rule", str), ("Component", str), ("Value", renderers.LayerData), diff --git a/volatility3/framework/plugins/windows/windows.py b/volatility3/framework/plugins/windows/windows.py index 9d4317df9..c98869665 100644 --- a/volatility3/framework/plugins/windows/windows.py +++ b/volatility3/framework/plugins/windows/windows.py @@ -112,15 +112,18 @@ class Windows(interfaces.plugins.PluginInterface): ) continue - yield 0, ( - format_hints.Hex(window.vol.offset), - station_name, - sess_id, - desktop_name, - window_name or renderers.NotAvailableValue(), - window_proc, - process_name, - process_pid, + yield ( + 0, + ( + format_hints.Hex(window.vol.offset), + station_name, + sess_id, + desktop_name, + window_name or renderers.NotAvailableValue(), + window_proc, + process_name, + process_pid, + ), ) def run(self): diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 040a50c1a..910ded109 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -31,7 +31,7 @@ except ImportError: except ImportError: vollog.info( - "Neither yara-x nor yara-python (>3.8.0) module not found, plugin (and dependent plugins) not available" + "Neither yara-x nor yara-python (>3.8.0) module was found, plugin (and dependent plugins) not available" ) raise diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 4f1de586a..899c19196 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -6,6 +6,7 @@ Renderers display the unified output format in some manner (be it text or file or graphical output """ + import collections import collections.abc import dataclasses @@ -93,7 +94,7 @@ class Disassembly(interfaces.renderers.BasicType): class LayerData(interfaces.renderers.BasicType): """Layer data - This requires the contex to be passed in, in case plugins want to use multiple contexts + This requires the context to be passed in, in case plugins want to use multiple contexts and to ensure the TreeGrid interface doesn't change, since this would break all existing plugins """ diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index d57c7e9f1..83f36a1a0 100644 --- a/volatility3/framework/renderers/format_hints.py +++ b/volatility3/framework/renderers/format_hints.py @@ -8,6 +8,7 @@ These hints allow a plugin to indicate how they would like data from a particula Text renderers should attempt to honour all hints provided in this module where possible """ + from typing import Type, Union from volatility3.framework import interfaces diff --git a/volatility3/framework/symbols/__init__.py b/volatility3/framework/symbols/__init__.py index 87f2288d7..a050298ba 100644 --- a/volatility3/framework/symbols/__init__.py +++ b/volatility3/framework/symbols/__init__.py @@ -210,9 +210,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): replacements = set() # Whole Symbols that still need traversing while traverse_list: - template_traverse_list, traverse_list = [ - self._resolved[traverse_list[0]] - ], traverse_list[1:] + template_traverse_list, traverse_list = ( + [self._resolved[traverse_list[0]]], + traverse_list[1:], + ) # Traverse a single symbol looking for any ReferenceTemplate objects while template_traverse_list: traverser, template_traverse_list = ( diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index 33035bc70..001f817bd 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -246,9 +246,12 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): if name.endswith(zip_match + extension) or ( zip_match == "*" and name.endswith(extension) ): - yield "jar:file:" + str( - pathlib.Path(zip_path) - ) + "!" + name + yield ( + "jar:file:" + + str(pathlib.Path(zip_path)) + + "!" + + name + ) @classmethod def create( diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index 327c6dd37..19fb8f1d4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -99,8 +99,10 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 3, 1) + _version = (2, 4, 0) _required_framework_version = (2, 0, 0) + deleted = "(deleted)" + smear = "" framework.require_interface_version(*_required_framework_version) @@ -168,6 +170,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # vfsmnt can be the vfsmount object itself (>=3.3) or a vfsmount * (<3.3) return "" + inode = dentry.d_inode path_reversed = [] smeared = False while ( @@ -191,6 +194,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): parent = dentry.d_parent dname = dentry.d_name.name_as_str() + # empty dentry names are most likely # the result of smearing if not dname: @@ -203,7 +207,10 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # if there is smear the missing dname will be empty. e.g. if the normal # path would be /foo/bar/baz, but bar is missing due to smear the results # returned here will show /foo//baz. Note the // for the missing dname. - return f" {path}" + return f"{LinuxUtilities.smear} {path}" + + if inode and inode.is_readable() and inode.is_valid() and inode.i_nlink == 0: + path = f" {path} {LinuxUtilities.deleted}" return path @classmethod @@ -301,7 +308,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return f"{pre_name}:[{inode.i_ino:d}]" @classmethod - def path_for_file(cls, context, task, filp) -> str: + def path_for_file(cls, context, task, filp, files_only=False) -> str: """Returns a file (or sock pipe) pathname relative to the task's root directory. A 'file' structure doesn't have enough information to properly restore its @@ -340,7 +347,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): except exceptions.InvalidAddressException: dname_is_valid = False - if dname_is_valid: + if dname_is_valid and not files_only: ret = LinuxUtilities._get_new_sock_pipe_path(context, task, filp) else: ret = LinuxUtilities._get_path_file(task, filp) @@ -353,6 +360,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): context: interfaces.context.ContextInterface, symbol_table: str, task: interfaces.objects.ObjectInterface, + files_only: bool = False, ): try: files = task.files @@ -376,7 +384,9 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): for fd_num, filp in enumerate(fds): if filp and filp.is_readable(): - full_path = LinuxUtilities.path_for_file(context, task, filp) + full_path = LinuxUtilities.path_for_file( + context, task, filp, files_only + ) yield fd_num, filp, full_path @@ -515,7 +525,7 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): vmlinux: interfaces.context.ModuleInterface, ) -> Optional[interfaces.objects.ObjectInterface]: """Cast a member of a structure out to the containing structure. - It mimicks the Linux kernel macro container_of() see include/linux.kernel.h + It mimics the Linux kernel macro container_of() see include/linux.kernel.h Args: addr: The pointer to the member. diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index a97cf2a5a..b0afe1f3d 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -36,7 +36,6 @@ vollog = logging.getLogger(__name__) class module(generic.GenericIntelProcess): - def is_valid(self): """Determine whether it is a valid module object by verifying the self-referential in module_kobject. This also confirms that the module is actively allocated and @@ -984,14 +983,13 @@ class maple_tree(objects.StructType): current_depth + 1, ) else: - # unkown maple node type + # unknown maple node type raise AttributeError( - f"Unkown Maple Tree node type {node_type} at offset {hex(pointer)}." + f"Unknown Maple Tree node type {node_type} at offset {hex(pointer)}." ) class mm_struct(objects.StructType): - # TODO: As of version 3.0.0 this method should be removed def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """ @@ -1303,6 +1301,42 @@ class vm_area_struct(objects.StructType): except exceptions.InvalidAddressException: return None + def get_malicious_pages(self, proclayer) -> List[int]: + """Identifies and returns a list of potentially malicious memory pages. + + A page is considered malicious if it is: + - Executable (protection flags match 'r-x') + - Dirty (modified since process start, according to proclayer.is_dirty()) + + Args: + proclayer: The process's memory layer + + Returns: + List[int]: A list of virtual addresses for pages flagged as potentially malicious. + """ + + malicious_pages = [] + flags_str = self.get_protection() + + if ( + proclayer + and "r-x" in flags_str + and self.vm_file.dereference().vol.offset != 0 + ): + for i in range(self.vm_start, self.vm_end, proclayer.page_size): + try: + if proclayer.is_dirty(i): + vollog.debug(f"Found malicious (dirty+exec) page at {hex(i)} !") + malicious_pages.append(i) + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug(f"Unable to translate address {hex(i)} : {excp}") + # Abort as it is likely that other addresses in the same range will also fail + break + return malicious_pages + # used by malfind def is_suspicious(self, proclayer=None): ret = False @@ -1318,7 +1352,7 @@ class vm_area_struct(objects.StructType): try: if proclayer.is_dirty(i): vollog.warning( - f"Found malicious (dirty+exec) page at {hex(i)} !" + f"Found malicious page(s) inside (dirty+exec) region {hex(self.vm_start)} !" ) # We do not attempt to find other dirty+exec pages once we have found one ret = True @@ -2001,8 +2035,9 @@ class mnt_namespace(objects.StructType): self._context, self ) for node in self.mounts.get_nodes(): + # See kernel's node_to_mount() mnt = linux.LinuxUtilities.container_of( - node, "mount", "mnt_list", vmlinux + node, "mount", "mnt_node", vmlinux ) yield mnt else: @@ -2325,7 +2360,7 @@ class kernel_cap_t(kernel_cap_struct): class Timespec64Abstract(abc.ABC): - """Abstract class to handle all required timespec64 operations, convertions and + """Abstract class to handle all required timespec64 operations, conversions and adjustments.""" @classmethod @@ -2421,7 +2456,7 @@ class Timespec64Abstract(abc.ABC): class Timespec64Concrete(Timespec64Abstract): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This is used to dynamically create timespec64-like objects, each with its own variables and the same methods as a timespec64 object extension. """ @@ -2432,7 +2467,7 @@ class Timespec64Concrete(Timespec64Abstract): class timespec64(Timespec64Abstract, objects.StructType): - """Handle all required timespec64 operations, convertions and adjustments. + """Handle all required timespec64 operations, conversions and adjustments. This works as an extension of the timespec64 object while maintaining the same methods as a Timespec64Concrete object. """ @@ -2800,7 +2835,7 @@ class IDR(objects.StructType): vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) if not vmlinux.get_type("idr_layer").has_member("layer"): vollog.info( - "Unsupported IDR implementation, it should be a very very old kernel, probabably < 2.6" + "Unsupported IDR implementation, it should be a very very old kernel, probably < 2.6" ) return None @@ -3078,7 +3113,6 @@ class latch_tree_root(objects.StructType): class kernel_symbol(objects.StructType): - def _offset_to_ptr(self, off) -> int: layer = self._context.layers[self.vol.layer_name] long_mask = (1 << layer.bits_per_register) - 1 diff --git a/volatility3/framework/symbols/linux/extensions/network.py b/volatility3/framework/symbols/linux/extensions/network.py index 30094d1b1..37fa5a41f 100644 --- a/volatility3/framework/symbols/linux/extensions/network.py +++ b/volatility3/framework/symbols/linux/extensions/network.py @@ -65,7 +65,7 @@ class net_device(objects.StructType): hwaddr = parent_layer.read(self.dev_addr, self.addr_len, pad=True) except exceptions.InvalidAddressException: vollog.debug( - f"Unable to read network inteface mac address from {self.dev_addr:#x}" + f"Unable to read network interface mac address from {self.dev_addr:#x}" ) return None @@ -255,10 +255,10 @@ class net_device(objects.StructType): return None def get_queue_length(self) -> int: - """Return the netwrok device transmision qeueue length (qlen) + """Return the network device transmission queue length (qlen) Returns: - int: the netwrok device transmision qeueue length (qlen) + int: the network device transmission queue length (qlen) """ return self.tx_queue_len diff --git a/volatility3/framework/symbols/linux/kallsyms.py b/volatility3/framework/symbols/linux/kallsyms.py index 35aba3ca5..be026fc4e 100644 --- a/volatility3/framework/symbols/linux/kallsyms.py +++ b/volatility3/framework/symbols/linux/kallsyms.py @@ -121,7 +121,7 @@ class _KallsymsIO: self._endian = endian def read(self, size: int) -> bytes: - """Return 'size' bytes from the current postion""" + """Return 'size' bytes from the current position""" layer = self._context.layers[self._layer_name] buf = layer.read(offset=self._position, length=size) self._position += size diff --git a/volatility3/framework/symbols/linux/utilities/module_extract.py b/volatility3/framework/symbols/linux/utilities/module_extract.py index 49ad4d9c0..e55f77668 100644 --- a/volatility3/framework/symbols/linux/utilities/module_extract.py +++ b/volatility3/framework/symbols/linux/utilities/module_extract.py @@ -21,7 +21,7 @@ from volatility3.framework.symbols.linux import extensions vollog = logging.getLogger(__name__) -# This module is responsbile for producing an ELF file of a kernel module (LKM) loaded in memory +# 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 @@ -423,7 +423,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): ) if not data: vollog.debug( - f"Coult not construct a symbol table for module at {module.vol.offset}. Cannot recover." + f"Could not construct a symbol table for module at {module.vol.offset}. Cannot recover." ) return None, None, None @@ -469,7 +469,7 @@ class ModuleExtract(interfaces.configuration.VersionableInterface): e_shentsize_int = 64 header_size = 64 - e_type = struct.pack(" Iterable[interfaces.objects.ObjectInterface]: + ) -> Iterable[extensions.module]: """Enumerate hidden modules by taking advantage of memory address alignment patterns This technique is much faster and uses less memory than the traditional scan method @@ -418,7 +416,7 @@ class Modules(interfaces.configuration.VersionableInterface): ): vollog.warning( f"Module addresses aren't aligned to {module_address_alignment} bytes. " - "Switching to 1 byte aligment scan method." + "Switching to 1 byte alignment scan method." ) module_address_alignment = 1 @@ -641,7 +639,7 @@ class Modules(interfaces.configuration.VersionableInterface): kernel = context.modules[vmlinux_name] - # For arrays, recusively get the value of each member as the type can be different + # For arrays, recursively get the value of each member as the type can be different if param_func == getters["param_array_get"]: array = param.arr @@ -862,7 +860,7 @@ class ModuleGathererKernel(ModuleGathererInterface): ) -> ModuleGathererInterface.gatherer_return_type: """ Returns a ModuleInfo instance that encodes the kernel - This is required to map function pointers to the kerenl executable + This is required to map function pointers to the kernel executable """ kernel = context.modules[kernel_module_name] @@ -920,19 +918,11 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): The constructor of the plugin must call super() with the `implementation` set """ - _version = (1, 0, 1) - _required_framework_version = (2, 0, 0) - - framework.require_interface_version(*_required_framework_version) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement( - name="kernel", - description="Linux kernel", - architectures=architectures.LINUX_ARCHS, - ), requirements.VersionRequirement( name="linux_utilities_modules", component=Modules, @@ -941,25 +931,29 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): requirements.VersionRequirement( name="linux-tainting", component=tainting.Tainting, version=(1, 0, 0) ), - requirements.BooleanRequirement( - name="dump", - description="Extract listed modules", - default=False, - optional=True, - ), ] - def generator(self): + @classmethod + def generate_results( + cls, + context: interfaces.context.ContextInterface, + implementation: Callable[ + [interfaces.context.ContextInterface, str], Iterable[extensions.module] + ], + kernel_module_name: str, + dump: bool, + open_implementation: Optional[interfaces.plugins.FileHandlerInterface], + ): """ Uses the implementation set in the constructor call to produce consistent output fields across module gathering plugins """ - for module in self.implementation(self.context, self.config["kernel"]): + for module in implementation(context, kernel_module_name): try: name = utility.array_to_string(module.name) except exceptions.InvalidAddressException: vollog.debug( - f"Unable to recover name for module {module.vol.offset:#x} from implementation {self.implementation}" + f"Unable to recover name for module {module.vol.offset:#x} from implementation {implementation}" ) continue @@ -969,21 +963,21 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): taints = ",".join( tainting.Tainting.get_taints_parsed( - self.context, self.config["kernel"], module.taints, True + context, kernel_module_name, module.taints, True ) ) parameters_iter = Modules.get_load_parameters( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) parameters = ", ".join([f"{key}={value}" for key, value in parameters_iter]) file_name = renderers.NotApplicableValue() - if self.config["dump"]: + if dump and open_implementation: elf_data = linux_utilities_module_extract.ModuleExtract.extract_module( - self.context, self.config["kernel"], module + context, kernel_module_name, module ) if not elf_data: vollog.warning( @@ -991,31 +985,30 @@ class ModuleDisplayPlugin(interfaces.configuration.VersionableInterface): ) file_name = renderers.NotAvailableValue() else: - file_name = self.open.sanitize_filename( + file_name = open_implementation.sanitize_filename( f"kernel_module.{name}.{module.vol.offset:#x}.elf" ) - with self.open(file_name) as file_handle: + with open_implementation(file_name) as file_handle: file_handle.write(elf_data) - yield 0, ( - format_hints.Hex(module.vol.offset), - name, - format_hints.Hex(code_size), - taints, - parameters, - file_name, + yield ( + 0, + ( + format_hints.Hex(module.vol.offset), + name, + format_hints.Hex(code_size), + taints, + parameters, + file_name, + ), ) - def run(self): - return renderers.TreeGrid( - [ - ("Offset", format_hints.Hex), - ("Module Name", str), - ("Code Size", format_hints.Hex), - ("Taints", str), - ("Load Arguments", str), - ("File Output", str), - ], - self._generator(), - ) + columns_results = [ + ("Offset", format_hints.Hex), + ("Module Name", str), + ("Code Size", format_hints.Hex), + ("Taints", str), + ("Load Arguments", str), + ("File Output", str), + ] diff --git a/volatility3/framework/symbols/windows/__init__.py b/volatility3/framework/symbols/windows/__init__.py index f9541579e..3296d7d2c 100755 --- a/volatility3/framework/symbols/windows/__init__.py +++ b/volatility3/framework/symbols/windows/__init__.py @@ -41,6 +41,7 @@ class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable): self.set_type_class("_POOL_TRACKER_BIG_PAGES", pool.POOL_TRACKER_BIG_PAGES) self.set_type_class("_IMAGE_DOS_HEADER", pe.IMAGE_DOS_HEADER) self.set_type_class("_KTIMER", extensions.KTIMER) + self.set_type_class("_LDR_DATA_TABLE_ENTRY", extensions.LDR_DATA_TABLE_ENTRY) # Might not necessarily defined in every version of windows self.optional_set_type_class("_IMAGE_NT_HEADERS", pe.IMAGE_NT_HEADERS) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index 4fc65564e..f32415124 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -1710,3 +1710,16 @@ class SHARED_CACHE_MAP(objects.StructType): ) return vacb_list + + +class LDR_DATA_TABLE_ENTRY(objects.StructType): + def get_load_count(self) -> Optional[int]: + try: + LoadCount = self.LoadCount.cast("short") + except Exception: + try: + LoadCount = self.ObsoleteLoadCount.cast("short") + except Exception: + LoadCount = None + + return LoadCount diff --git a/volatility3/framework/symbols/windows/extensions/callbacks.py b/volatility3/framework/symbols/windows/extensions/callbacks.py index f54db39f2..855933f59 100644 --- a/volatility3/framework/symbols/windows/extensions/callbacks.py +++ b/volatility3/framework/symbols/windows/extensions/callbacks.py @@ -48,7 +48,6 @@ class _SHUTDOWN_PACKET(objects.StructType, pool.ExecutiveObject): return False try: - device = self.DeviceObject if not device or not (device.DriverObject.DriverStart % 0x1000 == 0): vollog.debug( diff --git a/volatility3/framework/symbols/windows/extensions/consoles.py b/volatility3/framework/symbols/windows/extensions/consoles.py index 9666fd79c..b16855b9c 100644 --- a/volatility3/framework/symbols/windows/extensions/consoles.py +++ b/volatility3/framework/symbols/windows/extensions/consoles.py @@ -168,7 +168,7 @@ class SCREEN_INFORMATION(objects.StructType): @param truncate: True if the empty rows at the end (i.e. bottom) of the screen buffer should be - supressed. + suppressed. """ rows = [] diff --git a/volatility3/framework/symbols/windows/extensions/gui.py b/volatility3/framework/symbols/windows/extensions/gui.py index d1835631f..000c50319 100644 --- a/volatility3/framework/symbols/windows/extensions/gui.py +++ b/volatility3/framework/symbols/windows/extensions/gui.py @@ -146,7 +146,7 @@ class GUIExtensions(interfaces.configuration.VersionableInterface): self, window, max_windows ) -> Generator[Tuple[interfaces.objects.ObjectInterface, str], None, None]: """ - Recusively walks and yields the adjacent and child windows + Recursively walks and yields the adjacent and child windows """ seen_windows = set() seen_children = set() @@ -220,7 +220,6 @@ class GUIExtensions(interfaces.configuration.VersionableInterface): break class tagWND(objects.StructType, pool.ExecutiveObject): - def is_valid(self) -> bool: """ Enforce a valid sid diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index bf20c1ffc..ddc21798f 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -54,7 +54,6 @@ class MFTEntry(objects.StructType): return max(names, key=lambda x: len(str(x))) def _attributes(self) -> Iterator["MFTAttribute"]: - # We will update this on each pass in the next loop and use it as the new offset. attr_base_offset = self.FirstAttrOffset attribute_object_type_name = ( diff --git a/volatility3/framework/symbols/windows/extensions/shimcache.py b/volatility3/framework/symbols/windows/extensions/shimcache.py index 3b32d30c7..e7d92a48d 100644 --- a/volatility3/framework/symbols/windows/extensions/shimcache.py +++ b/volatility3/framework/symbols/windows/extensions/shimcache.py @@ -183,7 +183,6 @@ class SHIM_CACHE_ENTRY(objects.StructType): == self.ListEntry.Flink.Blink.dereference().vol.offset ) ): - return True else: return False diff --git a/volatility3/framework/symbols/windows/pdbconv.py b/volatility3/framework/symbols/windows/pdbconv.py index c23ffb350..5baf23fb2 100644 --- a/volatility3/framework/symbols/windows/pdbconv.py +++ b/volatility3/framework/symbols/windows/pdbconv.py @@ -489,7 +489,7 @@ class PdbReader: """Strips unnecessary components from the start of a symbol name.""" new_name = name - if new_name[:1] in ["_", "@", "\u007F"]: + if new_name[:1] in ["_", "@", "\u007f"]: new_name = new_name[1:] name_array = new_name.split("@") diff --git a/volatility3/framework/versionutils.py b/volatility3/framework/versionutils.py new file mode 100644 index 000000000..334410ff8 --- /dev/null +++ b/volatility3/framework/versionutils.py @@ -0,0 +1,23 @@ +# 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 +# + +from typing import Tuple + + +def matches_required(required: Tuple[int, ...], version: Tuple[int, int, int]) -> bool: + """ + Checks if a version tuple satisfies the required version major and minor constraints. + + Parameters: + required (Tuple[int, ...]): A tuple containing required major and optionally minor version numbers. + version (Tuple[int, int, int]): A tuple containing the full version (major, minor, patch). + + Returns: + bool: True if the version matches the required constraints, False otherwise. + """ + if len(required) > 0 and version[0] != required[0]: + return False + if len(required) > 1 and version[1] < required[1]: + return False + return True diff --git a/volatility3/plugins/__init__.py b/volatility3/plugins/__init__.py index 6afa8baf4..27fc0938c 100644 --- a/volatility3/plugins/__init__.py +++ b/volatility3/plugins/__init__.py @@ -12,6 +12,7 @@ are dependent upon, please DO NOT alter or remove this file unless you know the The framework is configured this way to allow plugin developers/users to override any plugin functionality whether existing or new. """ + from volatility3.framework import constants __path__ = constants.PLUGINS_PATH diff --git a/volatility3/plugins/linux/__init__.py b/volatility3/plugins/linux/__init__.py index 2d3e2386e..2ea8fb250 100644 --- a/volatility3/plugins/linux/__init__.py +++ b/volatility3/plugins/linux/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/mac/__init__.py b/volatility3/plugins/mac/__init__.py index 3ac3f1553..3f8e81ce1 100644 --- a/volatility3/plugins/mac/__init__.py +++ b/volatility3/plugins/mac/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/__init__.py b/volatility3/plugins/windows/__init__.py index d74f4fcd5..468493508 100644 --- a/volatility3/plugins/windows/__init__.py +++ b/volatility3/plugins/windows/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/plugins/windows/registry/__init__.py b/volatility3/plugins/windows/registry/__init__.py index aeeaa87f2..f012a52a0 100644 --- a/volatility3/plugins/windows/registry/__init__.py +++ b/volatility3/plugins/windows/registry/__init__.py @@ -11,6 +11,7 @@ existing or new. When overriding the plugins directory, you must include a file like this in any subdirectories that may be necessary. """ + import os import sys diff --git a/volatility3/symbols/__init__.py b/volatility3/symbols/__init__.py index c35f07cbe..162ea013e 100644 --- a/volatility3/symbols/__init__.py +++ b/volatility3/symbols/__init__.py @@ -6,6 +6,7 @@ This is the namespace for all volatility symbols, and determines the path for loading symbol ISF files """ + from volatility3.framework import constants __path__ = constants.SYMBOL_BASEPATHS