Skip to content

Commit c4cc50e

Browse files
authored
Merge branch 'develop' into orphan_threads_v2
2 parents 46a26c7 + 517f46e commit c4cc50e

File tree

28 files changed

+2738
-68
lines changed

28 files changed

+2738
-68
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
4747
- name: Clean up post-test
4848
run: |
49-
rm -rf *.lime
49+
rm -rf *.bin
5050
rm -rf *.img
5151
cd volatility3/symbols
5252
rm -rf linux

vol.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python3
2+
# PYTHON_ARGCOMPLETE_OK
23

34
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
45
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0

volatility3/cli/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
from typing import Any, Dict, List, Tuple, Type, Union
2323
from urllib import parse, request
2424

25+
try:
26+
import argcomplete
27+
28+
HAS_ARGCOMPLETE = True
29+
except ImportError:
30+
HAS_ARGCOMPLETE = False
31+
2532
from volatility3.cli import text_filter
2633
import volatility3.plugins
2734
import volatility3.symbols
@@ -351,6 +358,10 @@ def run(self):
351358
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
352359

353360
# Run the argparser
361+
if HAS_ARGCOMPLETE:
362+
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
363+
# before all the plugins have been added
364+
argcomplete.autocomplete(parser)
354365
args = parser.parse_args()
355366
if args.plugin is None:
356367
parser.error("Please select a plugin to run")

volatility3/cli/volargparse.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
2121

2222
def __init__(self, *args, **kwargs) -> None:
2323
super().__init__(*args, **kwargs)
24-
# We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__
25-
self.choices = None
2624

2725
def __call__(
2826
self,
@@ -100,3 +98,20 @@ def _match_argument(self, action, arg_strings_pattern) -> int:
10098

10199
# return the number of arguments matched
102100
return len(match.group(1))
101+
102+
def _check_value(self, action: argparse.Action, value: Any) -> None:
103+
"""This is called to ensure a value is correct/valid
104+
105+
In normal operation, it would check that a value provided is valid and return None
106+
If it was not valid, it would throw an ArgumentError
107+
108+
When people provide a partial plugin name, we want to look for a matching plugin name
109+
which happens in the HelpfulSubparserAction's __call_method
110+
111+
To get there without tripping the check_value failure, we have to prevent the exception
112+
being thrown when the value is a HelpfulSubparserAction. This therefore affects no other
113+
checks for normal parameters.
114+
"""
115+
if not isinstance(action, HelpfulSubparserAction):
116+
super()._check_value(action, value)
117+
return None

volatility3/cli/volshell/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@
2121
plugins,
2222
)
2323

24+
try:
25+
import argcomplete
26+
27+
HAS_ARGCOMPLETE = True
28+
except ImportError:
29+
HAS_ARGCOMPLETE = False
30+
31+
2432
# Make sure we log everything
2533

2634
rootlog = logging.getLogger()
@@ -276,6 +284,10 @@ def run(self):
276284
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
277285

278286
# Run the argparser
287+
if HAS_ARGCOMPLETE:
288+
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
289+
# before all the plugins have been added
290+
argcomplete.autocomplete(parser)
279291
args = parser.parse_args()
280292

