mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge branch 'develop' into vma_enumeration_smearing_protection
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,13 +66,17 @@ pip install -e ".[dev]"
|
||||
Symbol table packs for the various operating systems are available for download at:
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/windows.zip>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip>
|
||||
|
||||
The hashes to verify whether any of the symbol pack files have downloaded successfully or have changed can be found at:
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA256SUMS>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/SHA1SUMS>
|
||||
|
||||
<https://downloads.volatilityfoundation.org/volatility3/symbols/MD5SUMS>
|
||||
|
||||
Symbol tables zip files must be placed, as named, into the `volatility3/symbols` directory (or just the symbols directory next to the executable file).
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -13,7 +13,6 @@ DWARF2JSON = "./dwarf2json"
|
||||
|
||||
|
||||
class Downloader:
|
||||
|
||||
def __init__(self, url_lists: List[List[str]]) -> None:
|
||||
self.url_lists = url_lists
|
||||
|
||||
|
||||
@@ -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 <https://github.com/microsoft/avml>`_
|
||||
* `LiME - Linux Memory Extract <https://github.com/504ensicsLabs/LiME>`_
|
||||
|
||||
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 <https://github.com/504ensicsLabs/LiME/issues/111>`_.
|
||||
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 <https://github.com/Abyss-W4tcher/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 <https://isf-server.techanarchy.net/>`_ ,
|
||||
which is built and maintained by `kevthehermit <https://twitter.com/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 <volatility3.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 <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/insomnihack/2020/09/17/Insomihack-getdents.html>`_.
|
||||
Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/dfir/insomnihack-teaser-2020-getdents/>`_.
|
||||
|
||||
|
||||
.. 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 <https://github.com/stuxnet999/>`_ 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.
|
||||
|
||||
|
||||
+3
-1
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
+13
-10
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+31
-24
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
Linux-specific values that aren't found in debug symbols
|
||||
"""
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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())
|
||||
@@ -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(),
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user