281293
vollog.log(

volatility3/framework/constants/_version.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# We use the SemVer 2.0.0 versioning scheme
22
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
3-
VERSION_MINOR = 7 # Number of changes that only add to the interface
4-
VERSION_PATCH = 2 # Number of changes that do not change the interface
3+
VERSION_MINOR = 9 # Number of changes that only add to the interface
4+
VERSION_PATCH = 0 # Number of changes that do not change the interface
55
VERSION_SUFFIX = ""
66

7-
# TODO: At version 2.0.0, remove the symbol_shift feature
8-
97
PACKAGE_VERSION = (
108
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
119
+ VERSION_SUFFIX
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
2+
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
3+
#
4+
import logging
5+
from typing import List
6+
7+
from volatility3.framework import renderers, interfaces, exceptions
8+
from volatility3.framework.renderers import format_hints
9+
from volatility3.framework.interfaces import plugins
10+
from volatility3.framework.configuration import requirements
11+
12+
vollog = logging.getLogger(__name__)
13+
14+
15+
class EBPF(plugins.PluginInterface):
16+
"""Enumerate eBPF programs"""
17+
18+
_required_framework_version = (2, 0, 0)
19+
20+
_version = (1, 0, 0)
21+
22+
@classmethod
23+
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
24+
return [
25+
requirements.ModuleRequirement(
26+
name="kernel",
27+
description="Linux kernel",
28+
architectures=["Intel32", "Intel64"],
29+
),
30+
]
31+
32+
def get_ebpf_programs(
33+
self,
34+
context: interfaces.context.ContextInterface,
35+
vmlinux_module_name: str,
36+
) -> interfaces.objects.ObjectInterface:
37+
"""Enumerate eBPF programs walking its IDR.
38+
39+
Args:
40+
context: The context to retrieve required elements (layers, symbol tables) from
41+
vmlinux_module_name: The name of the kernel module on which to operate
42+
Yields:
43+
eBPF program objects
44+
"""
45+
vmlinux = context.modules[vmlinux_module_name]
46+
47+
if not vmlinux.has_symbol("prog_idr"):
48+
raise exceptions.VolatilityException(
49+
"Cannot find the eBPF prog idr. Unsupported kernel"
50+
)
51+
52+
prog_idr = vmlinux.object_from_symbol("prog_idr")
53+
for page_addr in prog_idr.get_entries():
54+
bpf_prog = vmlinux.object("bpf_prog", offset=page_addr, absolute=True)
55+
yield bpf_prog
56+
57+
def _generator(self):
58+
for prog in self.get_ebpf_programs(self.context, self.config["kernel"]):
59+
prog_addr = prog.vol.offset
60+
prog_type = prog.get_type() or renderers.NotAvailableValue()
61+
prog_tag = prog.get_tag() or renderers.NotAvailableValue()
62+
prog_name = prog.get_name() or renderers.NotAvailableValue()
63+
fields = (format_hints.Hex(prog_addr), prog_name, prog_tag, prog_type)
64+
yield (0, fields)
65+
66+
def run(self):
67+
headers = [
68+
("Address", format_hints.Hex),
69+
("Name", str),
70+
("Tag", str),
71+
("Type", str),
72+
]
73+
return renderers.TreeGrid(headers, self._generator())

volatility3/framework/plugins/linux/lsof.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
1+
# This file is Copyright 2024 Volatility Foundation and licensed under the Volatility Software License 1.0
22
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
33
#
44
"""A module containing a collection of plugins that produce data typically
55
found in Linux's /proc file system."""
6-
import logging
6+
import logging, datetime
77
from typing import List, Callable
88

9-
from volatility3.framework import renderers, interfaces, constants
9+
from volatility3.framework import renderers, interfaces, constants, exceptions
1010
from volatility3.framework.configuration import requirements
1111
from volatility3.framework.interfaces import plugins
1212
from volatility3.framework.objects import utility
1313
from volatility3.framework.symbols import linux
1414
from volatility3.plugins.linux import pslist
15+
from volatility3.plugins import timeliner
1516

1617
vollog = logging.getLogger(__name__)
1718

1819

19-
class Lsof(plugins.PluginInterface):
20-
"""Lists all memory maps for all processes."""
20+
class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
21+
"""Lists open files for each processes."""
2122

2223
_required_framework_version = (2, 0, 0)
23-
24-
_version = (1, 1, 0)
24+
_version = (1, 2, 0)
2525

2626
@classmethod
2727
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -45,14 +45,37 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
4545
),
4646
]
4747

48+
@classmethod
49+
def get_inode_metadata(cls, filp: interfaces.objects.ObjectInterface):
50+
try:
51+
dentry = filp.get_dentry()
52+
if dentry:
53+
inode_object = dentry.d_inode
54+
if inode_object and inode_object.is_valid():
55+
itype = (
56+
inode_object.get_inode_type() or renderers.NotAvailableValue()
57+
)
58+
return (
59+
inode_object.i_ino,
60+
itype,
61+
inode_object.i_size,
62+
inode_object.get_file_mode(),
63+
inode_object.get_change_time(),
64+
inode_object.get_modification_time(),
65+
inode_object.get_access_time(),
66+
)
67+
except (exceptions.InvalidAddressException, AttributeError) as e:
68+
vollog.warning(f"Can't get inode metadata: {e}")
69+
return None
70+
4871
@classmethod
4972
def list_fds(
5073
cls,
5174
context: interfaces.context.ContextInterface,
5275
symbol_table: str,
5376
filter_func: Callable[[int], bool] = lambda _: False,
5477
):
55-
linuxutils_symbol_table = None # type: ignore
78+
linuxutils_symbol_table = None
5679
for task in pslist.PsList.list_tasks(context, symbol_table, filter_func):
5780
if linuxutils_symbol_table is None:
5881
if constants.BANG not in task.vol.type_name:
@@ -69,21 +92,79 @@ def list_fds(
6992
for fd_fields in fd_generator:
7093
yield pid, task_comm, task, fd_fields
7194

95+
@classmethod
96+
def list_fds_and_inodes(
97+
cls,
98+
context: interfaces.context.ContextInterface,
99+
symbol_table: str,
100+
filter_func: Callable[[int], bool] = lambda _: False,
101+
):
102+
for pid, task_comm, task, (fd_num, filp, full_path) in cls.list_fds(
103+
context, symbol_table, filter_func
104+
):
105+
inode_metadata = cls.get_inode_metadata(filp)
106+
if inode_metadata is None:
107+
inode_metadata = tuple(
108+
interfaces.renderers.BaseAbsentValue() for _ in range(7)
109+
)
110+
yield pid, task_comm, task, fd_num, filp, full_path, inode_metadata
111+
72112
def _generator(self, pids, symbol_table):
73113
filter_func = pslist.PsList.create_pid_filter(pids)
74-
fds_generator = self.list_fds(
114+
fds_generator = self.list_fds_and_inodes(
75115
self.context, symbol_table, filter_func=filter_func
76116
)
77117

78-
for pid, task_comm, _task, fd_fields in fds_generator:
79-
fd_num, _filp, full_path = fd_fields
80-
81-
fields = (pid, task_comm, fd_num, full_path)
118+
for (
119+
pid,
120+
task_comm,
121+
task,
122+
fd_num,
123+
filp,
124+
full_path,
125+
inode_metadata,
126+
) in fds_generator:
127+
inode_num, itype, file_size, imode, ctime, mtime, atime = inode_metadata
128+
fields = (
129+
pid,
130+
task_comm,
131+
fd_num,
132+
full_path,
133+
inode_num,
134+
itype,
135+
imode,
136+
ctime,
137+
mtime,
138+
atime,
139+
file_size,
140+
)
82141
yield (0, fields)
83142

84143
def run(self):
85144
pids = self.config.get("pid", None)
86145
symbol_table = self.config["kernel"]
87146

88-
tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)]
147+
tree_grid_args = [
148+
("PID", int),
149+
("Process", str),
150+
("FD", int),
151+
("Path", str),
152+
("Inode", int),
153+
("Type", str),
154+
("Mode", str),
155+
("Changed", datetime.datetime),
156+
("Modified", datetime.datetime),
157+
("Accessed", datetime.datetime),
158+
("Size", int),
159+
]
89160
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
161+
162+
def generate_timeline(self):
163+
pids = self.config.get("pid", None)
164+
symbol_table = self.config["kernel"]
165+
for row in self._generator(pids, symbol_table):
166+
_depth, row_data = row
167+
description = f'Process {row_data[1]} ({row_data[0]}) Open "{row_data[3]}"'
168+
yield description, timeliner.TimeLinerType.CHANGED, row_data[7]
169+
yield description, timeliner.TimeLinerType.MODIFIED, row_data[8]
170+
yield description, timeliner.TimeLinerType.ACCESSED, row_data[9]

0 commit comments

Comments
 (0)