Merge branch 'develop' into linux_ifconfig_plugin

This commit is contained in:
ikelos
2025-01-30 11:01:06 +00:00
committed by GitHub
297 changed files with 39079 additions and 3686 deletions
+4 -2
View File
@@ -1,4 +1,4 @@
name: Black python linter
name: Black python formatter
on: [push, pull_request]
@@ -6,8 +6,10 @@ jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: psf/black@stable
with:
options: "--check --diff --verbose"
src: "./volatility3"
# FIXME: Remove when Volatility3 minimum Python version is >3.8
version: "24.8.0"
+50
View File
@@ -0,0 +1,50 @@
name: build-pyinstaller
on:
push:
branches:
- stable
- develop
- 'release/**'
pull_request:
branches:
- stable
- 'release/**'
jobs:
exe:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pyinstaller
- name: Pyinstall executable
run: |
pyinstaller --clean -y vol.spec
pyinstaller --clean -y volshell.spec
- name: Move files
run: |
mv dist/vol.exe vol.exe
mv dist/volshell.exe volshell.exe
- name: Archive
uses: actions/upload-artifact@v4
with:
name: volatility3-pyinstaller
path: |
vol.exe
volshell.exe
README.md
LICENSE.txt
+6 -7
View File
@@ -18,26 +18,25 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
python-version: ["3.8"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
pip install build
- name: Build PyPi packages
run: |
python setup.py sdist --formats=gztar,zip
python setup.py bdist_wheel
python -m build
- name: Archive dist
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: volatility3-pypi
path: |
+3 -7
View File
@@ -8,9 +8,9 @@ jobs:
fail-fast: false
matrix:
host: [ ubuntu-latest, windows-latest ]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
@@ -20,12 +20,8 @@ jobs:
- name: Setup python-pip
run: python -m pip install --upgrade pip
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Install volatility3
run: pip install .
- name: Run volatility3
run: vol --help
run: vol --help
+15
View File
@@ -0,0 +1,15 @@
---
name: Ruff
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v1
with:
args: check
src: "."
+17 -13
View File
@@ -6,32 +6,32 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
python-version: ["3.8"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install Cmake
pip install setuptools wheel
pip install -r ./test/requirements-testing.txt
python -m pip install --upgrade pip Cmake build
pip install .[test]
- name: Build PyPi packages
run: |
python setup.py sdist --formats=gztar,zip
python setup.py bdist_wheel
python -m build
- name: Download images
run: |
mkdir test_images
cd test_images
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz"
gunzip linux-sample-1.bin.gz
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz"
gunzip win-xp-laptop-2005-06-25.img.gz
cd -
- name: Download and Extract symbols
run: |
@@ -42,13 +42,17 @@ jobs:
- name: Testing...
run: |
py.test ./test/test_volatility.py --volatility=vol.py --image win-xp-laptop-2005-06-25.img -k test_windows -v
py.test ./test/test_volatility.py --volatility=vol.py --image linux-sample-1.bin -k test_linux -v
# VolShell
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_windows_volshell -v
pytest ./test/test_volatility.py --volatility=volshell.py --image-dir=./test_images -k test_linux_volshell -v
# Volatility
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_windows and not test_windows_volshell" -v
pytest ./test/test_volatility.py --volatility=vol.py --image-dir=./test_images -k "test_linux and not test_linux_volshell" -v
- name: Clean up post-test
run: |
rm -rf *.lime
rm -rf *.img
rm -rf test_images
cd volatility3/symbols
rm -rf linux
rm -rf linux.zip
+1
View File
@@ -27,6 +27,7 @@ config*.json
# Pyinstaller files
build
dist
*.egg-info
# Environments
.env
+4 -1
View File
@@ -20,4 +20,7 @@ build:
# Optionally set the version of Python and requirements required to build your docs
python:
install:
- requirements: doc/requirements.txt
- method: pip
path: .
extra_requirements:
- docs
-261
View File
@@ -1,261 +0,0 @@
[style]
# Align closing bracket with visual indentation.
align_closing_bracket_with_visual_indent=True
# Allow dictionary keys to exist on multiple lines. For example:
#
# x = {
# ('this is the first element of a tuple',
# 'this is the second element of a tuple'):
# value,
# }
allow_multiline_dictionary_keys=False
# Allow lambdas to be formatted on more than one line.
allow_multiline_lambdas=False
# Allow splits before the dictionary value.
allow_split_before_dict_value=True
# Number of blank lines surrounding top-level function and class
# definitions.
blank_lines_around_top_level_definition=2
# Insert a blank line before a class-level docstring.
blank_line_before_class_docstring=False
# Insert a blank line before a module docstring.
blank_line_before_module_docstring=False
# Insert a blank line before a 'def' or 'class' immediately nested
# within another 'def' or 'class'. For example:
#
# class Foo:
# # <------ this blank line
# def method():
# ...
blank_line_before_nested_class_or_def=True
# Do not split consecutive brackets. Only relevant when
# dedent_closing_brackets is set. For example:
#
# call_func_that_takes_a_dict(
# {
# 'key1': 'value1',
# 'key2': 'value2',
# }
# )
#
# would reformat to:
#
# call_func_that_takes_a_dict({
# 'key1': 'value1',
# 'key2': 'value2',
# })
coalesce_brackets=False
# The column limit.
column_limit=120
# The style for continuation alignment. Possible values are:
#
# - SPACE: Use spaces for continuation alignment. This is default behavior.
# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs) for continuation
# alignment.
# - LESS: Slightly left if cannot vertically align continuation lines with
# indent characters.
# - VALIGN-RIGHT: Vertically align continuation lines with indent
# characters. Slightly right (one more indent character) if cannot
# vertically align continuation lines with indent characters.
#
# For options FIXED, and VALIGN-RIGHT are only available when USE_TABS is
# enabled.
continuation_align_style=SPACE
# Indent width used for line continuations.
continuation_indent_width=4
# Put closing brackets on a separate line, dedented, if the bracketed
# expression can't fit in a single line. Applies to all kinds of brackets,
# including function definitions and calls. For example:
#
# config = {
# 'key1': 'value1',
# 'key2': 'value2',
# } # <--- this bracket is dedented and on a separate line
#
# time_series = self.remote_client.query_entity_counters(
# entity='dev3246.region1',
# key='dns.query_latency_tcp',
# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
# start_ts=now()-timedelta(days=3),
# end_ts=now(),
# ) # <--- this bracket is dedented and on a separate line
dedent_closing_brackets=False
# Disable the heuristic which places each list element on a separate line
# if the list is comma-terminated.
disable_ending_comma_heuristic=False
# Place each dictionary entry onto its own line.
each_dict_entry_on_separate_line=True
# The regex for an i18n comment. The presence of this comment stops
# reformatting of that line, because the comments are required to be
# next to the string they translate.
i18n_comment=
# The i18n function call names. The presence of this function stops
# reformatting on that line, because the string it has cannot be moved
# away from the i18n comment.
i18n_function_call=
# Indent the dictionary value if it cannot fit on the same line as the
# dictionary key. For example:
#
# config = {
# 'key1':
# 'value1',
# 'key2': value1 +
# value2,
# }
indent_dictionary_value=False
# The number of columns to use for indentation.
indent_width=4
# Join short lines into one line. E.g., single line 'if' statements.
join_multiple_lines=True
# Do not include spaces around selected binary operators. For example:
#
# 1 + 2 * 3 - 4 / 5
#
# will be formatted as follows when configured with "*,/":
#
# 1 + 2*3 - 4/5
#
no_spaces_around_selected_binary_operators=
# Use spaces around default or named assigns.
spaces_around_default_or_named_assign=True
# Use spaces around the power operator.
spaces_around_power_operator=True
# The number of spaces required before a trailing comment.
spaces_before_comment=2
# Insert a space between the ending comma and closing bracket of a list,
# etc.
space_between_ending_comma_and_closing_bracket=True
# Split before arguments
split_all_comma_separated_values=False
# Split before arguments if the argument list is terminated by a
# comma.
split_arguments_when_comma_terminated=False
# Set to True to prefer splitting before '&', '|' or '^' rather than
# after.
split_before_bitwise_operator=True
# Split before the closing bracket if a list or dict literal doesn't fit on
# a single line.
split_before_closing_bracket=True
# Split before a dictionary or set generator (comp_for). For example, note
# the split before the 'for':
#
# foo = {
# variable: 'Hello world, have a nice day!'
# for variable in bar if variable != 42
# }
split_before_dict_set_generator=True
# Split before the '.' if we need to split a longer expression:
#
# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d))
#
# would reformat to something like:
#
# foo = ('This is a really long string: {}, {}, {}, {}'
# .format(a, b, c, d))
split_before_dot=False
# Split after the opening paren which surrounds an expression if it doesn't
# fit on a single line.
split_before_expression_after_opening_paren=False
# If an argument / parameter list is going to be split, then split before
# the first argument.
split_before_first_argument=False
# Set to True to prefer splitting before 'and' or 'or' rather than
# after.
split_before_logical_operator=True
# Split named assignments onto individual lines.
split_before_named_assigns=True
# Set to True to split list comprehensions and generators that have
# non-trivial expressions and multiple clauses before each of these
# clauses. For example:
#
# result = [
# a_long_var + 100 for a_long_var in xrange(1000)
# if a_long_var % 10]
#
# would reformat to something like:
#
# result = [
# a_long_var + 100
# for a_long_var in xrange(1000)
# if a_long_var % 10]
split_complex_comprehension=True
# The penalty for splitting right after the opening bracket.
split_penalty_after_opening_bracket=200
# The penalty for splitting the line after a unary operator.
split_penalty_after_unary_operator=10000
# The penalty for splitting right before an if expression.
split_penalty_before_if_expr=0
# The penalty of splitting the line around the '&', '|', and '^'
# operators.
split_penalty_bitwise_operator=300
# The penalty for splitting a list comprehension or generator
# expression.
split_penalty_comprehension=80
# The penalty for characters over the column limit.
split_penalty_excess_character=7000
# The penalty incurred by adding a line split to the unwrapped line. The
# more line splits added the higher the penalty.
split_penalty_for_added_line_split=30
# The penalty of splitting a list of "import as" names. For example:
#
# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
# long_argument_2,
# long_argument_3)
#
# would reformat to something like:
#
# from a_very_long_or_indented_module_name_yada_yad import (
# long_argument_1, long_argument_2, long_argument_3)
split_penalty_import_names=0
# The penalty of splitting the line around the 'and' and 'or'
# operators.
split_penalty_logical_operator=300
# Use the Tab character for indentation.
use_tabs=False
+1 -1
View File
@@ -14,7 +14,7 @@ authors:
identifiers:
- type: url
value: 'https://github.com/volatilityfoundation/volatility3'
description: Volatility 3 source code respository
description: Volatility 3 source code repository
repository-code: 'https://github.com/volatilityfoundation/volatility3'
url: 'https://github.com/volatilityfoundation/volatility3'
abstract: >-
+1 -1
View File
@@ -1,6 +1,6 @@
prune development
include * .*
include doc/make.bat doc/Makefile doc/requirements.txt
include pyproject.toml doc/make.bat doc/Makefile
recursive-include doc/source *
recursive-include volatility3 *.json
recursive-exclude doc/source volatility3.*.rst
+16 -35
View File
@@ -18,63 +18,44 @@ the Volatility Software License (VSL). See the
[LICENSE](https://www.volatilityfoundation.org/license/vsl-v1.0) file for
more details.
## Requirements
## Installing
Volatility 3 requires Python 3.7.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
Volatility 3 requires Python 3.8.0 or later and is published on the [PyPi registry](https://pypi.org/project/volatility3).
```shell
pip3 install -r requirements-minimal.txt
pip install volatility3
```
Alternately, the minimal packages will be installed automatically when Volatility 3 is installed using setup.py. However, as noted in the Quick Start section below, Volatility 3 does not *need* to be installed via setup.py prior to using it.
If you want to use the latest development version of Volatility 3 we recommend you manually clone this repository and install an editable version of the project.
We recommend you use a virtual environment to keep installed dependencies separate from system packages.
```shell
python3 setup.py build
python3 setup.py install
```
To enable the full range of Volatility 3 functionality, use a command like the one below. For partial functionality, comment out any unnecessary packages in [requirements.txt](requirements.txt) prior to running the command.
```shell
pip3 install -r requirements.txt
```
## Downloading Volatility
The latest stable version of Volatility will always be the stable branch of the GitHub repository. You can get the latest version of the code using the following command:
The latest stable version of Volatility will always be the `stable` branch of the GitHub repository. The default branch is `develop`.
```shell
git clone https://github.com/volatilityfoundation/volatility3.git
cd volatility3/
python3 -m venv venv && . venv/bin/activate
pip install -e .[dev]
```
## Quick Start
1. Clone the latest version of Volatility from GitHub:
```shell
git clone https://github.com/volatilityfoundation/volatility3.git
```
1. Install Volatility 3 as documented in the Installing section of the readme.
2. See available options:
```shell
python3 vol.py -h
vol -h
```
3. To get more information on a Windows memory sample and to make sure
Volatility supports that sample type, run
`python3 vol.py -f <imagepath> windows.info`
Example:
3. To get more information on a Windows memory sample and to make sure Volatility supports that sample type, run `vol -f <imagepath> windows.info`:
```shell
python3 vol.py -f /home/user/samples/stuxnet.vmem windows.info
vol -f /home/user/samples/stuxnet.vmem windows.info
```
4. Run some other plugins. The `-f` or `--single-location` is not strictly
required, but most plugins expect a single sample. Some also
require/accept other options. Run `python3 vol.py <plugin> -h`
for more information on a particular command.
4. Run some other plugins. The `-f` or `--single-location` is not strictly required, but most plugins expect a single sample.
Some also require/accept other options. Run `vol <plugin> -h` for more information on a particular command.
## Symbol Tables
@@ -107,7 +88,7 @@ The latest generated copy of the documentation can be found at: <https://volatil
## Licensing and Copyright
Copyright (C) 2007-2023 Volatility Foundation
Copyright (C) 2007-2025 Volatility Foundation
All Rights Reserved
+31 -14
View File
@@ -28,10 +28,10 @@ class BannerCacheGenerator:
def run(self):
context = contexts.Context()
json_output = {'version': 1}
json_output = {"version": 1}
path = self._path
filename = '*'
filename = "*"
for banner_cache in [linux.LinuxBannerCache, mac.MacBannerCache]:
sub_path = banner_cache.os
@@ -39,37 +39,54 @@ class BannerCacheGenerator:
for extension in constants.ISF_EXTENSIONS:
# Hopefully these will not be large lists, otherwise this might be slow
try:
for found in pathlib.Path(path).joinpath(sub_path).resolve().rglob(filename + extension):
for found in (
pathlib.Path(path)
.joinpath(sub_path)
.resolve()
.rglob(filename + extension)
):
potentials.append(found.as_uri())
except FileNotFoundError:
# If there's no linux symbols, don't cry about it
pass
new_banners = banner_cache.read_new_banners(context, 'BannerServer', potentials, banner_cache.symbol_name,
banner_cache.os, progress_callback = PrintedProgress())
new_banners = banner_cache.read_new_banners(
context,
"BannerServer",
potentials,
banner_cache.symbol_name,
banner_cache.os,
progress_callback=PrintedProgress(),
)
result_banners = {}
for new_banner in new_banners:
# Only accept file schemes
value = [self.convert_url(url) for url in new_banners[new_banner] if
urllib.parse.urlparse(url).scheme == 'file']
value = [
self.convert_url(url)
for url in new_banners[new_banner]
if urllib.parse.urlparse(url).scheme == "file"
]
if value and new_banner:
# Convert files into URLs
result_banners[str(base64.b64encode(new_banner), 'latin-1')] = value
result_banners[str(base64.b64encode(new_banner), "latin-1")] = value
json_output[banner_cache.os] = result_banners
output_path = os.path.join(self._path, 'banners.json')
with open(output_path, 'w') as fp:
output_path = os.path.join(self._path, "banners.json")
with open(output_path, "w") as fp:
vollog.warning(f"Banners file written to {output_path}")
json.dump(json_output, fp)
if __name__ == '__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--path', default = os.path.dirname(__file__))
parser.add_argument('--urlprefix', help = 'Web prefix that will eventually serve the ISF files',
default = 'http://localhost/symbols')
parser.add_argument("--path", default=os.path.dirname(__file__))
parser.add_argument(
"--urlprefix",
help="Web prefix that will eventually serve the ISF files",
default="http://localhost/symbols",
)
args = parser.parse_args()
+224 -116
View File
@@ -15,17 +15,17 @@ class VolatilityImage:
filepath: str = ""
vol2_profile: str = ""
vol2_imageinfo_time: float = None
vol2_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
vol3_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
rekall_plugin_parameters: Dict[str, List[str]] = field(default_factory = dict)
vol2_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
vol3_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
rekall_plugin_parameters: Dict[str, List[str]] = field(default_factory=dict)
@dataclass
class VolatilityPlugin:
name: str = ""
vol2_plugin_parameters: List[str] = field(default_factory = list)
vol3_plugin_parameters: List[str] = field(default_factory = list)
rekall_plugin_parameters: List[str] = field(default_factory = list)
vol2_plugin_parameters: List[str] = field(default_factory=list)
vol3_plugin_parameters: List[str] = field(default_factory=list)
rekall_plugin_parameters: List[str] = field(default_factory=list)
class VolatilityTest:
@@ -39,32 +39,50 @@ class VolatilityTest:
def result_titles(self) -> List[str]:
return [self.long_name]
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None:
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> None:
pass
def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> List[float]:
def create_results(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> List[float]:
self.create_prerequisites(plugin, image, image_hash)
# Volatility 2 Test
print(f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}")
print(
f"[*] Testing {self.short_name} {plugin.name} with image {image.filepath}"
)
os.chdir(self.path)
cmd = self.plugin_cmd(plugin, image)
start_time = time.perf_counter()
try:
completed = subprocess.run(cmd, cwd = self.path, capture_output = True, timeout = 420)
completed = subprocess.run(
cmd, cwd=self.path, capture_output=True, timeout=420
)
except subprocess.TimeoutExpired as excp:
completed = excp
end_time = time.perf_counter()
total_time = end_time - start_time
print(f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}")
print(
f" Tested {self.short_name} {plugin.name} with image {image.filepath}: {total_time}"
)
with open(
os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stdout'),
"wb") as f:
os.path.join(
self.output_directory,
f"{self.short_name}_{plugin.name}_{image_hash}_stdout",
),
"wb",
) as f:
f.write(completed.stdout)
if completed.stderr:
with open(
os.path.join(self.output_directory, f'{self.short_name}_{plugin.name}_{image_hash}_stderr'),
"wb") as f:
os.path.join(
self.output_directory,
f"{self.short_name}_{plugin.name}_{image_hash}_stderr",
),
"wb",
) as f:
f.write(completed.stderr)
return [total_time]
@@ -77,31 +95,57 @@ class Volatility2Test(VolatilityTest):
long_name = "Volatility 2"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage):
return ["python2", "-u", "vol.py", "-f", image.filepath, "--profile", image.vol2_profile
] + plugin.vol2_plugin_parameters + image.vol2_plugin_parameters.get(plugin.name, [])
return (
[
"python2",
"-u",
"vol.py",
"-f",
image.filepath,
"--profile",
image.vol2_profile,
]
+ plugin.vol2_plugin_parameters
+ image.vol2_plugin_parameters.get(plugin.name, [])
)
def result_titles(self):
return [self.long_name, "Imageinfo", f"{self.long_name} + Imageinfo"]
def create_results(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash) -> List[float]:
def create_results(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash
) -> List[float]:
result = super().create_results(plugin, image, image_hash)
result += [image.vol2_imageinfo_time, result[0] + image.vol2_imageinfo_time]
return result
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash):
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash
):
# Volatility 2 image info
if not image.vol2_profile:
print(f"[*] Testing {self.short_name} imageinfo with image {image.filepath}")
print(
f"[*] Testing {self.short_name} imageinfo with image {image.filepath}"
)
os.chdir(self.path)
cmd = ["python2", "-u", "vol.py", "-f", image.filepath, "imageinfo"]
start_time = time.perf_counter()
vol2_completed = subprocess.run(cmd, cwd = self.path, capture_output = True)
vol2_completed = subprocess.run(cmd, cwd=self.path, capture_output=True)
end_time = time.perf_counter()
image.vol2_imageinfo_time = end_time - start_time
print(f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}")
with open(os.path.join(self.output_directory, f'vol2_imageinfo_{image_hash}_stdout'), "wb") as f:
print(
f" Tested volatility2 imageinfo with image {image.filepath}: {end_time - start_time}"
)
with open(
os.path.join(
self.output_directory, f"vol2_imageinfo_{image_hash}_stdout"
),
"wb",
) as f:
f.write(vol2_completed.stdout)
image.vol2_profile = re.search(b"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout)[1]
image.vol2_profile = re.search(
rb"Suggested Profile\(s\) : ([^,]+)", vol2_completed.stdout
)[1]
class RekallTest(VolatilityTest):
@@ -113,11 +157,16 @@ class RekallTest(VolatilityTest):
plugin.rekall_plugin_parameters = plugin.vol2_plugin_parameters
if not image.rekall_plugin_parameters:
image.rekall_plugin_parameters = image.vol2_plugin_parameters
return ["rekall", "-f", image.filepath] + plugin.rekall_plugin_parameters + image.rekall_plugin_parameters.get(
plugin.name, [])
return (
["rekall", "-f", image.filepath]
+ plugin.rekall_plugin_parameters
+ image.rekall_plugin_parameters.get(plugin.name, [])
)
def create_prerequisites(self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str) -> None:
shutil.rmtree('/home/mike/.rekall_cache/sessions')
def create_prerequisites(
self, plugin: VolatilityPlugin, image: VolatilityImage, image_hash: str
) -> None:
shutil.rmtree("/home/mike/.rekall_cache/sessions")
class Volatility3Test(VolatilityTest):
@@ -125,14 +174,18 @@ class Volatility3Test(VolatilityTest):
long_name = "Volatility 3"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]:
return [
"python",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, [])
return (
[
"python",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
]
+ plugin.vol3_plugin_parameters
+ image.vol3_plugin_parameters.get(plugin.name, [])
)
class Volatility3PyPyTest(VolatilityTest):
@@ -140,26 +193,32 @@ class Volatility3PyPyTest(VolatilityTest):
long_name = "Volatility 3 (PyPy)"
def plugin_cmd(self, plugin: VolatilityPlugin, image: VolatilityImage) -> List[str]:
return [
"pypy3",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
] + plugin.vol3_plugin_parameters + image.vol3_plugin_parameters.get(plugin.name, [])
return (
[
"pypy3",
"-u",
"vol.py",
"-q",
"-f",
image.filepath,
]
+ plugin.vol3_plugin_parameters
+ image.vol3_plugin_parameters.get(plugin.name, [])
)
class VolatilityTester:
def __init__(self,
images: List[VolatilityImage],
plugins: List[VolatilityPlugin],
frameworks: List[str],
output_dir: str,
vol2_path: str = None,
vol3_path: str = None,
rekall_path = None):
def __init__(
self,
images: List[VolatilityImage],
plugins: List[VolatilityPlugin],
frameworks: List[str],
output_dir: str,
vol2_path: str = None,
vol3_path: str = None,
rekall_path=None,
):
self.images = images
self.plugins = plugins
if not vol2_path:
@@ -172,7 +231,7 @@ class VolatilityTester:
Volatility3Test(vol3_path, output_dir),
Volatility3PyPyTest(vol3_path, output_dir),
Volatility2Test(vol2_path, output_dir),
RekallTest(rekall_path, output_dir)
RekallTest(rekall_path, output_dir),
]
self.tests = [x for x in available_tests if x.short_name.lower() in frameworks]
self.csv_writer = None
@@ -183,7 +242,7 @@ class VolatilityTester:
print(f"[?] Frameworks: {[x.long_name for x in self.tests]}")
def run_tests(self):
with open("volatility-timings.csv", 'w') as csvfile:
with open("volatility-timings.csv", "w") as csvfile:
self.csv_writer = csv.writer(csvfile)
titles = ["Image Hash", "Image Path", "Plugin Name"]
for test in self.tests:
@@ -203,72 +262,121 @@ class VolatilityTester:
self.csv_writer.writerow([image_hash, image.filepath, plugin.name] + results)
if __name__ == '__main__':
if __name__ == "__main__":
plugins = [
VolatilityPlugin(name = "pslist",
vol2_plugin_parameters = ["pslist"],
vol3_plugin_parameters = ["windows.pslist"]),
VolatilityPlugin(name = "psscan",
vol2_plugin_parameters = ["psscan"],
vol3_plugin_parameters = ["windows.psscan"],
rekall_plugin_parameters = ["psscan", "--scan_kernel"]),
VolatilityPlugin(name = "driverscan",
vol2_plugin_parameters = ["driverscan"],
vol3_plugin_parameters = ["windows.driverscan"],
rekall_plugin_parameters = ["driverscan", "--scan_kernel"]),
VolatilityPlugin(name = "handles",
vol2_plugin_parameters = ["handles"],
vol3_plugin_parameters = ["windows.handles"]),
VolatilityPlugin(name = "modules",
vol2_plugin_parameters = ["modules"],
vol3_plugin_parameters = ["windows.modules"]),
VolatilityPlugin(name = "hivelist",
vol2_plugin_parameters = ["hivelist"],
vol3_plugin_parameters = ["registry.hivelist"],
rekall_plugin_parameters = ["hives"]),
VolatilityPlugin(name = "vadinfo",
vol2_plugin_parameters = ["vadinfo"],
vol3_plugin_parameters = ["windows.vadinfo"],
rekall_plugin_parameters = ["vad"]),
VolatilityPlugin(name = "modscan",
vol2_plugin_parameters = ["modscan"],
vol3_plugin_parameters = ["windows.modscan"],
rekall_plugin_parameters = ["modscan", "--scan_kernel"]),
VolatilityPlugin(name = "svcscan",
vol2_plugin_parameters = ["svcscan"],
vol3_plugin_parameters = ["windows.svcscan"],
rekall_plugin_parameters = ["svcscan"]),
VolatilityPlugin(name = "ssdt", vol2_plugin_parameters = ["ssdt"], vol3_plugin_parameters = ["windows.ssdt"]),
VolatilityPlugin(name = "printkey",
vol2_plugin_parameters = ["printkey", "-K", "Classes"],
vol3_plugin_parameters = ["registry.printkey", "--key", "Classes"],
rekall_plugin_parameters = ["printkey", "--key", "Classes"])
VolatilityPlugin(
name="pslist",
vol2_plugin_parameters=["pslist"],
vol3_plugin_parameters=["windows.pslist"],
),
VolatilityPlugin(
name="psscan",
vol2_plugin_parameters=["psscan"],
vol3_plugin_parameters=["windows.psscan"],
rekall_plugin_parameters=["psscan", "--scan_kernel"],
),
VolatilityPlugin(
name="driverscan",
vol2_plugin_parameters=["driverscan"],
vol3_plugin_parameters=["windows.driverscan"],
rekall_plugin_parameters=["driverscan", "--scan_kernel"],
),
VolatilityPlugin(
name="handles",
vol2_plugin_parameters=["handles"],
vol3_plugin_parameters=["windows.handles"],
),
VolatilityPlugin(
name="modules",
vol2_plugin_parameters=["modules"],
vol3_plugin_parameters=["windows.modules"],
),
VolatilityPlugin(
name="hivelist",
vol2_plugin_parameters=["hivelist"],
vol3_plugin_parameters=["registry.hivelist"],
rekall_plugin_parameters=["hives"],
),
VolatilityPlugin(
name="vadinfo",
vol2_plugin_parameters=["vadinfo"],
vol3_plugin_parameters=["windows.vadinfo"],
rekall_plugin_parameters=["vad"],
),
VolatilityPlugin(
name="modscan",
vol2_plugin_parameters=["modscan"],
vol3_plugin_parameters=["windows.modscan"],
rekall_plugin_parameters=["modscan", "--scan_kernel"],
),
VolatilityPlugin(
name="svcscan",
vol2_plugin_parameters=["svcscan"],
vol3_plugin_parameters=["windows.svcscan"],
rekall_plugin_parameters=["svcscan"],
),
VolatilityPlugin(
name="ssdt",
vol2_plugin_parameters=["ssdt"],
vol3_plugin_parameters=["windows.ssdt"],
),
VolatilityPlugin(
name="printkey",
vol2_plugin_parameters=["printkey", "-K", "Classes"],
vol3_plugin_parameters=["registry.printkey", "--key", "Classes"],
rekall_plugin_parameters=["printkey", "--key", "Classes"],
),
]
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", type = str, default = os.getcwd(), help = "Directory to store all results")
parser.add_argument("--vol3path",
type = str,
default = os.path.join(os.getcwd(), 'volatility3'),
help = "Path ot the volatility 3 directory")
parser.add_argument("--vol2path",
type = str,
default = os.path.join(os.getcwd(), 'volatility'),
help = "Path to the volatility 2 directory")
parser.add_argument("--rekallpath",
type = str,
default = os.path.join(os.getcwd(), 'rekall'),
help = "Path to the rekall directory")
parser.add_argument("--frameworks",
nargs = "+",
type = str,
choices = [x.short_name.lower() for x in VolatilityTest.__subclasses__()],
default = [x.short_name.lower() for x in VolatilityTest.__subclasses__()],
help = "A comma separated list of frameworks to test")
parser.add_argument('images', metavar = 'IMAGE', type = str, nargs = '+', help = 'The list of images to compare')
parser.add_argument(
"--output-dir",
type=str,
default=os.getcwd(),
help="Directory to store all results",
)
parser.add_argument(
"--vol3path",
type=str,
default=os.path.join(os.getcwd(), "volatility3"),
help="Path ot the volatility 3 directory",
)
parser.add_argument(
"--vol2path",
type=str,
default=os.path.join(os.getcwd(), "volatility"),
help="Path to the volatility 2 directory",
)
parser.add_argument(
"--rekallpath",
type=str,
default=os.path.join(os.getcwd(), "rekall"),
help="Path to the rekall directory",
)
parser.add_argument(
"--frameworks",
nargs="+",
type=str,
choices=[x.short_name.lower() for x in VolatilityTest.__subclasses__()],
default=[x.short_name.lower() for x in VolatilityTest.__subclasses__()],
help="A comma separated list of frameworks to test",
)
parser.add_argument(
"images",
metavar="IMAGE",
type=str,
nargs="+",
help="The list of images to compare",
)
args = parser.parse_args()
vt = VolatilityTester([VolatilityImage(filepath = x) for x in args.images], plugins,
[x.lower() for x in args.frameworks], args.output_dir, args.vol2path, args.vol3path,
args.rekallpath)
vt = VolatilityTester(
[VolatilityImage(filepath=x) for x in args.images],
plugins,
[x.lower() for x in args.frameworks],
args.output_dir,
args.vol2path,
args.vol3path,
args.rekallpath,
)
vt.run_tests()
+27 -24
View File
@@ -7,11 +7,12 @@
# Cleaned up C version (as the basis for my code) here, thanks to Pepijn Bruienne / @bruienne
# https://gist.github.com/bruienne/029494bbcfb358098b41
import os
import struct
import sys
def seekread(f, offset = None, length = 0, relative = True):
def seekread(f, offset=None, length=0, relative=True):
if offset is not None:
# offset provided, let's seek
f.seek(offset, [0, 1, 2][relative])
@@ -22,55 +23,57 @@ def seekread(f, offset = None, length = 0, relative = True):
def parse_pbzx(pbzx_path):
section = 0
xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section)
with open(pbzx_path, 'rb') as f:
xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz"
with open(pbzx_path, "rb") as f:
# pbzx = f.read()
# f.close()
magic = seekread(f, length = 4)
if magic != 'pbzx':
magic = seekread(f, length=4)
if magic != "pbzx":
raise RuntimeError("Error: Not a pbzx file")
# Read 8 bytes for initial flags
flags = seekread(f, length = 8)
flags = seekread(f, length=8)
# Interpret the flags as a 64-bit big-endian unsigned int
flags = struct.unpack('>Q', flags)[0]
flags = struct.unpack(">Q", flags)[0]
while flags & (1 << 24):
with open(xar_out_path, 'wb') as xar_f:
with open(xar_out_path, "wb") as xar_f:
xar_f.seek(0, os.SEEK_END)
# Read in more flags
flags = seekread(f, length = 8)
flags = struct.unpack('>Q', flags)[0]
flags = seekread(f, length=8)
flags = struct.unpack(">Q", flags)[0]
# Read in length
f_length = seekread(f, length = 8)
f_length = struct.unpack('>Q', f_length)[0]
xzmagic = seekread(f, length = 6)
if xzmagic != '\xfd7zXZ\x00':
f_length = seekread(f, length=8)
f_length = struct.unpack(">Q", f_length)[0]
xzmagic = seekread(f, length=6)
if xzmagic != "\xfd7zXZ\x00":
# This isn't xz content, this is actually _raw decompressed cpio_ chunk of 16MB in size...
# Let's back up ...
seekread(f, offset = -6, length = 0)
seekread(f, offset=-6, length=0)
# ... and split it out ...
f_content = seekread(f, length = f_length)
f_content = seekread(f, length=f_length)
section += 1
decomp_out = '%s.part%02d.cpio' % (pbzx_path, section)
with open(decomp_out, 'wb') as g:
decomp_out = f"{pbzx_path}.part{section:02d}.cpio"
with open(decomp_out, "wb") as g:
g.write(f_content)
# Now to start the next section, which should hopefully be .xz (we'll just assume it is ...)
section += 1
xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section)
xar_out_path = f"{pbzx_path}.part{section:02d}.cpio.xz"
else:
f_length -= 6
# This part needs buffering
f_content = seekread(f, length = f_length)
tail = seekread(f, offset = -2, length = 2)
f_content = seekread(f, length=f_length)
tail = seekread(f, offset=-2, length=2)
xar_f.write(xzmagic)
xar_f.write(f_content)
if tail != 'YZ':
if tail != "YZ":
raise RuntimeError("Error: Footer is not xar file footer")
def main():
parse_pbzx(sys.argv[1])
print("Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file")
print(
"Now xz decompress the .xz chunks, then 'cat' them all together in order into a single new.cpio file"
)
if __name__ == '__main__':
if __name__ == "__main__":
main()
+122 -75
View File
@@ -13,10 +13,10 @@ import pdbparse.undecorate
logger = logging.getLogger(__name__)
logger.setLevel(1)
if __name__ == '__main__':
if __name__ == "__main__":
console = logging.StreamHandler()
console.setLevel(1)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
logger.addHandler(console)
@@ -25,19 +25,19 @@ 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'])
for sym_url in ['http://msdl.microsoft.com/download/symbols']:
file_name = ".".join(file_name.split(".")[:-1] + ["pdb"])
for sym_url in ["http://msdl.microsoft.com/download/symbols"]:
url = sym_url + f"/{file_name}/{guid}/"
result = None
for suffix in [file_name[:-1] + '_', file_name]:
for suffix in [file_name[:-1] + "_", file_name]:
try:
logger.debug(f"Attempting to retrieve {url + suffix}")
logger.debug("Attempting to retrieve %s", url + suffix)
result, _ = request.urlretrieve(url + suffix)
except request.HTTPError as excp:
logger.debug(f"Failed with {excp}")
logger.debug("Failed with %s", excp)
if result:
logger.debug(f"Successfully written to {result}")
logger.debug("Successfully written to %s", result)
break
return result
@@ -69,7 +69,7 @@ class PDBConvertor:
"float": "float",
"double": "float",
"long double": "float",
"void": "void"
"void": "void",
}
base_type_size = {
@@ -122,13 +122,18 @@ class PDBConvertor:
self._seen_ctypes.add(ctype)
return self.ctype[ctype]
def lookup_ctype_pointers(self, ctype_pointer: str) -> Dict[str, Union[str, Dict[str, str]]]:
base_type = ctype_pointer.replace('32P', '').replace('64P', '')
def lookup_ctype_pointers(
self, ctype_pointer: str
) -> Dict[str, Union[str, Dict[str, str]]]:
base_type = ctype_pointer.replace("32P", "").replace("64P", "")
if base_type == ctype_pointer:
# We raise a KeyError, because we've been asked about a type that isn't a pointer
raise KeyError
self._seen_ctypes.add(base_type)
return {"kind": "pointer", "subtype": {"kind": "base", "name": self.ctype[base_type]}}
return {
"kind": "pointer",
"subtype": {"kind": "base", "name": self.ctype[base_type]},
}
def read_pdb(self) -> Dict:
"""Reads in the PDB file and forms essentially a python dictionary of necessary data"""
@@ -137,32 +142,31 @@ class PDBConvertor:
"enums": self.read_enums(),
"metadata": self.generate_metadata(),
"symbols": self.read_symbols(),
"base_types": self.read_basetypes()
"base_types": self.read_basetypes(),
}
return output
def generate_metadata(self) -> Dict[str, Any]:
"""Generates the metadata necessary for this object"""
dbg = self._pdb.STREAM_DBI
last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), 'ascii')[-16:]
guidstr = u'{:08x}{:04x}{:04x}{}'.format(self._pdb.STREAM_PDB.GUID.Data1, self._pdb.STREAM_PDB.GUID.Data2,
self._pdb.STREAM_PDB.GUID.Data3, last_bytes)
last_bytes = str(binascii.hexlify(self._pdb.STREAM_PDB.GUID.Data4), "ascii")[
-16:
]
guidstr = f"{self._pdb.STREAM_PDB.GUID.Data1:08x}{self._pdb.STREAM_PDB.GUID.Data2:04x}{self._pdb.STREAM_PDB.GUID.Data3:04x}{last_bytes}"
pdb_data = {
"GUID": guidstr.upper(),
"age": self._pdb.STREAM_PDB.Age,
"database": "ntkrnlmp.pdb",
"machine_type": int(dbg.machine)
"machine_type": int(dbg.machine),
}
result = {
"format": "6.0.0",
"producer": {
"datetime": datetime.datetime.now().isoformat(),
"name": "pdbconv",
"version": "0.1.0"
"version": "0.1.0",
},
"windows": {
"pdb": pdb_data
}
"windows": {"pdb": pdb_data},
}
return result
@@ -173,16 +177,21 @@ class PDBConvertor:
stream = self._pdb.STREAM_TPI
for type_index in stream.types:
user_type = stream.types[type_index]
if (user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref):
if user_type.leaf_type == "LF_ENUM" and not user_type.prop.fwdref:
output.update(self._format_enum(user_type))
return output
def _format_enum(self, user_enum):
output = {
user_enum.name: {
'base': self.lookup_ctype(user_enum.utype),
'size': self._determine_size(user_enum.utype),
'constants': dict([(enum.name, enum.enum_value) for enum in user_enum.fieldlist.substructs])
"base": self.lookup_ctype(user_enum.utype),
"size": self._determine_size(user_enum.utype),
"constants": dict(
[
(enum.name, enum.enum_value)
for enum in user_enum.fieldlist.substructs
]
),
}
}
return output
@@ -195,14 +204,14 @@ class PDBConvertor:
try:
sects = self._pdb.STREAM_SECT_HDR_ORIG.sections
omap = self._pdb.STREAM_OMAP_FROM_SRC
except AttributeError as e:
except AttributeError:
# In this case there is no OMAP, so we use the given section
# headers and use the identity function for omap.remap
sects = self._pdb.STREAM_SECT_HDR.sections
omap = None
for sym in self._pdb.STREAM_GSYM.globals:
if not hasattr(sym, 'offset'):
if not hasattr(sym, "offset"):
continue
try:
virt_base = sects[sym.segment - 1].VirtualAddress
@@ -223,9 +232,9 @@ class PDBConvertor:
stream = self._pdb.STREAM_TPI
for type_index in stream.types:
user_type = stream.types[type_index]
if (user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref):
if user_type.leaf_type == "LF_STRUCTURE" and not user_type.prop.fwdref:
output.update(self._format_usertype(user_type, "struct"))
elif (user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref):
elif user_type.leaf_type == "LF_UNION" and not user_type.prop.fwdref:
output.update(self._format_usertype(user_type, "union"))
return output
@@ -233,16 +242,22 @@ class PDBConvertor:
"""Produces a single usertype"""
fields: Dict[str, Dict[str, Any]] = {}
[fields.update(self._format_field(s)) for s in usertype.fieldlist.substructs]
return {usertype.name: {'fields': fields, 'kind': kind, 'size': usertype.size}}
return {usertype.name: {"fields": fields, "kind": kind, "size": usertype.size}}
def _format_field(self, field) -> Dict[str, Dict[str, Any]]:
return {field.name: {"offset": field.offset, "type": self._format_kind(field.index)}}
return {
field.name: {"offset": field.offset, "type": self._format_kind(field.index)}
}
def _determine_size(self, field):
output = None
if isinstance(field, str):
output = self.base_type_size[field]
elif (field.leaf_type == "LF_STRUCTURE" or field.leaf_type == "LF_ARRAY" or field.leaf_type == "LF_UNION"):
elif (
field.leaf_type == "LF_STRUCTURE"
or field.leaf_type == "LF_ARRAY"
or field.leaf_type == "LF_UNION"
):
output = field.size
elif field.leaf_type == "LF_POINTER":
output = self.base_type_size[field.ptr_attr.type]
@@ -256,6 +271,7 @@ class PDBConvertor:
output = self._determine_size(field.index)
if output is None:
import pdb
pdb.set_trace()
raise ValueError(f"Unknown size for field: {field.name}")
return output
@@ -267,36 +283,37 @@ class PDBConvertor:
output = self.lookup_ctype_pointers(kind)
except KeyError:
try:
output = {'kind': 'base', 'name': self.lookup_ctype(kind)}
output = {"kind": "base", "name": self.lookup_ctype(kind)}
except KeyError:
output = {'kind': 'base', 'name': kind}
elif kind.leaf_type == 'LF_MODIFIER':
output = {"kind": "base", "name": kind}
elif kind.leaf_type == "LF_MODIFIER":
output = self._format_kind(kind.modified_type)
elif kind.leaf_type == 'LF_STRUCTURE':
output = {'kind': 'struct', 'name': kind.name}
elif kind.leaf_type == 'LF_UNION':
output = {'kind': 'union', 'name': kind.name}
elif kind.leaf_type == 'LF_BITFIELD':
elif kind.leaf_type == "LF_STRUCTURE":
output = {"kind": "struct", "name": kind.name}
elif kind.leaf_type == "LF_UNION":
output = {"kind": "union", "name": kind.name}
elif kind.leaf_type == "LF_BITFIELD":
output = {
'kind': 'bitfield',
'type': self._format_kind(kind.base_type),
'bit_length': kind.length,
'bit_position': kind.position
"kind": "bitfield",
"type": self._format_kind(kind.base_type),
"bit_length": kind.length,
"bit_position": kind.position,
}
elif kind.leaf_type == 'LF_POINTER':
output = {'kind': 'pointer', 'subtype': self._format_kind(kind.utype)}
elif kind.leaf_type == 'LF_ARRAY':
elif kind.leaf_type == "LF_POINTER":
output = {"kind": "pointer", "subtype": self._format_kind(kind.utype)}
elif kind.leaf_type == "LF_ARRAY":
output = {
'kind': 'array',
'count': kind.size // self._determine_size(kind.element_type),
'subtype': self._format_kind(kind.element_type)
"kind": "array",
"count": kind.size // self._determine_size(kind.element_type),
"subtype": self._format_kind(kind.element_type),
}
elif kind.leaf_type == 'LF_ENUM':
output = {'kind': 'enum', 'name': kind.name}
elif kind.leaf_type == 'LF_PROCEDURE':
output = {'kind': "function"}
elif kind.leaf_type == "LF_ENUM":
output = {"kind": "enum", "name": kind.name}
elif kind.leaf_type == "LF_PROCEDURE":
output = {"kind": "function"}
else:
import pdb
pdb.set_trace()
return output
@@ -306,40 +323,70 @@ class PDBConvertor:
if "64" in self._pdb.STREAM_DBI.machine:
ptr_size = 8
output = {"pointer": {"endian": "little", "kind": "int", "signed": False, "size": ptr_size}}
output = {
"pointer": {
"endian": "little",
"kind": "int",
"signed": False,
"size": ptr_size,
}
}
for index in self._seen_ctypes:
output[self.ctype[index]] = {
"endian": "little",
"kind": self.ctype_python_types.get(self.ctype[index], "int"),
"signed": False if "_U" in index else True,
"size": self.base_type_size[index]
"size": self.base_type_size[index],
}
return output
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = "Convertor for PDB files to Volatility 3 Intermediate Symbol Format")
parser.add_argument("-o", "--output", metavar = "OUTPUT", help = "Filename for data output", required = True)
file_group = parser.add_argument_group("file", description = "File-based conversion of PDB to ISF")
file_group.add_argument("-f", "--file", metavar = "FILE", help = "PDB file to translate to ISF")
data_group = parser.add_argument_group("data", description = "Convert based on a GUID and filename pattern")
data_group.add_argument("-p", "--pattern", metavar = "PATTERN", help = "Filename pattern to recover PDB file")
data_group.add_argument("-g",
"--guid",
metavar = "GUID",
help = "GUID + Age string for the required PDB file",
default = None)
data_group.add_argument("-k",
"--keep",
action = "store_true",
default = False,
help = "Keep the downloaded PDB file")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Convertor for PDB files to Volatility 3 Intermediate Symbol Format"
)
parser.add_argument(
"-o",
"--output",
metavar="OUTPUT",
help="Filename for data output",
required=True,
)
file_group = parser.add_argument_group(
"file", description="File-based conversion of PDB to ISF"
)
file_group.add_argument(
"-f", "--file", metavar="FILE", help="PDB file to translate to ISF"
)
data_group = parser.add_argument_group(
"data", description="Convert based on a GUID and filename pattern"
)
data_group.add_argument(
"-p",
"--pattern",
metavar="PATTERN",
help="Filename pattern to recover PDB file",
)
data_group.add_argument(
"-g",
"--guid",
metavar="GUID",
help="GUID + Age string for the required PDB file",
default=None,
)
data_group.add_argument(
"-k",
"--keep",
action="store_true",
default=False,
help="Keep the downloaded PDB file",
)
args = parser.parse_args()
delfile = False
filename = None
if args.guid is not None and args.pattern is not None:
filename = PDBRetreiver().retreive_pdb(guid = args.guid, file_name = args.pattern)
filename = PDBRetreiver().retreive_pdb(guid=args.guid, file_name=args.pattern)
delfile = True
elif args.file:
filename = args.file
@@ -352,7 +399,7 @@ if __name__ == '__main__':
convertor = PDBConvertor(filename)
with open(args.output, "w") as f:
json.dump(convertor.read_pdb(), f, indent = 2, sort_keys = True)
json.dump(convertor.read_pdb(), f, indent=2, sort_keys=True)
if args.keep:
print(f"Temporary PDB file: {filename}")
+8 -9
View File
@@ -1,34 +1,33 @@
import argparse
import json
import logging
import os
import sys
# TODO: Rather nasty hack, when volatility's actually installed this would be unnecessary
sys.path += ".."
import logging
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
logger = logging.getLogger("")
logger.addHandler(console)
logger.setLevel(logging.DEBUG)
from volatility3 import schemas
from volatility3 import schemas # noqa: E402
if __name__ == '__main__':
if __name__ == "__main__":
parser = argparse.ArgumentParser("Validates ")
parser.add_argument("-s", "--schema", dest = "schema", default = None)
parser.add_argument("filenames", metavar = "FILE", nargs = '+')
parser.add_argument("-s", "--schema", dest="schema", default=None)
parser.add_argument("filenames", metavar="FILE", nargs="+")
args = parser.parse_args()
schema = None
if args.schema:
with open(os.path.abspath(args.schema), 'r') as s:
with open(os.path.abspath(args.schema)) as s:
schema = json.load(s)
failures = []
@@ -36,7 +35,7 @@ if __name__ == '__main__':
try:
if os.path.exists(filename):
print(f"[?] Validating file: {filename}")
with open(filename, 'r') as t:
with open(filename) as t:
test = json.load(t)
if args.schema:
+56 -42
View File
@@ -9,7 +9,7 @@ import requests
import rpmfile
from debian import debfile
DWARF2JSON = './dwarf2json'
DWARF2JSON = "./dwarf2json"
class Downloader:
@@ -17,7 +17,7 @@ class Downloader:
def __init__(self, url_lists: List[List[str]]) -> None:
self.url_lists = url_lists
def download_lists(self, keep = False):
def download_lists(self, keep=False):
for url_list in self.url_lists:
print("Downloading files...")
files_for_processing = self.download_list(url_list)
@@ -35,43 +35,45 @@ class Downloader:
with tempfile.NamedTemporaryFile() as archivedata:
archivedata.write(data.content)
archivedata.seek(0)
if url.endswith('.rpm'):
if url.endswith(".rpm"):
processed_files[url] = self.process_rpm(archivedata)
elif url.endswith('.deb'):
elif url.endswith(".deb"):
processed_files[url] = self.process_deb(archivedata)
return processed_files
def process_rpm(self, archivedata) -> Optional[str]:
rpm = rpmfile.RPMFile(fileobj = archivedata)
rpm = rpmfile.RPMFile(fileobj=archivedata)
member = None
extracted = None
for member in rpm.getmembers():
if 'vmlinux' in member.name or 'System.map' in member.name:
if "vmlinux" in member.name or "System.map" in member.name:
print(f" - Extracting {member.name}")
extracted = rpm.extractfile(member)
break
if not member or not extracted:
return None
with tempfile.NamedTemporaryFile(delete = False,
prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output:
with tempfile.NamedTemporaryFile(
delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map"
) as output:
print(f" - Writing to {output.name}")
output.write(extracted.read())
return output.name
def process_deb(self, archivedata) -> Optional[str]:
deb = debfile.DebFile(fileobj = archivedata)
deb = debfile.DebFile(fileobj=archivedata)
member = None
extracted = None
for member in deb.data.tgz().getmembers():
if member.name.endswith('vmlinux') or 'System.map' in member.name:
if member.name.endswith("vmlinux") or "System.map" in member.name:
print(f" - Extracting {member.name}")
extracted = deb.data.get_file(member.name)
break
if not member or not extracted:
return None
with tempfile.NamedTemporaryFile(delete = False,
prefix = 'vmlinux' if 'vmlinux' in member.name else 'System.map') as output:
with tempfile.NamedTemporaryFile(
delete=False, prefix="vmlinux" if "vmlinux" in member.name else "System.map"
) as output:
print(f" - Writing to {output.name}")
output.write(extracted.read())
return output.name
@@ -83,43 +85,55 @@ class Downloader:
if named_files[i] is None:
print(f"FAILURE: None encountered for {i}")
return
args = [DWARF2JSON, 'linux']
output_filename = 'unknown-kernel.json'
args = [DWARF2JSON, "linux"]
output_filename = "unknown-kernel.json"
for named_file in named_files:
prefix = '--system-map'
if 'System' not in named_files[named_file]:
prefix = '--elf'
output_filename = './' + '-'.join((named_file.split('/')[-1]).split('-')[2:])[:-4] + '.json.xz'
prefix = "--system-map"
if "System" not in named_files[named_file]:
prefix = "--elf"
output_filename = (
"./"
+ "-".join((named_file.split("/")[-1]).split("-")[2:])[:-4]
+ ".json.xz"
)
args += [prefix, named_files[named_file]]
print(f" - Running {args}")
proc = subprocess.run(args, capture_output = True)
proc = subprocess.run(args, capture_output=True)
print(f" - Writing to {output_filename}")
with lzma.open(output_filename, 'w') as f:
with lzma.open(output_filename, "w") as f:
f.write(proc.stdout)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description = "Takes a list of URLs for Centos and downloads them")
parser.add_argument("-f",
"--file",
dest = 'filename',
metavar = "FILENAME",
help = "Filename to be read",
required = True)
parser.add_argument("-d",
"--dwarf2json",
dest = 'dwarfpath',
metavar = "PATH",
default = DWARF2JSON,
help = "Path to the dwarf2json binary",
required = True)
parser.add_argument("-k",
"--keep",
dest = 'keep',
action = 'store_true',
help = 'Keep extracted temporary files after completion',
default = False)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Takes a list of URLs for Centos and downloads them"
)
parser.add_argument(
"-f",
"--file",
dest="filename",
metavar="FILENAME",
help="Filename to be read",
required=True,
)
parser.add_argument(
"-d",
"--dwarf2json",
dest="dwarfpath",
metavar="PATH",
default=DWARF2JSON,
help="Path to the dwarf2json binary",
required=True,
)
parser.add_argument(
"-k",
"--keep",
dest="keep",
action="store_true",
help="Keep extracted temporary files after completion",
default=False,
)
args = parser.parse_args()
DWARF2JSON = args.dwarfpath
@@ -132,4 +146,4 @@ if __name__ == '__main__':
urls += [[lines[2 * i].strip(), lines[(2 * i) + 1].strip()]]
d = Downloader(urls)
d.download_lists(keep = args.keep)
d.download_lists(keep=args.keep)
-8
View File
@@ -1,8 +0,0 @@
# These packages are required for building the documentation.
sphinx>=4.0.0,<7
sphinx_autodoc_typehints>=1.4.0
sphinx-rtd-theme>=0.4.3
yara-python
pycryptodome
pefile
+7 -8
View File
@@ -14,7 +14,7 @@ Memory layers
-------------
A memory layer is a body of data that can be accessed by requesting data at a specific address. At its lowest level
this data is stored on a phyiscal medium (RAM) and very early computers addresses locations in memory directly. However,
this data is stored on a phyiscal medium (RAM) and very early computers addressed locations in memory directly. However,
as the size of memory increased and it became more difficult to manage memory most architectures moved to a "paged" model
of memory, where the available memory is cut into specific fixed-sized pages. To help further, programs can ask for any address
and the processor will look up their (virtual) address in a map, to find out where the (physical) address that it lives at is,
@@ -25,8 +25,8 @@ address `9`). The automagic that runs at the start of every volatility session
a kernel virtual layer, which allows for kernel addresses to be looked up and the correct data returned. There can, however, be
several maps, and in general there is a different map for each process (although a portion of the operating system's memory is
usually mapped to the same location across all processes). The maps may take the same address but point to a different part of
physical memory. It also means that two processes could theoretically share memory, but having an virtual address mapped to the
same physical address as another process. See the worked example below for more information.
physical memory. It also means that two processes could theoretically share memory, both having a virtual address mapped to the
same physical address. See the worked example below for more information.
To translate an address on a layer, call :py:meth:`layer.mapping(offset, length, ignore_errors) <volatility3.framework.interfaces.layers.TranslationLayerInterface.mapping>` and it will return a list of chunks without overlap, in order,
for the requested range. If a portion cannot be mapped, an exception will be thrown unless `ignore_errors` is true. Each
@@ -61,7 +61,7 @@ mean they each see something different:
4 -> 2 16 - Free
In this example, part of the operating system is visible across all processes (although not all processes can write to the memory, there
is a permissions model for intel addressing which is not discussed further here).)
is a permissions model for Intel addressing which is not discussed further here).
In Volatility 3 mappings are represented by a directed graph of layers, whose end nodes are
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are :py:class:`TranslationLayers <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
@@ -69,13 +69,13 @@ In this way, a raw memory image in the LiME file format and a page file can be c
memory layer. When requesting addresses from the Intel layer, it will use the Intel memory mapping algorithm, along
with the address of the directory table base or page table map, to translate that
address into a physical address, which will then either be directed towards the swap layer or the LiME layer. Should it
be directed towards the LiME layer, the LiME file format algorithm will be translate the new address to determine where
be directed towards the LiME layer, the LiME file format algorithm will translate the new address to determine where
within the file the data is stored. When the :py:meth:`layer.read() <volatility3.framework.interfaces.layers.TranslationLayerInterface.read>`
method is called, the translation is done automatically and the correct data gathered and combined.
.. note:: Volatility 2 had a similar concept, called address spaces, but these could only stack linearly one on top of another.
The list of layers supported by volatility can be determined by running the `frameworkinfo` plugin.
The list of layers supported by Volatility can be determined by running the `frameworkinfo` plugin.
Templates and Objects
---------------------
@@ -167,8 +167,7 @@ There are certain setup tasks that establish the context in a way favorable to a
several tasks that are repetitive and also easy to get wrong. These are called
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutomagicInterface>`, since they do things like magically
taking a raw memory image and automatically providing the plugin with an appropriate Intel translation layer and an
accurate symbol table without either the plugin or the calling program having to specify all the necessary details.
accurate symbol table without either the plugin or the calling program having to specify all the necessary details. Automagics are a core component which consumers of the library can call or not at their discretion.
.. note:: Volatility 2 used to do this as well, but it wasn't a particularly modular mechanism, and was used only for
stacking address spaces (rather than identifying profiles), and it couldn't really be disabled/configured easily.
Automagics in Volatility 3 are a core component which consumers of the library can call or not at their discretion.
+6 -8
View File
@@ -19,6 +19,8 @@ import sys
import sphinx.ext.apidoc
from importlib.util import find_spec
def setup(app):
volatility_directory = os.path.abspath(
@@ -124,7 +126,7 @@ def setup(app):
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("../.."))
from volatility3.framework import constants
from volatility3.framework import constants # noqa: E402
# -- General configuration ------------------------------------------------
@@ -147,13 +149,9 @@ extensions = [
autosectionlabel_prefix_document = True
try:
import sphinx_autodoc_typehints
if find_spec("sphinx_autodoc_typehints") is not None:
extensions.append("sphinx_autodoc_typehints")
except ImportError:
# If the autodoc typehints extension isn't available, carry on regardless
pass
# If the autodoc typehints extension isn't available, carry on regardless
# Add any paths that contain templates here, relative to this directory.
# templates_path = ['tools/templates']
@@ -169,7 +167,7 @@ master_doc = "index"
# General information about the project.
project = "Volatility 3"
copyright = "2012-2022, Volatility Foundation"
copyright = "2012-2025, Volatility Foundation"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -11,6 +11,7 @@ Volatility3 does not provide the ability to acquire memory. Below are some exam
* `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>`_.
Procedure to create symbol tables for linux
--------------------------------------------
@@ -26,7 +27,7 @@ To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol
Listing plugins
---------------
The following is a sample of the linux plugins available for volatility3, it is not complete and more more plugins may
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.
@@ -39,7 +40,7 @@ For plugin requests, please create an issue with a description of the requested
linux.check_creds.Check_creds
linux.check_idt.Check_idt
.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of linux plugins.
.. note:: Here the the command is piped to grep and head to provide the start of the list of linux plugins.
Using plugins
@@ -79,9 +80,9 @@ Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memo
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 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.
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.
.. tip:: Use the banner text which is most repeated to search from ISF Server.
.. tip:: Use the banner text which is most repeated to search on the ISF Server.
linux.pslist
~~~~~~~~~~~~
@@ -156,7 +157,7 @@ linux.pstree
***** 1548 1266 gsd-keyboard
***** 1550 1266 gsd-media-keys
``linux.pstree`` helps us to display the parent child relationships between processes.
``linux.pstree`` helps us to display the parent-child relationships between processes.
linux.bash
~~~~~~~~~~
+4 -4
View File
@@ -37,7 +37,7 @@ For plugin requests, please create an issue with a description of the requested
mac.check_sysctl.Check_sysctl
mac.check_trap_table.Check_trap_table
.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of macOS plugins.
.. note:: Here the the command is piped to grep and head to provide the start of the list of macOS plugins.
Using plugins
@@ -78,7 +78,7 @@ Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memo
The above command helps us to find the memory dump's Darwin kernel version. Now using the above banner we can search for the needed ISF file.
If ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory.
If an ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory.
mac.pslist
~~~~~~~~~~
@@ -125,7 +125,7 @@ mac.pstree
337 1 system_installd
* 455 337 update_dyld_shar
``mac.pstree`` helps us to display the parent child relationships between processes.
``mac.pstree`` helps us to display the parent-child relationships between processes.
mac.ifconfig
~~~~~~~~~~~~
@@ -150,4 +150,4 @@ mac.ifconfig
utun0 False
utun0 fe80:5::2a95:bb15:87e3:977c False
we can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation.
We can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation.
@@ -15,19 +15,19 @@ Memory can be acquired using a number of tools, below are some examples but othe
Listing Plugins
---------------
The following is a sample of the windows plugins available for volatility3, it is not complete and more more plugins may
The following is a sample of the windows 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.
.. code-block:: shell-session
$ python3 vol.py --help | grep windows | head -n 5
$ python3 vol.py --help | grep windows | head -n 4
windows.bigpools.BigPools
windows.cmdline.CmdLine
windows.crashinfo.Crashinfo
windows.dlllist.DllList
.. note:: Here the the command is piped to grep and head in-order to provide the start of a list of the available windows plugins.
.. note:: Here the the command is piped to grep and head to provide the start of a list of the available windows plugins.
Using plugins
-------------
@@ -95,9 +95,9 @@ windows.pstree
** 616 504 svchost.exe 0xfa8002b86ab0 13 314 0 False 2022-02-07 16:32:16.000000 N/A
** 624 504 svchost.exe 0xfa8002410630 10 350 0 False 2022-02-07 16:30:14.000000 N/A
``windows.pstree`` helps to display the parent child relationships between processes.
``windows.pstree`` helps to display the parent-child relationships between processes.
.. note:: Here the the command is piped to head in-order to provide smaller output, here listing only the first 20.
.. note:: Here the the command is piped to head to provide smaller output, here listing only the first 20.
windows.hashdump
~~~~~~~~~~~~~~~~
@@ -116,9 +116,3 @@ windows.hashdump
Dennis 1003 aad3b435b51404eeaad3b435b51404ee cf96684bbc7877920adaa9663698bf54
``windows.hashdump`` helps to list the hashes of the users in the system.
+18 -8
View File
@@ -23,7 +23,7 @@ Alignment
.. _Array:
Array
This represents a list of items, which can be access by an index, which is zero-based (meaning the first
This represents a list of items, which can be accessed by an index, which is zero-based (meaning the first
element has index 0). Items in arrays are almost always the same size (it is not a generic list, as in python)
even if they are :ref:`pointers<pointer>` to different sized objects.
@@ -43,7 +43,14 @@ Dereference
.. _Domain:
Domain
This the grouping for input values for a mapping or mathematical function.
The set of input values for a mapping or mathematical function.
I
-
.. _Intermediate Symbol File (ISF):
Intermediate Symbol File (ISF)
They contain kernel structures and specific offsets formatted as JSON. For macOS and Linux analysis, the kernel needs to be added as an ISF file to the volatility 3 symbols directory. For Windows, the required ISF file can often be generated from PDB files automatically downloaded from Microsoft servers, and therefore does not require manual intervention.
M
-
@@ -54,9 +61,7 @@ Map, mapping
of the :ref:`Range<range>`). Mappings can be seen as a mathematical function, and therefore volatility 3
attempts to use mathematical functional notation where possible. Within volatility a mapping is most often
used to refer to the function for translating addresses from a higher layer (domain) to a lower layer (range).
For further information, please see
`Function (mathematics) in wikipedia https://en.wikipedia.org/wiki/Function_(mathematics)`
For further information, please see `Function (mathematics) in Wikipedia<https://en.wikipedia.org/wiki/Function_(mathematics)>_`.
.. _Member:
@@ -69,7 +74,7 @@ O
.. _Object:
Object
This has a specific meaning within computer programming (as in Object Oriented Programming), but within the world
This has a specific meaning within computer programming (as in object-oriented programming), but within the world
of Volatility it is used to refer to a type that has been associated with a chunk of data, or a specific instance
of a type. See also :ref:`Type<type>`.
@@ -116,6 +121,11 @@ Page Table
possible to use them as a way to map a particular address within a (potentially larger, but sparsely populated)
virtual space to a concrete (and usually contiguous) physical space, through the process of :ref:`mapping<map>`.
.. _Plugin:
Plugin
Plugins are the "functions" of the volatility framework. They carry out algorithms on data stored in layers using objects constructed from symbols. Broadly, plugins take in a number of TranslationLayers (the data, which is a representation of part of an image, in a specified type described by templates) and outputs a TreeGrid.
.. _Pointer:
Pointer
@@ -145,9 +155,9 @@ Struct, Structure
Symbol
This is used in many different contexts, as a short term for many things. Within Volatility, a symbol is a
construct that usually encompasses a specific type :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
construct that usually encompasses a specific :ref:`type<Type>` at a specific :ref:`offset<Offset>`,
representing a particular instance of that type within the memory of a compiled and running program. An example
would be the location in memory of a list of active tcp endpoints maintained by the networking stack
would be the location in memory of a list of active TCP endpoints maintained by the networking stack
within an operating system.
T
+62 -36
View File
@@ -41,24 +41,36 @@ to be able to run properly. Any that are defined as optional need not necessari
@classmethod
def get_requirements(cls):
return [requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ListRequirement(name = 'pid',
element_type = int,
description = "Process IDs to include (all other processes are excluded)",
optional = True),
requirements.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
return [
requirements.ModuleRequirement(
name = 'kernel',
description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]
),
requirements.ListRequirement(
name = 'pid',
element_type = int,
description = "Process IDs to include (all other processes are excluded)",
optional = True
),
requirements.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
),
]
This is a classmethod, because it is called before the specific plugin object has been instantiated (in order to know how
This is a classmethod, so it can be called before the specific plugin object has been instantiated (in order to know how
to instantiate the plugin). At the moment these requirements are fairly straightforward:
::
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name = 'kernel',
description = 'Windows kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement specifies the need for a particular submodule. Each module requires a
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a
@@ -85,9 +97,11 @@ not be requested directly from the user.
::
requirements.TranslationLayerRequirement(name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]),
requirements.TranslationLayerRequirement(
name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]
),
This requirement indicates that the plugin will operate on a single
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
@@ -110,8 +124,10 @@ not be requested directly from the user.
::
requirements.SymbolTableRequirement(name = "nt_symbols",
description = "Windows kernel symbols"),
requirements.SymbolTableRequirement(
name = "nt_symbols",
description = "Windows kernel symbols"
),
This requirement specifies the need for a particular
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
@@ -127,10 +143,12 @@ not be requested directly from the user.
::
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True),
requirements.ListRequirement(
name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True
),
The next requirement is a List Requirement, populated by integers. The description will be presented to the user to
describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value
@@ -138,9 +156,11 @@ being defined within the configuration tree at all.
::
requirements.PluginRequirement(name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0))]
requirements.PluginRequirement(
name = 'pslist',
plugin = pslist.PsList,
version = (2, 0, 0)
)
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major
@@ -180,16 +200,24 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces.
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
kernel = self.context.modules[self.config['kernel']]
return renderers.TreeGrid([("PID", int),
("Process", str),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str)],
self._generator(pslist.PsList.list_processes(self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Base", format_hints.Hex),
("Size", format_hints.Hex),
("Name", str),
("Path", str),
],
self._generator(
pslist.PsList.list_processes(
self.context,
kernel.layer_name,
kernel.symbol_table_name,
filter_func = filter_func
)
)
)
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
It checks the plugin's configuration for the ``pid`` value, and passes it in as a list if it finds it, or None if
@@ -281,5 +309,3 @@ such as ``<table>!_UNICODE``) and the parameters to that type.
Since the cast value must populate a string typed column, it had to be a Python string (such as being cast to the native
type string) and could not have been a special Structure such as ``_UNICODE``. For the format hint columns, the format
hint type must be used to ensure the error checking does not fail.
+8 -8
View File
@@ -9,7 +9,7 @@ How Volatility finds symbol tables
All files are stored as JSON data, they can be in pure JSON files as ``.json``, or compressed as ``.json.gz`` or ``.json.xz``.
Volatility will automatically decompress them on use. It will also cache their contents (compressed) when used, located
under the user's home directory, in :file:`.cache/volatility3`, along with other useful data. The cache directory currently
under the user's home directory, in :file:`.cache/volatility3` or when `XDG_CACHE_HOME` is set in :file:`${XDG_CACHE_HOME}/volatility3`, along with other useful data. The cache directory currently
cannot be altered.
Symbol table JSON files live, by default, under the :file:`volatility3/symbols` directory. The symbols directory is
@@ -25,9 +25,9 @@ as long as the symbol files stay in the same location.
Windows symbol tables
---------------------
For Windows systems, Volatility accepts a string made up of the GUID and Age of the required PDB file. It then
For Windows systems, Volatility accepts a string made up of the GUID and age of the required PDB file. It then
searches all files under the configured symbol directories under the windows subdirectory. Any that contain metadata
which matches the pdb name and GUID/age (or any compressed variant) will be used. If such a symbol table cannot be found, then
which matches the PDB name and GUID/age (or any compressed variant) will be used. If such a symbol table cannot be found, then
the associated PDB file will be downloaded from Microsoft's Symbol Server and converted into the appropriate JSON
format, and will be saved in the correct location.
@@ -54,8 +54,8 @@ most Volatility plugins. Note that in most linux distributions, the standard ke
and the kernel with debugging information is stored in a package that must be acquired separately.
A generic table isn't guaranteed to produce accurate results, and would reduce the number of structures
that all plugins could rely on. As such, and because linux kernels with different configurations can produce different structures,
volatility 3 requires that the banners in the JSON file match the banners found in the image *exactly*, not just the version
that all plugins could rely on. As such, and because Linux kernels with different configurations can produce different structures,
Volatility 3 requires that the banners in the JSON file match the banners found in the image *exactly*, not just the version
number. This can include elements such as the compilation time and even the version of gcc used for the compilation.
The exact match is required to ensure that the results volatility returns are accurate, therefore there is no simple means
provided to get the wrong JSON ISF file to easily match.
@@ -63,8 +63,8 @@ provided to get the wrong JSON ISF file to easily match.
To determine the string for a particular memory image, use the `banners` plugin. Once the specific banner is known,
try to locate that exact kernel debugging package for the operating system. Unfortunately each distribution provides
its debugging packages under different package names and there are so many that the distribution may not keep all old
versions of the debugging symbols, and therefore **it may not be possible to find the right symbols to analyze a linux
memory image with volatility**. With Macs there are far fewer kernels and only one distribution, making it easier to
versions of the debugging symbols, and therefore **it may not be possible to find the right symbols to analyze a Linux
memory image with Volatility**. With Macs there are far fewer kernels and only one distribution, making it easier to
ensure that the right symbols can be found.
Once a kernel with debugging symbols/appropriate DWARF file has been located, `dwarf2json <https://github.com/volatilityfoundation/dwarf2json>`_ will convert it into an
@@ -75,7 +75,7 @@ symbol offsets within the DWARF data, which dwarf2json can extract into the JSON
The banners available for volatility to use can be found using the `isfinfo` plugin, but this will potentially take a
long time to run depending on the number of JSON files available. This will list all the JSON (ISF) files that
volatility3 is aware of, and for linux/mac systems what banner string they search for. For volatility to use the JSON
Volatility 3 is aware of, and for linux/mac systems what banner string they search for. For volatility to use the JSON
file, the banners must match exactly (down to the compilation date).
.. note::
+8 -8
View File
@@ -3,7 +3,7 @@ Using Volatility 3 as a Library
This portion of the documentation discusses how to access the Volatility 3 framework from an external application.
The general process of using volatility as a library is to as follows:
The general process of using volatility as a library is as follows:
1. :ref:`create_context`
2. (Optional) :ref:`available_plugins`
@@ -21,7 +21,7 @@ Creating a context
First we make sure the volatility framework works the way we expect it (and is the version we expect). The
versioning used is semantic versioning, meaning any version with the same major number and a higher or equal
minor number will satisfy the requirement. An example is below since the CLI doesn't need any of the features
from versions 1.1 or 1.2:
from version 1.1 or later:
::
@@ -86,7 +86,7 @@ List requirements are a list of simple types (integers, booleans, floats and str
options, multiple requirements needs all their subrequirements fulfilled and the other types require the names of
valid translation layers or symbol tables within the context, respectively. Luckily, each of these requirements can
tell you whether they've been fulfilled or not later in the process. For now, they can be used to ask the user to
fill in any parameters they made need to. Some requirements are optional, others are not.
fill in any parameters they may need to. Some requirements are optional, others are not.
The plugin is essentially a multiple requirement. It should also be noted that automagic classes can have requirements
(as can translation layers).
@@ -100,7 +100,7 @@ Once you know what requirements the plugin will need, you can populate them with
The configuration is essentially a hierarchical tree of values, much like the windows registry.
Each plugin is instantiated at a particular branch within the hierarchy and will look for its configuration
options under that hierarchy (if it holds any configurable items, it will likely instantiate those at a point
underneaths its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed.
underneath its own branch). To set the hierarchy, you'll need to know where the configurables will be constructed.
For this example, we'll assume plugins' base_config_path is set as `plugins`, and that automagics are configured under
the `automagic` tree. We'll see later how to ensure this matches up with the plugins and automagic when they're
@@ -139,7 +139,7 @@ A suitable list of automagics for a particular plugin (based on operating system
This will take the plugin module, extract the operating system (first level of the hierarchy) and then return just
the automagics which apply to the operating system. Each automagic can exclude itself from being used for specific
operating systems, so that an automagic designed for linux is not used for windows or mac plugins.
operating systems, such that an automagic designed for linux is not used for windows or mac plugins.
These automagics can then be run by providing the list, the context, the plugin to be run, the hierarchy name that
the plugin will be constructed on ('plugins' by default) and a progress_callback. This is a callable which takes
@@ -157,8 +157,8 @@ Any exceptions that occur during the execution of the automagic will be returned
Run the plugin
--------------
Firstly, we should check whether the plugin will be able to run (ie, whether the configuration options it needs
have been successfully set). We do this as follow (where plugin_config_path is the base_config_path (which defaults
Firstly, we should check whether the plugin will be able to run (i.e., whether the configuration options it needs
have been successfully set). We do this as follows, where plugin_config_path is the base_config_path (which defaults
to `plugins` and then the name of the class itself):
::
@@ -166,7 +166,7 @@ to `plugins` and then the name of the class itself):
unsatisfied = plugin.unsatisfied(context, plugin_config_path)
If unsatisfied is an empty list, then the plugin has been given everything it requires. If not, it will be a
Dictionary of the hierarchy paths and their associated requirements that weren't satisfied.
dict of the hierarchy paths and their associated requirements that weren't satisfied.
The plugin can then be instantiated with the context (containing the plugin's configuration) and the path that the
plugin can find its configuration at. This configuration path only needs to be a unique value to identify where the
+21 -8
View File
@@ -58,7 +58,7 @@ Options
EXTEND. Extensions must be of the form **configuration.item.name=value**
-p PLUGIN_DIRS, --plugin-dirs PLUGIN_DIRS
Specified a semi-colon separated list of paths that contain directories
Specified as a semi-colon separated list of paths that contain directories
where plugins may be found. These paths are searched before the default
paths when loading python files for plugins. This can therefore be used
to override built-in plugins. NOTE: All python code within this directory
@@ -67,12 +67,12 @@ Options
-s SYMBOL_DIRS, --symbol-dirs SYMBOL_DIRS
SYMBOL_DIRS is a semi-colon separated list of paths that contain symbol
files or symbol zip packs. Symbols must be within a particular directory
structure if they depending on the operating system of the symbols,
structure if they depend on the operating system of the symbols,
whilst symbol packs must be in the root of the directory and named after
the after the operating system to which they apply.
the operating system to which they apply.
-v, --verbose
A flag which can be used multiple times, each time increasing the level of
A flag which can be used multiple times (up to six, -vvvvvv), each time increasing the level of
detail in the logs produced.
-l LOG, --log LOG
@@ -87,7 +87,7 @@ Options
-q, --quiet
When present, this flag mutes the progress feedback for operations. This
can be beneficial when piping the output directly to a file or another
tool. This also removes the
tool.
-r RENDERER, --renderer RENDERER
Specifies the output format in which to display results. The default is
@@ -120,9 +120,7 @@ Options
Change the default path used to store the cache.
--offline
Do not search online for additional JSON files.
Run offline mode (defaults to false) and for
remote windows symbol tables, linux/mac banner repositories.
Run offline mode (defaults to false). Do not search online for additional JSON files, remote windows symbol tables, nor linux/mac banner repositories.
--single-location SINGLE_LOCATION
This specifies a URL which will be downloaded if necessary, and built
@@ -143,3 +141,18 @@ Options
`hivescan` would match `windows.registry.hivescan.HiveScan`, but
`pslist` is ambiguous because it could match `windows.pslist` or
`linux.pslist`.
Overriding options
------------------
The default values for the command line interface are defined by constants within the code,
but can be overridden by creating a JSON file (`%APPDATA%/volatility3/vol.json` for Windows
systems, or `~/.config/volatility3/vol.json` or `volshell.json` for all others).
The format of this file is a JSON dictionary, containing the options above and their value.
It should be noted that the ordering is (`x < y` means `x` is overridden by `y`):
`in-built default value < config file value < command line parameter`
It should also be noted that boolean flags (such as `offline`) that are overridden as true will
not be unset by not specifying the command line flag.
+7 -8
View File
@@ -27,7 +27,7 @@ The object model has changed as well, objects now inherit directly from their Py
object is actually a Python integer (and has all the associated methods, and can be used wherever a normal int could).
In Volatility 2, a complex proxy object was constructed which tried to emulate all the methods of the host object, but
ultimately it was a different type and could not be used in the same places (critically, it could make the ordering of
operations important, since a + b might not work, but b + a might work fine).
operations important, since x + y might not work, but y + x might work fine).
Volatility 3 has also had significant speed improvements, where Volatility 2 was designed to allow access to live memory
images and situations in which the underlying data could change during the run of the plugin, in Volatility 3 the data
@@ -36,11 +36,11 @@ This was because live memory analysis was barely ever used, and this feature cou
re-read many times over for no benefit (particularly since each re-read could result in many additional image reads
from following page table translations).
Finally, in order to provide Volatility specific information without impact on the ability for structures to have members
Further, in order to provide Volatility specific information without impact on the ability for structures to have members
with arbitrary names, all the metadata about the object (such as its layer or offset) have been moved to a read-only :py:meth:`~volatility3.framework.interfaces.objects.ObjectInterface.vol`
dictionary.
Further the distinction between a :py:class:`~volatility3.framework.interfaces.objects.Template` (the thing that
Finally, the distinction between a :py:class:`~volatility3.framework.interfaces.objects.Template` (the thing that
constructs an object) and the :py:class:`Object <volatility3.framework.interfaces.objects.ObjectInterface>` itself has
been made more explicit. In Volatility 2, some information (such as size) could only be determined from a constructed object,
leading to instantiating a template on an empty buffer, just to determine the size. In Volatility 3, templates contain
@@ -56,15 +56,14 @@ Volatility 2 were strictly limited to a stack, one on top of one other. In Vola
Automagic
---------
In Volatility 2, we often tried to make this simpler for both users and developers. This resulted in something was
referred to as automagic, in that it was magic that happened automatically. We've now codified that more, so that the
In Volatility 2, we often tried to make this simpler for both users and developers. This resulted in something referred to as automagic, in that it was magic that happened automatically. We've now codified that more, so that the
automagic processes are clearly defined and can be enabled or disabled as necessary for any particular run. We also
included a stacker automagic to emulate the most common feature of Volatility 2, automatically stacking address spaces
(now translation layers) on top of each other.
By default the automagic chosen to be run are determined based on the plugin requested, so that linux plugins get linux
specific automagic and windows plugins get windows specific automagic. This should reduce unnecessarily searching for
linux kernels in a windows image, for example. At the moment this is not user configurableS.
By default the automagic chosen to be run are determined based on the plugin requested, so that Linux plugins get Linux
specific automagic and Windows plugins get Windows specific automagic. This should reduce unnecessarily searching for
Linux kernels in a Windows image, for example. At the moment this is not user configurable.
Searching and Scanning
----------------------
+59 -6
View File
@@ -36,7 +36,7 @@ operating system mode for volshell, and the current layer available for use.
(primary) >>>
Volshell itself in essentially a plugin, but an interactive one. As such, most values are accessed through `self`
Volshell itself is essentially a plugin, but an interactive one. As such, most values are accessed through `self`
although there is also a `context` object whenever a context must be provided.
The prompt for the tool will indicate the name of the current layer (which can be accessed as `self.current_layer`
@@ -92,7 +92,7 @@ It can also be provided with an object and will interpret the data for each in t
0x2e8 : UniqueProcessId symbol_table_name1!pointer 4
...
These values can be accessed directory as attributes
These values can be accessed directly as attributes
::
@@ -144,12 +144,12 @@ We can provide arguments via the `dpo` method call:
356 4 smss.exe 0x8c0bccf8d040 3 - N/A False 2021-03-13 17:25:33.000000 N/A Disabled
...
Here's we've provided the kernel name that was requested by the volshell plugin itself (the generic volshell does not
Here we've provided the kernel name that was requested by the volshell plugin itself (the generic volshell does not
load a kernel module, and instead only has a TranslationLayerRequirement).
A different module could be created and provided instead. The context used
by the `dpo` method is always `context`.
Instead of print the results directly to screen, they can be gathered into a TreeGrid objects for direct access by
Instead of printing the results directly to screen, they can be gathered into a TreeGrid objects for direct access by
using the `generate_treegrid` or `gt` command.
::
@@ -180,15 +180,68 @@ used:
layer = cc(mynewlayer.MyNewLayer, on_top_of = 'primary', other_parameter = 'important')
with open('output.dmp', 'wb') as fp:
for i in range(0, 1073741824, 0x1000):
for i in range(0, 0x4000000, 0x1000):
data = layer.read(i, 0x1000, pad = True)
fp.write(data)
As this demonstrates, all of the python is accessible, as are the volshell built in functions (such as `cc` which
creates a constructable, like a layer or a symbol table).
User Convenience
----------------
There are functions available that make often-done tasks easier, and generally provide a shell-like experience. These can be listed using `help()` which, as already mentioned, is advertised when volshell starts.
Loading files
-------------
^^^^^^^^^^^^^
Files can be loaded as physical layers using the `load_file` or `lf` command, which takes a filename or a URI. This will be added
to `context.layers` and can be accessed by the name returned by `lf`.
Regex
^^^^^
It is easy to scan for some bytes or a pattern using `regex_scan` or `rx`.
::
(layer_name) >>> rx(rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+")
0x880001400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x8800014000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x8800014000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x8800014000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x8800014000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x8800014000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0x880001769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0x880001769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0x880001769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0x880001769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0x880001769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0x880001769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0x880001769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0x880001769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81400070 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81400080 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81400090 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff814000a0 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff814000b0 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff814000c0 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff814000d0 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff814000e0 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
0xffff81769027 4c 69 6e 75 78 20 76 65 72 73 69 6f 6e 20 33 2e Linux.version.3.
0xffff81769037 32 2e 30 2d 34 2d 61 6d 64 36 34 20 28 64 65 62 2.0-4-amd64.(deb
0xffff81769047 69 61 6e 2d 6b 65 72 6e 65 6c 40 6c 69 73 74 73 ian-kernel@lists
0xffff81769057 2e 64 65 62 69 61 6e 2e 6f 72 67 29 20 28 67 63 .debian.org).(gc
0xffff81769067 63 20 76 65 72 73 69 6f 6e 20 34 2e 36 2e 33 20 c.version.4.6.3.
0xffff81769077 28 44 65 62 69 61 6e 20 34 2e 36 2e 33 2d 31 34 (Debian.4.6.3-14
0xffff81769087 29 20 29 20 23 31 20 53 4d 50 20 44 65 62 69 61 ).).#1.SMP.Debia
0xffff81769097 6e 20 33 2e 32 2e 35 37 2d 33 2b 64 65 62 37 75 n.3.2.57-3+deb7u
An optional size can be given for the displayed results as with the other fuctions (db, dw, dd, dq, etc).
You can, of course, specify a different layer name as well.
-4
View File
@@ -1,4 +0,0 @@
[mypy]
mypy_path = ./stubs
show_traceback = True
ignore_missing_imports = True
+99
View File
@@ -0,0 +1,99 @@
[project]
name = "volatility3"
description = "Memory forensics framework"
keywords = [
"volatility",
"memory",
"forensics",
"framework",
"windows",
"linux",
"volshell",
]
readme = "README.md"
authors = [
{ name = "Volatility Foundation", email = "volatility@volatilityfoundation.org" },
]
requires-python = ">=3.8.0"
license = { text = "VSL" }
dynamic = ["version"]
dependencies = ["pefile>=2024.8.26"]
[project.optional-dependencies]
full = [
"yara-python>=4.5.1,<5",
"capstone>=5.0.3,<6",
"pycryptodome>=3.21.0,<4",
"leechcorepyc>=2.19.2,<3; sys_platform != 'darwin'",
# https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst
# 10.0.0 dropped support for Python3.7
# 11.0.0 dropped support for Python3.8, which is still supported by Volatility3
"pillow>=10.0.0,<11.0.0",
]
cloud = ["gcsfs>=2024.10.0", "s3fs>=2024.10.0"]
dev = [
"volatility3[full,cloud]",
"jsonschema>=4.23.0,<5",
"pyinstaller>=6.5.0,<7",
"pyinstaller-hooks-contrib>=2024.9",
"types-jsonschema>=4.23.0,<5",
]
test = [
"volatility3[dev]",
"pytest>=8.3.3,<9",
"capstone>=5.0.3,<6",
"yara-x>=0.10.0,<1",
]
docs = [
"volatility3[dev]",
"sphinx>=4.0.0,<9",
"sphinx-autodoc-typehints>=2.0.0,<3",
"sphinx-rtd-theme>=3.0.1,<4",
]
[project.urls]
homepage = "https://github.com/volatilityfoundation/volatility3/"
documentation = "https://volatility3.readthedocs.io/"
repository = "https://github.com/volatilityfoundation/volatility3"
issues = "https://github.com/volatilityfoundation/volatility3/issues"
[project.scripts]
vol = "volatility3.cli:main"
volshell = "volatility3.cli.volshell:main"
[tool.setuptools.dynamic]
version = { attr = "volatility3.framework.constants._version.PACKAGE_VERSION" }
[tool.setuptools.packages.find]
include = ["volatility3*"]
[tool.mypy]
mypy_path = "./stubs"
show_traceback = true
[tool.ruff]
line-length = 88
target-version = "py38"
[tool.ruff.lint]
select = [
"F", # pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"G", # flake8-logging-format
"PIE", # flake8-pie
"UP", # pyupgrade
]
ignore = [
"E501", # ignore due to conflict with formatter
]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
-22
View File
@@ -1,22 +0,0 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
# This is required for the yara plugins
yara-python>=3.8.0
# This is required for several plugins that perform malware analysis and disassemble code.
# It can also improve accuracy of Windows 8 and later memory samples.
capstone>=3.0.5
# This is required by plugins that decrypt passwords, password hashes, etc.
pycryptodome
# This can improve error messages regarding improperly configured ISF files,
# but is only recommended for development
jsonschema>=2.3.0
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
-2
View File
@@ -1,2 +0,0 @@
# These packages are required for core functionality.
pefile>=2023.2.7 #foo
-22
View File
@@ -1,22 +0,0 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
# This is required for the yara plugins
yara-python>=3.8.0
# This is required for several plugins that perform malware analysis and disassemble code.
# It can also improve accuracy of Windows 8 and later memory samples.
capstone>=3.0.5
# This is required by plugins that decrypt passwords, password hashes, etc.
pycryptodome
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage
gcsfs>=2023.1.0
s3fs>=2023.1.0
-53
View File
@@ -1,53 +0,0 @@
# 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 setuptools
from volatility3.framework import constants
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
def get_install_requires():
requirements = []
with open("requirements-minimal.txt", "r", encoding="utf-8") as fh:
for line in fh.readlines():
stripped_line = line.strip()
if stripped_line == "" or stripped_line.startswith("#"):
continue
requirements.append(stripped_line)
return requirements
setuptools.setup(
name="volatility3",
description="Memory forensics framework",
version=constants.PACKAGE_VERSION,
license="VSL",
keywords="volatility memory forensics framework windows linux volshell",
author="Volatility Foundation",
long_description=long_description,
long_description_content_type="text/markdown",
author_email="volatility@volatilityfoundation.org",
url="https://github.com/volatilityfoundation/volatility3/",
project_urls={
"Bug Tracker": "https://github.com/volatilityfoundation/volatility3/issues",
"Documentation": "https://volatility3.readthedocs.io/",
"Source Code": "https://github.com/volatilityfoundation/volatility3",
},
packages=setuptools.find_namespace_packages(
include=["volatility3", "volatility3.*"]
),
package_dir={"volatility3": "volatility3"},
python_requires=">=3.7.0",
include_package_data=True,
entry_points={
"console_scripts": [
"vol = volatility3.cli:main",
"volshell = volatility3.cli.volshell:main",
],
},
install_requires=get_install_requires(),
)
+2 -4
View File
@@ -2,14 +2,12 @@
## Requirements
The Volatility 3 Testing Framework requires the same version of Python as Volatility3 itself. To install the current set of dependencies that the framework requires, use a command like this:
The Volatility 3 Testing Framework requires the same version of Python as Volatility 3 itself. To install the current set of dependencies that the framework requires, use a command like this:
```shell
pip3 install -r requirements-testing.txt
pip3 install -e .[test]
```
NOTE: `requirements-testing.txt` can be found in this current `test/` directory.
## Quick Start: Manual Testing
1. To test Volatility 3 on an image, first download one with a command such as:
View File
+28 -5
View File
@@ -35,16 +35,39 @@ def pytest_addoption(parser):
def pytest_generate_tests(metafunc):
"""Parameterize tests based on image names"""
images = metafunc.config.getoption("image")
images = metafunc.config.getoption("image").copy()
for image_dir in metafunc.config.getoption("image_dir"):
images = images + [
os.path.join(image_dir, dir) for dir in os.listdir(image_dir)
images += [
os.path.join(image_dir, dir_name) for dir_name in os.listdir(image_dir)
]
# tests with "image" parameter are run against images
# tests with "image" parameter are run against image
if "image" in metafunc.fixturenames:
filtered_images = []
ids = []
for image in images:
image_base = os.path.basename(image)
test_name = metafunc.definition.originalname
if test_name.startswith("test_windows_") and not image_base.startswith(
"win-"
):
continue
elif test_name.startswith("test_linux_") and not image_base.startswith(
"linux-"
):
continue
elif test_name.startswith("test_mac_") and not image_base.startswith(
"mac-"
):
continue
filtered_images.append(image)
ids.append(image_base)
metafunc.parametrize(
"image", images, ids=[os.path.basename(image) for image in images]
"image",
filtered_images,
ids=ids,
)
View File
View File
@@ -0,0 +1,388 @@
import sys
import struct
import traceback
import unittest
sys.path.insert(0, "../../volatility3")
from volatility3.plugins.windows import scheduled_tasks
class TestActionsDecoding(unittest.TestCase):
def test_decode_exe_action(self):
# fmt: off
buf = struct.pack(
"512B",
*[
0x03, 0x00, 0x16, 0x00, 0x00, 0x00, 0x4c, 0x00,
0x6f, 0x00, 0x63, 0x00, 0x61, 0x00, 0x6c, 0x00,
0x53, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00,
0x65, 0x00, 0x6d, 0x00, 0x66, 0x66, 0x00, 0x00,
0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, 0x25, 0x00,
0x77, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x64, 0x00,
0x69, 0x00, 0x72, 0x00, 0x25, 0x00, 0x5c, 0x00,
0x73, 0x00, 0x79, 0x00, 0x73, 0x00, 0x74, 0x00,
0x65, 0x00, 0x6d, 0x00, 0x33, 0x00, 0x32, 0x00,
0x5c, 0x00, 0x57, 0x00, 0x69, 0x00, 0x6e, 0x00,
0x64, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x73, 0x00,
0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00,
0x72, 0x00, 0x53, 0x00, 0x68, 0x00, 0x65, 0x00,
0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00, 0x76, 0x00,
0x31, 0x00, 0x2e, 0x00, 0x30, 0x00, 0x5c, 0x00,
0x70, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00,
0x72, 0x00, 0x73, 0x00, 0x68, 0x00, 0x65, 0x00,
0x6c, 0x00, 0x6c, 0x00, 0x2e, 0x00, 0x65, 0x00,
0x78, 0x00, 0x65, 0x00, 0x62, 0x01, 0x00, 0x00,
0x2d, 0x00, 0x45, 0x00, 0x78, 0x00, 0x65, 0x00,
0x63, 0x00, 0x75, 0x00, 0x74, 0x00, 0x69, 0x00,
0x6f, 0x00, 0x6e, 0x00, 0x50, 0x00, 0x6f, 0x00,
0x6c, 0x00, 0x69, 0x00, 0x63, 0x00, 0x79, 0x00,
0x20, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x72, 0x00,
0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x72, 0x00,
0x69, 0x00, 0x63, 0x00, 0x74, 0x00, 0x65, 0x00,
0x64, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00,
0x6f, 0x00, 0x6e, 0x00, 0x49, 0x00, 0x6e, 0x00,
0x74, 0x00, 0x65, 0x00, 0x72, 0x00, 0x61, 0x00,
0x63, 0x00, 0x74, 0x00, 0x69, 0x00, 0x76, 0x00,
0x65, 0x00, 0x20, 0x00, 0x2d, 0x00, 0x4e, 0x00,
0x6f, 0x00, 0x50, 0x00, 0x72, 0x00, 0x6f, 0x00,
0x66, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00,
0x20, 0x00, 0x2d, 0x00, 0x57, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00,
0x53, 0x00, 0x74, 0x00, 0x79, 0x00, 0x6c, 0x00,
0x65, 0x00, 0x20, 0x00, 0x48, 0x00, 0x69, 0x00,
0x64, 0x00, 0x64, 0x00, 0x65, 0x00, 0x6e, 0x00,
0x20, 0x00, 0x22, 0x00, 0x26, 0x00, 0x20, 0x00,
0x25, 0x00, 0x77, 0x00, 0x69, 0x00, 0x6e, 0x00,
0x64, 0x00, 0x69, 0x00, 0x72, 0x00, 0x25, 0x00,
0x5c, 0x00, 0x73, 0x00, 0x79, 0x00, 0x73, 0x00,
0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x33, 0x00,
0x32, 0x00, 0x5c, 0x00, 0x57, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x64, 0x00, 0x6f, 0x00, 0x77, 0x00,
0x73, 0x00, 0x50, 0x00, 0x6f, 0x00, 0x77, 0x00,
0x65, 0x00, 0x72, 0x00, 0x53, 0x00, 0x68, 0x00,
0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x5c, 0x00,
0x76, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x30, 0x00,
0x5c, 0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00,
0x75, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00,
0x5c, 0x00, 0x53, 0x00, 0x6d, 0x00, 0x62, 0x00,
0x53, 0x00, 0x68, 0x00, 0x61, 0x00, 0x72, 0x00,
0x65, 0x00, 0x5c, 0x00, 0x44, 0x00, 0x69, 0x00,
0x73, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00,
0x65, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x75, 0x00,
0x73, 0x00, 0x65, 0x00, 0x64, 0x00, 0x53, 0x00,
0x6d, 0x00, 0x62, 0x00, 0x31, 0x00, 0x2e, 0x00,
0x70, 0x00, 0x73, 0x00, 0x31, 0x00, 0x20, 0x00,
0x2d, 0x00, 0x53, 0x00, 0x63, 0x00, 0x65, 0x00,
0x6e, 0x00, 0x61, 0x00, 0x72, 0x00, 0x69, 0x00,
0x6f, 0x00, 0x20, 0x00, 0x43, 0x00, 0x6c, 0x00,
0x69, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00,
0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
)
try:
actions = scheduled_tasks.ActionSet.decode(buf).actions # type: ignore
self.assertEqual(len(actions), 1)
self.assertEqual(actions[0].action_type, scheduled_tasks.ActionType.Exe)
except Exception:
self.fail(
f"ActionDecoder.decode should not raise exception:\n{traceback.format_exc()}"
)
class TestTriggersDecoding(unittest.TestCase):
def test_decode_all_triggers(self):
"""
Tests decoding a set of all triggers that can be constructed via the
Task Scheduler GUI interface. Ensures that the correct number of bytes
is being consumed for each trigger structure.
"""
buf = struct.pack(
"1808B",
# fmt: off
*[
0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x38, 0x21, 0x41, 0x42, 0x48, 0x48, 0x48, 0x48,
0xa0, 0x12, 0xa0, 0xa4, 0x48, 0x48, 0x48, 0x48,
0x0e, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x41, 0x00, 0x75, 0x00, 0x74, 0x00, 0x68, 0x00,
0x6f, 0x00, 0x72, 0x00, 0x00, 0x00, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a,
0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44,
0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00,
0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00,
0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00,
0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00,
0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00,
0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00,
0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x2c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x80, 0xf4, 0x03, 0x00, 0xff, 0xff, 0xff, 0xff,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0xdd, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x07, 0x0a, 0x00, 0x00, 0x00, 0x09, 0x00,
0x80, 0x48, 0x11, 0xf8, 0x36, 0x1a, 0xdb, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x2e, 0xe2, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xc2, 0x31, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xda, 0xaf, 0x8d, 0x09, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0xcc, 0xcc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x65, 0x00, 0x78, 0x00, 0x65, 0x00,
0x22, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3c, 0x00, 0x51, 0x00, 0x75, 0x00, 0x65, 0x00,
0x72, 0x00, 0x79, 0x00, 0x4c, 0x00, 0x69, 0x00,
0x73, 0x00, 0x74, 0x00, 0x3e, 0x00, 0x3c, 0x00,
0x51, 0x00, 0x75, 0x00, 0x65, 0x00, 0x72, 0x00,
0x79, 0x00, 0x20, 0x00, 0x49, 0x00, 0x64, 0x00,
0x3d, 0x00, 0x22, 0x00, 0x30, 0x00, 0x22, 0x00,
0x20, 0x00, 0x50, 0x00, 0x61, 0x00, 0x74, 0x00,
0x68, 0x00, 0x3d, 0x00, 0x22, 0x00, 0x49, 0x00,
0x6e, 0x00, 0x74, 0x00, 0x65, 0x00, 0x72, 0x00,
0x6e, 0x00, 0x65, 0x00, 0x74, 0x00, 0x20, 0x00,
0x45, 0x00, 0x78, 0x00, 0x70, 0x00, 0x6c, 0x00,
0x6f, 0x00, 0x72, 0x00, 0x65, 0x00, 0x72, 0x00,
0x22, 0x00, 0x3e, 0x00, 0x3c, 0x00, 0x53, 0x00,
0x65, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x63, 0x00,
0x74, 0x00, 0x20, 0x00, 0x50, 0x00, 0x61, 0x00,
0x74, 0x00, 0x68, 0x00, 0x3d, 0x00, 0x22, 0x00,
0x49, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x65, 0x00,
0x72, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x74, 0x00,
0x20, 0x00, 0x45, 0x00, 0x78, 0x00, 0x70, 0x00,
0x6c, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x65, 0x00,
0x72, 0x00, 0x22, 0x00, 0x3e, 0x00, 0x2a, 0x00,
0x5b, 0x00, 0x53, 0x00, 0x79, 0x00, 0x73, 0x00,
0x74, 0x00, 0x65, 0x00, 0x6d, 0x00, 0x5b, 0x00,
0x45, 0x00, 0x76, 0x00, 0x65, 0x00, 0x6e, 0x00,
0x74, 0x00, 0x49, 0x00, 0x44, 0x00, 0x3d, 0x00,
0x32, 0x00, 0x5d, 0x00, 0x5d, 0x00, 0x3c, 0x00,
0x2f, 0x00, 0x53, 0x00, 0x65, 0x00, 0x6c, 0x00,
0x65, 0x00, 0x63, 0x00, 0x74, 0x00, 0x3e, 0x00,
0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00,
0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x3e, 0x00,
0x3c, 0x00, 0x2f, 0x00, 0x51, 0x00, 0x75, 0x00,
0x65, 0x00, 0x72, 0x00, 0x79, 0x00, 0x4c, 0x00,
0x69, 0x00, 0x73, 0x00, 0x74, 0x00, 0x3e, 0x00,
0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a,
0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44,
0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00,
0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00,
0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00,
0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00,
0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00,
0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00,
0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x77, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x01, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x1c, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x15, 0x00, 0x00, 0x00, 0x69, 0xce, 0x28, 0x2a,
0xce, 0xd8, 0x1f, 0x77, 0x37, 0x9c, 0xe2, 0x44,
0xf4, 0x01, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x40, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x44, 0x00, 0x45, 0x00, 0x53, 0x00, 0x4b, 0x00,
0x54, 0x00, 0x4f, 0x00, 0x50, 0x00, 0x2d, 0x00,
0x45, 0x00, 0x33, 0x00, 0x38, 0x00, 0x38, 0x00,
0x44, 0x00, 0x38, 0x00, 0x50, 0x00, 0x5c, 0x00,
0x41, 0x00, 0x64, 0x00, 0x6d, 0x00, 0x69, 0x00,
0x6e, 0x00, 0x69, 0x00, 0x73, 0x00, 0x74, 0x00,
0x72, 0x00, 0x61, 0x00, 0x74, 0x00, 0x6f, 0x00,
0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
# fmt: on
)
triggers = scheduled_tasks.TriggerSet.decode(buf)
self.assertIsNotNone(triggers)
def test_decode_triggers(self):
# fmt: off
buf = struct.pack(
"320B",
*[
0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x08, 0xA1, 0x40, 0x42, 0x48, 0x48, 0x48, 0x48,
0x7A, 0x7F, 0x59, 0xDC, 0x48, 0x48, 0x48, 0x48,
0x22, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x49, 0x00, 0x6E, 0x00, 0x74, 0x00, 0x65, 0x00,
0x72, 0x00, 0x61, 0x00, 0x63, 0x00, 0x74, 0x00,
0x69, 0x00, 0x76, 0x00, 0x65, 0x00, 0x55, 0x00,
0x73, 0x00, 0x65, 0x00, 0x72, 0x00, 0x73, 0x00,
0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
0x05, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x0C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
0x04, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x2C, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x80, 0x51, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xB9, 0x61, 0x1A, 0xA8, 0xB9, 0x61, 0x1A,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x2C, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xD9, 0x04,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48,
0x01, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48,
]
)
# fmt: on
triggers = scheduled_tasks.TriggerSet.decode(buf)
self.assertIsNotNone(triggers)
if not triggers:
return
self.assertGreater(len(triggers.triggers), 0)
-10
View File
@@ -1,10 +0,0 @@
# These packages are required for core functionality.
pefile>=2017.8.1 #foo
# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
# This is required for the yara plugins
yara-python>=3.8.0
pytest>=7.0.0
+585 -69
View File
@@ -13,6 +13,7 @@ import shutil
import tempfile
import hashlib
import json
import contextlib
#
# HELPER FUNCTIONS
@@ -37,7 +38,9 @@ def runvol(args, volatility, python):
return p.returncode, stdout, stderr
def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]):
def runvol_plugin(plugin, img, volatility, python, pluginargs=None, globalargs=None):
pluginargs = pluginargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
@@ -52,15 +55,70 @@ def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[])
return runvol(args, volatility, python)
def runvolshell(img, volshell, python, volshellargs=None, globalargs=None):
volshellargs = volshellargs or []
globalargs = globalargs or []
args = (
globalargs
+ [
"--single-location",
img,
"-q",
]
+ volshellargs
)
return runvol(args, volshell, python)
#
# TESTS
#
def basic_volshell_test(image, volatility, python, globalargs):
# Basic VolShell test to verify requirements and ensure VolShell runs without crashing
volshell_commands = [
"print(ps())",
"exit()",
]
# FIXME: When the minimum Python version includes 3.12, replace the following with:
# with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
fd, filename = tempfile.mkstemp(suffix=".txt")
try:
volshell_script = "\n".join(volshell_commands)
with os.fdopen(fd, "w") as f:
f.write(volshell_script)
rc, out, _err = runvolshell(
img=image,
volshell=volatility,
python=python,
volshellargs=["--script", filename],
globalargs=globalargs,
)
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
assert rc == 0
assert out.count(b"\n") >= 4
return out
# WINDOWS
def test_windows_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-w"])
assert out.count(b"<EPROCESS") > 40
def test_windows_pslist(image, volatility, python):
rc, out, err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
rc, out, _err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
out = out.lower()
assert out.find(b"system") != -1
assert out.find(b"csrss.exe") != -1
@@ -68,7 +126,7 @@ def test_windows_pslist(image, volatility, python):
assert out.count(b"\n") > 10
assert rc == 0
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.pslist.PsList", image, volatility, python, pluginargs=["--pid", "4"]
)
out = out.lower()
@@ -78,7 +136,7 @@ def test_windows_pslist(image, volatility, python):
def test_windows_psscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.psscan.PsScan", image, volatility, python)
rc, out, _err = runvol_plugin("windows.psscan.PsScan", image, volatility, python)
out = out.lower()
assert out.find(b"system") != -1
assert out.find(b"csrss.exe") != -1
@@ -88,21 +146,21 @@ def test_windows_psscan(image, volatility, python):
def test_windows_dlllist(image, volatility, python):
rc, out, err = runvol_plugin("windows.dlllist.DllList", image, volatility, python)
rc, out, _err = runvol_plugin("windows.dlllist.DllList", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
def test_windows_modules(image, volatility, python):
rc, out, err = runvol_plugin("windows.modules.Modules", image, volatility, python)
rc, out, _err = runvol_plugin("windows.modules.Modules", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
def test_windows_hivelist(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.registry.hivelist.HiveList", image, volatility, python
)
out = out.lower()
@@ -131,7 +189,7 @@ def test_windows_dumpfiles(image, volatility, python):
path = tempfile.mkdtemp()
rc, out, err = runvol_plugin(
rc, _out, _err = runvol_plugin(
"windows.dumpfiles.DumpFiles",
image,
volatility,
@@ -161,7 +219,7 @@ def test_windows_dumpfiles(image, volatility, python):
def test_windows_handles(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.handles.Handles", image, volatility, python, pluginargs=["--pid", "4"]
)
@@ -178,15 +236,27 @@ def test_windows_handles(image, volatility, python):
def test_windows_svcscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.svcscan.SvcScan", image, volatility, python)
rc, out, _err = runvol_plugin("windows.svcscan.SvcScan", image, volatility, python)
assert out.find(b"Microsoft ACPI Driver") != -1
assert out.count(b"\n") > 250
assert rc == 0
def test_windows_thrdscan(image, volatility, python):
rc, out, _err = runvol_plugin(
"windows.thrdscan.ThrdScan", image, volatility, python
)
# find pid 4 (of system process) which starts with lowest tids
assert out.find(b"\t4\t8") != -1
assert out.find(b"\t4\t12") != -1
assert out.find(b"\t4\t16") != -1
# assert out.find(b"this raieses AssertionError") != -1
assert rc == 0
def test_windows_privileges(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"]
)
@@ -198,7 +268,7 @@ def test_windows_privileges(image, volatility, python):
def test_windows_getsids(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.getsids.GetSIDs", image, volatility, python, pluginargs=["--pid", "4"]
)
@@ -210,7 +280,7 @@ def test_windows_getsids(image, volatility, python):
def test_windows_envars(image, volatility, python):
rc, out, err = runvol_plugin("windows.envars.Envars", image, volatility, python)
rc, out, _err = runvol_plugin("windows.envars.Envars", image, volatility, python)
assert out.find(b"PATH") != -1
assert out.find(b"PROCESSOR_ARCHITECTURE") != -1
@@ -222,7 +292,7 @@ def test_windows_envars(image, volatility, python):
def test_windows_callbacks(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.callbacks.Callbacks", image, volatility, python
)
@@ -234,7 +304,7 @@ def test_windows_callbacks(image, volatility, python):
def test_windows_vadwalk(image, volatility, python):
rc, out, err = runvol_plugin("windows.vadwalk.VadWalk", image, volatility, python)
rc, out, _err = runvol_plugin("windows.vadwalk.VadWalk", image, volatility, python)
assert out.find(b"Vad") != -1
assert out.find(b"VadS") != -1
@@ -245,7 +315,7 @@ def test_windows_vadwalk(image, volatility, python):
def test_windows_devicetree(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"windows.devicetree.DeviceTree", image, volatility, python
)
@@ -258,73 +328,513 @@ def test_windows_devicetree(image, volatility, python):
assert rc == 0
# LINUX
def test_windows_vadyarascan_yara_rule(image, volatility, python):
yara_rule_01 = r"""
rule fullvadyarascan
{
strings:
$s1 = "!This program cannot be run in DOS mode."
$s2 = "Qw))Pw"
$s3 = "W_wD)Pw"
$s4 = "1Xw+2Xw"
$s5 = "xd`wh``w"
$s6 = "0g`w0g`w8g`w8g`w@g`w@g`wHg`wHg`wPg`wPg`wXg`wXg`w`g`w`g`whg`whg`wpg`wpg`wxg`wxg`w"
condition:
all of them
}
"""
# FIXME: When the minimum Python version includes 3.12, replace the following with:
# with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
fd, filename = tempfile.mkstemp(suffix=".yar")
try:
with os.fdopen(fd, "w") as f:
f.write(yara_rule_01)
rc, out, _err = runvol_plugin(
"windows.vadyarascan.VadYaraScan",
image,
volatility,
python,
pluginargs=["--pid", "4012", "--yara-file", filename],
)
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
def test_linux_pslist(image, volatility, python):
rc, out, err = runvol_plugin("linux.pslist.PsList", image, volatility, python)
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.find(b"watchdog") != -1
assert out.count(b"\n") > 10
assert out.count(b"\n") > 4
assert rc == 0
def test_linux_check_idt(image, volatility, python):
rc, out, err = runvol_plugin("linux.check_idt.Check_idt", image, volatility, python)
out = out.lower()
assert out.count(b"__kernel__") >= 10
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_check_syscall(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.check_syscall.Check_syscall", image, volatility, python
def test_windows_vadyarascan_yara_string(image, volatility, python):
rc, out, _err = runvol_plugin(
"windows.vadyarascan.VadYaraScan",
image,
volatility,
python,
pluginargs=["--pid", "4012", "--yara-string", "MZ"],
)
out = out.lower()
assert out.find(b"sys_close") != -1
assert out.find(b"sys_open") != -1
assert out.count(b"\n") > 100
assert rc == 0
def test_linux_lsmod(image, volatility, python):
rc, out, err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
def test_linux_lsof(image, volatility, python):
rc, out, err = runvol_plugin("linux.lsof.Lsof", image, volatility, python)
out = out.lower()
# LINUX
def test_linux_volshell(image, volatility, python):
out = basic_volshell_test(image, volatility, python, globalargs=["-l"])
assert out.count(b"<task_struct") > 100
def test_linux_pslist(image, volatility, python):
rc, out, _err = runvol_plugin("linux.pslist.PsList", image, volatility, python)
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.find(b"watchdog") != -1
assert out.count(b"\n") > 10
def test_linux_check_idt(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_idt.Check_idt", image, volatility, python
)
assert rc == 0
out = out.lower()
assert out.count(b"__kernel__") >= 10
assert out.count(b"\n") > 10
def test_linux_check_syscall(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_syscall.Check_syscall", image, volatility, python
)
assert rc == 0
out = out.lower()
assert out.find(b"sys_close") != -1
assert out.find(b"sys_open") != -1
assert out.count(b"\n") > 100
def test_linux_lsmod(image, volatility, python):
rc, out, _err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python)
assert rc == 0
out = out.lower()
assert out.count(b"\n") > 10
def test_linux_lsof(image, volatility, python):
rc, out, _err = runvol_plugin("linux.lsof.Lsof", image, volatility, python)
assert rc == 0
out = out.lower()
assert out.count(b"socket:") >= 10
assert out.count(b"\n") > 35
assert rc == 0
def test_linux_proc_maps(image, volatility, python):
rc, out, err = runvol_plugin("linux.proc.Maps", image, volatility, python)
out = out.lower()
rc, out, _err = runvol_plugin("linux.proc.Maps", image, volatility, python)
assert rc == 0
out = out.lower()
assert out.count(b"anonymous mapping") >= 10
assert out.count(b"\n") > 100
assert rc == 0
def test_linux_tty_check(image, volatility, python):
rc, out, err = runvol_plugin("linux.tty_check.tty_check", image, volatility, python)
out = out.lower()
rc, out, _err = runvol_plugin(
"linux.tty_check.tty_check", image, volatility, python
)
assert rc == 0
out = out.lower()
assert out.find(b"__kernel__") != -1
assert out.count(b"\n") >= 5
def test_linux_sockstat(image, volatility, python):
rc, out, _err = runvol_plugin("linux.sockstat.Sockstat", image, volatility, python)
assert rc == 0
assert out.count(b"AF_UNIX") >= 354
assert out.count(b"AF_BLUETOOTH") >= 5
assert out.count(b"AF_INET") >= 32
assert out.count(b"AF_INET6") >= 20
assert out.count(b"AF_PACKET") >= 1
assert out.count(b"AF_NETLINK") >= 43
def test_linux_library_list(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.library_list.LibraryList",
image,
volatility,
python,
pluginargs=["--pids", "2363"],
)
assert rc == 0
assert re.search(
rb"NetworkManager\s2363\s0x7f52cdda0000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
out,
)
assert out.count(b"\n") > 10
def test_linux_pstree(image, volatility, python):
rc, out, _err = runvol_plugin("linux.pstree.PsTree", image, volatility, python)
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.count(b"\n") > 10
def test_linux_pidhashtable(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.pidhashtable.PIDHashTable", image, volatility, python
)
assert rc == 0
out = out.lower()
assert (out.find(b"init") != -1) or (out.find(b"systemd") != -1)
assert out.count(b"\n") > 10
def test_linux_bash(image, volatility, python):
rc, out, _err = runvol_plugin("linux.bash.Bash", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_boottime(image, volatility, python):
rc, out, _err = runvol_plugin("linux.boottime.Boottime", image, volatility, python)
assert rc == 0
out = out.lower()
assert out.count(b"utc") >= 1
def test_linux_capabilities(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.capabilities.Capabilities",
image,
volatility,
python,
globalargs=["-vvv"],
)
if rc != 0 and err.count(b"Unsupported kernel capabilities implementation") > 0:
# The linux-sample-1.bin kernel implementation isn't supported.
# However, we can still check that the plugin requirements are met.
return None
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_check_creds(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_creds.Check_creds", image, volatility, python
)
# linux-sample-1.bin has no processes sharing credentials.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_elfs(image, volatility, python):
rc, out, _err = runvol_plugin("linux.elfs.Elfs", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_envars(image, volatility, python):
rc, out, _err = runvol_plugin("linux.envars.Envars", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_kthreads(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.kthreads.Kthreads",
image,
volatility,
python,
globalargs=["-vvv"],
)
if rc != 0 and err.count(b"Unsupported kthread implementation") > 0:
# The linux-sample-1.bin kernel implementation isn't supported.
# However, we can still check that the plugin requirements are met.
return None
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_malfind(image, volatility, python):
rc, out, _err = runvol_plugin("linux.malfind.Malfind", image, volatility, python)
# linux-sample-1.bin has no process memory ranges with potential injected code.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_mountinfo(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.mountinfo.MountInfo", image, volatility, python
)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_psaux(image, volatility, python):
rc, out, _err = runvol_plugin("linux.psaux.PsAux", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 50
def test_linux_ptrace(image, volatility, python):
rc, out, _err = runvol_plugin("linux.ptrace.Ptrace", image, volatility, python)
# linux-sample-1.bin has no processes being ptraced.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_vmaregexscan(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.vmaregexscan.VmaRegExScan",
image,
volatility,
python,
pluginargs=["--pid", "1", "--pattern", "\\x7fELF"],
)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_vmayarascan_yara_rule(image, volatility, python):
yara_rule_01 = r"""
rule fullvmayarascan
{
strings:
$s1 = "_nss_files_parse_grent"
$s2 = "/lib64/ld-linux-x86-64.so.2"
$s3 = "(bufferend - (char *) 0) % sizeof (char *) == 0"
condition:
all of them
}
"""
# FIXME: When the minimum Python version includes 3.12, replace the following with:
# with tempfile.NamedTemporaryFile(delete_on_close=False) as fd: ...
fd, filename = tempfile.mkstemp(suffix=".yar")
try:
with os.fdopen(fd, "w") as f:
f.write(yara_rule_01)
rc, out, _err = runvol_plugin(
"linux.vmayarascan.VmaYaraScan",
image,
volatility,
python,
pluginargs=["--pid", "8600", "--yara-file", filename],
)
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(filename)
assert rc == 0
assert out.count(b"\n") > 4
def test_linux_vmayarascan_yara_string(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.vmayarascan.VmaYaraScan",
image,
volatility,
python,
pluginargs=["--pid", "1", "--yara-string", "ELF"],
)
assert rc == 0
assert out.count(b"\n") > 10
def test_linux_page_cache_files(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.pagecache.Files",
image,
volatility,
python,
pluginargs=["--find", "/etc/passwd"],
)
assert rc == 0
assert out.count(b"\n") > 4
# inode_num inode_addr ... file_path
assert re.search(
rb"146829\s0x88001ab5c270.*?/etc/passwd",
out,
)
def test_linux_page_cache_inodepages(image, volatility, python):
inode_address = hex(0x88001AB5C270)
inode_dump_filename = f"inode_{inode_address}.dmp"
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
image,
volatility,
python,
pluginargs=["--inode", inode_address],
)
assert rc == 0
assert out.count(b"\n") > 4
# PageVAddr PagePAddr MappingAddr .. DumpSafe
assert re.search(
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
out,
)
try:
rc, out, _err = runvol_plugin(
"linux.pagecache.InodePages",
image,
volatility,
python,
pluginargs=["--inode", inode_address, "--dump"],
)
assert rc == 0
assert out.count(b"\n") >= 4
assert os.path.exists(inode_dump_filename)
with open(inode_dump_filename, "rb") as fp:
inode_contents = fp.read()
assert inode_contents.count(b"\n") > 30
assert inode_contents.count(b"root:x:0:0:root:/root:/bin/bash") > 0
finally:
with contextlib.suppress(FileNotFoundError):
os.remove(inode_dump_filename)
def test_linux_check_afinfo(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_afinfo.Check_afinfo", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_check_modules(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.check_modules.Check_modules", image, volatility, python
)
# linux-sample-1.bin has no suspicious results.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_ebpf_progs(image, volatility, python):
rc, out, err = runvol_plugin(
"linux.ebpf.EBPF",
image,
volatility,
python,
globalargs=["-vvv"],
)
if rc != 0 and err.count(b"Unsupported kernel") > 0:
# The linux-sample-1.bin kernel implementation isn't supported.
# However, we can still check that the plugin requirements are met.
return None
assert rc == 0
assert out.count(b"\n") > 4
def test_linux_iomem(image, volatility, python):
rc, out, _err = runvol_plugin("linux.iomem.IOMem", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_keyboard_notifiers(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.keyboard_notifiers.Keyboard_notifiers", image, volatility, python
)
# linux-sample-1.bin has no suspicious results for this plugin.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_kmesg(image, volatility, python):
rc, out, _err = runvol_plugin("linux.kmsg.Kmsg", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_netfilter(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.netfilter.Netfilter", image, volatility, python
)
# linux-sample-1.bin has no suspicious results for this plugin.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_psscan(image, volatility, python):
rc, out, _err = runvol_plugin("linux.psscan.PsScan", image, volatility, python)
assert rc == 0
assert out.count(b"\n") > 100
def test_linux_hidden_modules(image, volatility, python):
rc, out, _err = runvol_plugin(
"linux.hidden_modules.Hidden_modules", image, volatility, python
)
# linux-sample-1.bin has no hidden modules.
# This validates that plugin requirements are met and exceptions are not raised.
assert rc == 0
assert out.count(b"\n") >= 4
def test_linux_ip_addr(image, volatility, python):
@@ -360,8 +870,12 @@ def test_linux_ip_link(image, volatility, python):
# MAC
def test_mac_volshell(image, volatility, python):
basic_volshell_test(image, volatility, python, globalargs=["-m"])
def test_mac_pslist(image, volatility, python):
rc, out, err = runvol_plugin("mac.pslist.PsList", image, volatility, python)
rc, out, _err = runvol_plugin("mac.pslist.PsList", image, volatility, python)
out = out.lower()
assert (out.find(b"kernel_task") != -1) or (out.find(b"launchd") != -1)
@@ -370,7 +884,7 @@ def test_mac_pslist(image, volatility, python):
def test_mac_check_syscall(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"mac.check_syscall.Check_syscall", image, volatility, python
)
out = out.lower()
@@ -383,7 +897,7 @@ def test_mac_check_syscall(image, volatility, python):
def test_mac_check_sysctl(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"mac.check_sysctl.Check_sysctl", image, volatility, python
)
out = out.lower()
@@ -394,7 +908,7 @@ def test_mac_check_sysctl(image, volatility, python):
def test_mac_check_trap_table(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"mac.check_trap_table.Check_trap_table", image, volatility, python
)
out = out.lower()
@@ -405,7 +919,7 @@ def test_mac_check_trap_table(image, volatility, python):
def test_mac_ifconfig(image, volatility, python):
rc, out, err = runvol_plugin("mac.ifconfig.Ifconfig", image, volatility, python)
rc, out, _err = runvol_plugin("mac.ifconfig.Ifconfig", image, volatility, python)
out = out.lower()
assert out.find(b"127.0.0.1") != -1
@@ -415,7 +929,7 @@ def test_mac_ifconfig(image, volatility, python):
def test_mac_lsmod(image, volatility, python):
rc, out, err = runvol_plugin("mac.lsmod.Lsmod", image, volatility, python)
rc, out, _err = runvol_plugin("mac.lsmod.Lsmod", image, volatility, python)
out = out.lower()
assert out.find(b"com.apple") != -1
@@ -424,7 +938,7 @@ def test_mac_lsmod(image, volatility, python):
def test_mac_lsof(image, volatility, python):
rc, out, err = runvol_plugin("mac.lsof.Lsof", image, volatility, python)
rc, out, _err = runvol_plugin("mac.lsof.Lsof", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 50
@@ -432,7 +946,7 @@ def test_mac_lsof(image, volatility, python):
def test_mac_malfind(image, volatility, python):
rc, out, err = runvol_plugin("mac.malfind.Malfind", image, volatility, python)
rc, out, _err = runvol_plugin("mac.malfind.Malfind", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 20
@@ -440,7 +954,7 @@ def test_mac_malfind(image, volatility, python):
def test_mac_mount(image, volatility, python):
rc, out, err = runvol_plugin("mac.mount.Mount", image, volatility, python)
rc, out, _err = runvol_plugin("mac.mount.Mount", image, volatility, python)
out = out.lower()
assert out.find(b"/dev") != -1
@@ -449,7 +963,7 @@ def test_mac_mount(image, volatility, python):
def test_mac_netstat(image, volatility, python):
rc, out, err = runvol_plugin("mac.netstat.Netstat", image, volatility, python)
rc, out, _err = runvol_plugin("mac.netstat.Netstat", image, volatility, python)
assert out.find(b"TCP") != -1
assert out.find(b"UDP") != -1
@@ -459,7 +973,7 @@ def test_mac_netstat(image, volatility, python):
def test_mac_proc_maps(image, volatility, python):
rc, out, err = runvol_plugin("mac.proc_maps.Maps", image, volatility, python)
rc, out, _err = runvol_plugin("mac.proc_maps.Maps", image, volatility, python)
out = out.lower()
assert out.find(b"[heap]") != -1
@@ -468,7 +982,7 @@ def test_mac_proc_maps(image, volatility, python):
def test_mac_psaux(image, volatility, python):
rc, out, err = runvol_plugin("mac.psaux.Psaux", image, volatility, python)
rc, out, _err = runvol_plugin("mac.psaux.Psaux", image, volatility, python)
out = out.lower()
assert out.find(b"executable_path") != -1
@@ -477,7 +991,7 @@ def test_mac_psaux(image, volatility, python):
def test_mac_socket_filters(image, volatility, python):
rc, out, err = runvol_plugin(
rc, out, _err = runvol_plugin(
"mac.socket_filters.Socket_filters", image, volatility, python
)
out = out.lower()
@@ -487,7 +1001,7 @@ def test_mac_socket_filters(image, volatility, python):
def test_mac_timers(image, volatility, python):
rc, out, err = runvol_plugin("mac.timers.Timers", image, volatility, python)
rc, out, _err = runvol_plugin("mac.timers.Timers", image, volatility, python)
out = out.lower()
assert out.count(b"\n") > 6
@@ -495,7 +1009,9 @@ def test_mac_timers(image, volatility, python):
def test_mac_trustedbsd(image, volatility, python):
rc, out, err = runvol_plugin("mac.trustedbsd.Trustedbsd", image, volatility, python)
rc, out, _err = runvol_plugin(
"mac.trustedbsd.Trustedbsd", image, volatility, python
)
out = out.lower()
assert out.count(b"\n") > 10
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# 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
+190 -56
View File
@@ -19,9 +19,17 @@ import os
import sys
import tempfile
import traceback
from typing import Any, Dict, Type, Union
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from urllib import parse, request
try:
import argcomplete
HAS_ARGCOMPLETE = True
except ImportError:
HAS_ARGCOMPLETE = False
from volatility3.cli import text_filter
import volatility3.plugins
import volatility3.symbols
from volatility3 import framework
@@ -49,14 +57,14 @@ formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
class PrintedProgress(object):
class PrintedProgress:
"""A progress handler that prints the progress value and the description
onto the command line."""
def __init__(self):
self._max_message_len = 0
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
"""A simple function for providing text-based feedback.
.. warning:: Only for development use.
@@ -73,14 +81,14 @@ class PrintedProgress(object):
class MuteProgress(PrintedProgress):
"""A dummy progress handler that produces no output when called."""
def __call__(self, progress: Union[int, float], description: str = None):
def __call__(self, progress: Union[int, float], description: Optional[str] = None):
pass
class CommandLine:
"""Constructs a command-line interface object for users to run plugins."""
CLI_NAME = "volatility"
CLI_NAME = os.path.basename(sys.argv[0]) # vol or volatility
def __init__(self):
self.setup_logging()
@@ -105,6 +113,9 @@ class CommandLine:
]
)
# Load up system defaults
delayed_logs, default_config = self.load_system_defaults("vol.json")
parser = volargparse.HelpfulArgParser(
add_help=False,
prog=self.CLI_NAME,
@@ -115,9 +126,7 @@ class CommandLine:
"--help",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit, for specific plugin options use '{} <pluginname> --help'".format(
parser.prog
),
help=f"Show this help message and exit, for specific plugin options use '{parser.prog} <pluginname> --help'",
)
parser.add_argument(
"-c",
@@ -224,12 +233,37 @@ class CommandLine:
default=constants.CACHE_PATH,
type=str,
)
parser.add_argument(
isf_group = parser.add_mutually_exclusive_group()
isf_group.add_argument(
"--offline",
help="Do not search online for additional JSON files",
default=False,
action="store_true",
)
isf_group.add_argument(
"-u",
"--remote-isf-url",
metavar="URL",
help="Search online for ISF json files",
default=constants.REMOTE_ISF_URL,
type=str,
)
parser.add_argument(
"--filters",
help="List of filters to apply to the output (in the form of [+-]columname,pattern[!])",
default=[],
action="append",
)
parser.add_argument(
"--hide-columns",
help="Case-insensitive space separated list of prefixes to determine which columns to hide in the output if provided",
default=None,
action="extend",
nargs="*",
type=str,
)
parser.set_defaults(**default_config)
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
# processed the plugin choice or had the plugin subparser added.
@@ -241,6 +275,30 @@ class CommandLine:
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)
file_logger.setLevel(1)
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
rootlog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
if partial_args.verbosity < 1:
sys.tracebacklimit = None
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
### Alter constants if necessary
if partial_args.plugin_dirs:
volatility3.plugins.__path__ = [
os.path.abspath(p) for p in partial_args.plugin_dirs.split(";")
@@ -254,23 +312,6 @@ class CommandLine:
if partial_args.cache_path:
constants.CACHE_PATH = partial_args.cache_path
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
file_logger.setLevel(1)
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
rootlog.addHandler(file_logger)
vollog.info("Logging started")
if partial_args.verbosity < 3:
if partial_args.verbosity < 1:
sys.tracebacklimit = None
console.setLevel(30 - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
vollog.info(f"Volatility plugins path: {volatility3.plugins.__path__}")
vollog.info(f"Volatility symbols path: {volatility3.symbols.__path__}")
@@ -287,6 +328,8 @@ class CommandLine:
if partial_args.offline:
constants.OFFLINE = partial_args.offline
elif partial_args.remote_isf_url:
constants.REMOTE_ISF_URL = partial_args.remote_isf_url
# Do the initialization
ctx = contexts.Context() # Construct a blank context
@@ -315,14 +358,26 @@ class CommandLine:
subparser = parser.add_subparsers(
title="Plugins",
dest="plugin",
description="For plugin specific options, run '{} <plugin> --help'".format(
self.CLI_NAME
),
description=f"For plugin specific options, run '{self.CLI_NAME} <plugin> --help'",
action=volargparse.HelpfulSubparserAction,
metavar="PLUGIN",
)
for plugin in sorted(plugin_list):
# First line of a plugin docstring will be the short description for -h.
# Text after the first two consecutive new lines will be
# the additional description (argparse epilog).
short_help = additional_help = None
if plugin_list[plugin].__doc__ is not None:
doc_split = plugin_list[plugin].__doc__.split("\n\n", 1)
short_help = doc_split[0].strip()
if len(doc_split) > 1:
additional_help = doc_split[1].strip()
plugin_parser = subparser.add_parser(
plugin, help=plugin_list[plugin].__doc__
plugin,
help=short_help,
description=short_help,
epilog=additional_help,
)
self.populate_requirements_argparse(plugin_parser, plugin_list[plugin])
@@ -332,9 +387,15 @@ class CommandLine:
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
# Run the argparser
if HAS_ARGCOMPLETE:
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
# before all the plugins have been added
argcomplete.autocomplete(parser)
args = parser.parse_args()
if args.plugin is None:
parser.error("Please select a plugin to run")
parser.error(
f"Please select a plugin to run (see '{self.CLI_NAME} --help' for options"
)
vollog.log(
constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}"
@@ -362,7 +423,7 @@ class CommandLine:
# UI fills in the config, here we load it from the config file and do it before we process the CL parameters
if args.config:
with open(args.config, "r") as f:
with open(args.config) as f:
json_val = json.load(f)
ctx.config.splice(
plugin_config_path,
@@ -444,7 +505,11 @@ class CommandLine:
try:
# Construct and run the plugin
if constructed:
renderers[args.renderer]().render(constructed.run())
grid = constructed.run()
renderer = renderers[args.renderer]()
renderer.filter = text_filter.CLIFilter(grid, args.filters)
renderer.column_hide_list = args.hide_columns
renderer.render(grid)
except exceptions.VolatilityException as excp:
self.process_exceptions(excp)
@@ -463,6 +528,50 @@ class CommandLine:
)
return requirements.URIRequirement.location_from_file(filename)
def load_system_defaults(
self, filename: str
) -> Tuple[List[Tuple[int, str]], Dict[str, Any]]:
"""Modify the main configuration based on the default configuration override"""
# Build the config path
default_config_path = os.path.join(
os.path.expanduser("~"), ".config", "volatility3", filename
)
if sys.platform == "win32":
default_config_path = os.path.join(
os.environ.get("APPDATA", os.path.expanduser("~")),
"volatility3",
filename,
)
delayed_logs = []
# Process it if the files exist
if os.path.exists(default_config_path):
with open(default_config_path, "rb") as config_json:
result = json.load(config_json)
if not isinstance(result, dict):
delayed_logs.append(
(
logging.INFO,
f"Default configuration file {default_config_path} does not contain a dictionary",
)
)
else:
delayed_logs.append(
(
logging.INFO,
f"Loading default configuration options from {default_config_path}",
)
)
delayed_logs.append(
(
logging.DEBUG,
f"Loaded configuration: {json.dumps(result, indent = 2, sort_keys = True)}",
)
)
return delayed_logs, result
return delayed_logs, {}
def process_exceptions(self, excp):
"""Provide useful feedback if an exception occurs during a run of a plugin."""
# Ensure there's nothing in the cache
@@ -474,6 +583,8 @@ class CommandLine:
fulltrace = traceback.TracebackException.from_exception(excp).format(chain=True)
vollog.debug("".join(fulltrace))
file_a_bug_msg = f"Please re-run with -vvv and file a bug with the output at {constants.BUG_URL}"
if isinstance(excp, exceptions.InvalidAddressException):
general = "Volatility was unable to read a requested page:"
if isinstance(excp, exceptions.SwappedInvalidAddressException):
@@ -518,22 +629,28 @@ class CommandLine:
elif isinstance(excp, exceptions.LayerException):
general = f"Volatility experienced a layer-related issue: {excp.layer_name}"
detail = f"{excp}"
caused_by = [
"A faulty layer implementation (re-run with -vvv and file a bug)"
]
caused_by = [f"A faulty layer implementation. {file_a_bug_msg}"]
elif isinstance(excp, exceptions.MissingModuleException):
general = f"Volatility could not import a necessary module: {excp.module}"
detail = f"{excp}"
caused_by = [
"A required python module is not installed (install the module and re-run)"
]
elif isinstance(excp, exceptions.RenderException):
general = "Volatility experienced an issue when rendering the output:"
detail = f"{excp}"
caused_by = ["An invalid renderer option, such as no visible columns"]
elif isinstance(excp, exceptions.VersionMismatchException):
general = "A version mismatch was detected between two components:"
detail = f"{excp}"
caused_by = [
excp.failure_reason or "An outdated API caller, such as a method.",
file_a_bug_msg,
]
else:
general = "Volatility encountered an unexpected situation."
detail = ""
caused_by = [
"Please re-run using with -vvv and file a bug with the output",
f"at {constants.BUG_URL}",
]
caused_by = [file_a_bug_msg]
# Code that actually renders the exception
output = sys.stderr
@@ -616,9 +733,7 @@ class CommandLine:
if isinstance(requirement, requirements.ListRequirement):
if not isinstance(value, list):
raise TypeError(
"Configuration for ListRequirement was not a list: {}".format(
requirement.name
)
f"Configuration for ListRequirement was not a list: {requirement.name}"
)
value = [requirement.element_type(x) for x in value]
if not inspect.isclass(configurables_list[configurable]):
@@ -631,6 +746,17 @@ class CommandLine:
)
context.config[extended_path] = value
def order_extra_verbose_levels(self):
for level, level_value in enumerate(
[
constants.LOGLEVEL_V,
constants.LOGLEVEL_VV,
constants.LOGLEVEL_VVV,
constants.LOGLEVEL_VVVV,
]
):
logging.addLevelName(level_value, f"DETAIL {level+1}")
def file_handler_class_factory(self, direct=True):
output_dir = self.output_dir
@@ -639,19 +765,17 @@ class CommandLine:
"""Gets the final filename"""
if output_dir is None:
raise TypeError("Output directory is not a string")
os.makedirs(output_dir, exist_ok=True)
pref_name_array = self.preferred_filename.split(".")
filename, extension = (
os.path.join(output_dir, ".".join(pref_name_array[:-1])),
pref_name_array[-1],
)
output_filename = f"{filename}.{extension}"
output_filename = os.path.join(output_dir, self.preferred_filename)
filename, extension = os.path.splitext(output_filename)
counter = 1
while os.path.exists(output_filename):
output_filename = f"{filename}-{counter}.{extension}"
output_filename = f"{filename}-{counter}{extension}"
counter += 1
return output_filename
class CLIMemFileHandler(io.BytesIO, CLIFileHandler):
@@ -682,7 +806,7 @@ class CommandLine:
fd, self._name = tempfile.mkstemp(
suffix=".vol3", prefix="tmp_", dir=output_dir
)
self._file = io.open(fd, mode="w+b")
self._file = open(fd, mode="w+b")
CLIFileHandler.__init__(self, filename)
for item in dir(self._file):
if not item.startswith("_") and item not in (
@@ -714,8 +838,16 @@ class CommandLine:
if self._file.closed:
return None
self._file.close()
output_filename = self._get_final_filename()
# Update the filename, which may have changed if a file with
# the same name already existed. This needs to be done before
# closing the file, otherwise FileHandlerInterface will raise
# an exception. Also, the preferred_filename setter only allows
# a specific set of characters, where '/' is not in that list
self.preferred_filename = os.path.basename(output_filename)
self._file.close()
os.rename(self._name, output_filename)
if direct:
@@ -747,9 +879,7 @@ class CommandLine:
requirement, interfaces.configuration.RequirementInterface
):
raise TypeError(
"Plugin contains requirements that are not RequirementInterfaces: {}".format(
configurable.__name__
)
f"Plugin contains requirements that are not RequirementInterfaces: {configurable.__name__}"
)
if isinstance(requirement, interfaces.configuration.SimpleTypeRequirement):
additional["type"] = requirement.instance_type
@@ -763,7 +893,11 @@ class CommandLine:
requirement,
volatility3.framework.configuration.requirements.ListRequirement,
):
additional["type"] = requirement.element_type
# Allow a list of integers, specified with the convenient 0x hexadecimal format
if requirement.element_type is int:
additional["type"] = lambda x: int(x, 0)
else:
additional["type"] = requirement.element_type
nargs = "*" if requirement.optional else "+"
additional["nargs"] = nargs
elif isinstance(
+99
View File
@@ -0,0 +1,99 @@
import logging
import re
from typing import Any, List, Optional
from volatility3.framework import constants, interfaces
vollog = logging.getLogger(__name__)
class CLIFilter:
def __init__(self, treegrid, filters: List[str]):
self._filters = self._prepare(treegrid, filters)
def _prepare(self, treegrid: interfaces.renderers.TreeGrid, filters: List[str]):
"""Runs through the filter strings and creates the necessary filter objects"""
output = []
for filter in filters:
exclude = False
regex = False
pattern = None
column_name = None
if filter.startswith("-"):
exclude = True
filter = filter[1:]
elif filter.startswith("+"):
filter = filter[1:]
components = filter.split(",")
if len(components) < 2:
pattern = components[0]
else:
column_name = components[0]
pattern = ",".join(components[1:])
if pattern and pattern.endswith("!"):
regex = True
pattern = pattern[:-1]
column_num = None
if column_name:
for num, column in enumerate(treegrid.columns):
if column_name.lower() in column.name.lower():
column_num = num
break
if pattern:
output.append(ColumnFilter(column_num, pattern, regex, exclude))
vollog.log(constants.LOGLEVEL_VVV, "Filters:\n" + repr(output))
return output
def filter(
self,
row: List[Any],
) -> bool:
"""Filters the row based on each of the column_filters"""
if not self._filters:
return False
found = any(column_filter.found(row) for column_filter in self._filters)
return not found
class ColumnFilter:
def __init__(
self,
column_num: Optional[int],
pattern: str,
regex: bool = False,
exclude: bool = False,
) -> None:
self.column_num = column_num
self.pattern = pattern
self.regex = regex
self.exclude = exclude
def find(self, item) -> bool:
"""Identifies whether an item is found in the appropriate column"""
try:
if self.regex:
return bool(re.search(self.pattern, f"{item}"))
return self.pattern in f"{item}"
except OSError:
return False
def found(self, row: List[Any]) -> bool:
"""Determines whether a row should be filtered
If the classes exclude value is false, and the necessary pattern is found, the row is not filtered,
otherwise it is filtered.
"""
if self.column_num is None:
found = any(self.find(x) for x in row)
else:
found = self.find(row[self.column_num])
if self.exclude:
return not found
return found
def __repr__(self) -> str:
"""Returns a display of a column filter"""
return f"ColumnFilter(column={self.column_num},exclude={self.exclude},regex={self.regex},pattern={self.pattern})"
+133 -60
View File
@@ -10,8 +10,9 @@ import string
import sys
from functools import wraps
from typing import Any, Callable, Dict, List, Tuple
from volatility3.cli import text_filter
from volatility3.framework import interfaces, renderers
from volatility3.framework import exceptions, interfaces, renderers
from volatility3.framework.renderers import format_hints
vollog = logging.getLogger(__name__)
@@ -24,7 +25,7 @@ except ImportError:
vollog.debug("Disassembly library capstone not found")
def hex_bytes_as_text(value: bytes) -> str:
def hex_bytes_as_text(value: bytes, width: int = 16) -> str:
"""Renders HexBytes as text.
Args:
@@ -35,19 +36,26 @@ def hex_bytes_as_text(value: bytes) -> str:
"""
if not isinstance(value, bytes):
raise TypeError(f"hex_bytes_as_text takes bytes not: {type(value)}")
ascii = []
hex = []
count = 0
output = ""
for byte in value:
hex.append(f"{byte:02x}")
ascii.append(chr(byte) if 0x20 < byte <= 0x7E else ".")
if (count % 8) == 7:
output += "\n"
output += " ".join(hex[count - 7 : count + 1])
output += "\t"
output += "".join(ascii[count - 7 : count + 1])
count += 1
printables = ""
output = "\n"
for count, byte in enumerate(value):
output += f"{byte:02x} "
char = chr(byte)
printables += char if 0x20 <= byte <= 0x7E else "."
if count % width == width - 1:
output += printables
if count < len(value) - 1:
output += "\n"
printables = ""
# Handle leftovers when the length is not mutiple of width
if printables:
padding = width - len(printables)
output += " " * padding
output += printables
output += " " * padding
return output
@@ -124,7 +132,7 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str:
for i in disasm_types[disasm.architecture].disasm(
disasm.data, disasm.offset
):
output += f"\n0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}"
output += f"\n{i.address:#x}:\t{i.mnemonic}\t{i.op_str}"
return output
return QuickTextRenderer._type_renderers[bytes](disasm.data)
@@ -134,6 +142,31 @@ class CLIRenderer(interfaces.renderers.Renderer):
name = "unnamed"
structured_output = False
filter: text_filter.CLIFilter = None
column_hide_list: list = None
def ignored_columns(
self,
grid: interfaces.renderers.TreeGrid,
) -> List[interfaces.renderers.Column]:
ignored_column_list = []
if self.column_hide_list:
for column in grid.columns:
accept = True
for column_prefix in self.column_hide_list:
if column.name.lower().startswith(column_prefix.lower()):
accept = False
if not accept:
ignored_column_list.append(column)
elif self.column_hide_list is None:
return []
if len(ignored_column_list) == len(grid.columns):
raise exceptions.RenderException("No visible columns to render")
vollog.info(
f"Hiding columns: {[column.name for column in ignored_column_list]}"
)
return ignored_column_list
class QuickTextRenderer(CLIRenderer):
@@ -143,7 +176,7 @@ class QuickTextRenderer(CLIRenderer):
format_hints.HexBytes: optional(hex_bytes_as_text),
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
interfaces.renderers.Disassembly: optional(display_disassembly),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
"default": optional(lambda x: f"{x}"),
}
@@ -166,25 +199,31 @@ class QuickTextRenderer(CLIRenderer):
outfd = sys.stdout
line = []
ignore_columns = self.ignored_columns(grid)
for column in grid.columns:
# Ignore the type because namedtuples don't realize they have accessible attributes
line.append(f"{column.name}")
if column not in ignore_columns:
line.append(f"{column.name}")
outfd.write("\n{}\n".format("\t".join(line)))
def visitor(node: interfaces.renderers.TreeNode, accumulator):
line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
if column not in ignore_columns:
line.append(renderer(node.values[column_index]))
if self.filter and self.filter.filter(line):
return accumulator
accumulator.write("\n")
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
accumulator.write(
"*" * max(0, node.path_depth - 1)
+ ("" if (node.path_depth <= 1) else " ")
)
line = []
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
line.append(renderer(node.values[column_index]))
accumulator.write("{}".format("\t".join(line)))
accumulator.flush()
return accumulator
@@ -217,7 +256,7 @@ class CSVRenderer(CLIRenderer):
format_hints.HexBytes: optional(hex_bytes_as_text),
format_hints.MultiTypeData: optional(multitypedata_as_text),
interfaces.renderers.Disassembly: optional(display_disassembly),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: optional(lambda x: x.strftime("%Y-%m-%d %H:%M:%S.%f %Z")),
"default": optional(lambda x: f"{x}"),
}
@@ -235,11 +274,13 @@ class CSVRenderer(CLIRenderer):
grid: The TreeGrid object to render
"""
outfd = sys.stdout
ignore_columns = self.ignored_columns(grid)
header_list = ["TreeDepth"]
for column in grid.columns:
# Ignore the type because namedtuples don't realize they have accessible attributes
header_list.append(f"{column.name}")
if column not in ignore_columns:
header_list.append(f"{column.name}")
writer = csv.DictWriter(
outfd, header_list, lineterminator="\n", escapechar="\\"
@@ -249,12 +290,20 @@ class CSVRenderer(CLIRenderer):
def visitor(node: interfaces.renderers.TreeNode, accumulator):
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
row = {"TreeDepth": str(max(0, node.path_depth - 1))}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
row[f"{column.name}"] = renderer(node.values[column_index])
if column not in ignore_columns:
line.append(row[f"{column.name}"])
else:
del row[f"{column.name}"]
if self.filter and self.filter.filter(line):
return accumulator
accumulator.writerow(row)
return accumulator
@@ -288,11 +337,12 @@ class PrettyTextRenderer(CLIRenderer):
sys.stderr.write("Formatting...\n")
ignore_columns = self.ignored_columns(grid)
display_alignment = ">"
column_separator = " | "
tree_indent_column = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(20)
random.choices(string.ascii_uppercase + string.digits, k=20)
)
max_column_widths = dict(
[(column.name, len(column.name)) for column in grid.columns]
@@ -306,9 +356,10 @@ class PrettyTextRenderer(CLIRenderer):
max_column_widths[tree_indent_column] = max(
max_column_widths.get(tree_indent_column, 0), node.path_depth
)
line = {}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
rendered_line = []
for column_index, column in enumerate(grid.columns):
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
@@ -319,7 +370,13 @@ class PrettyTextRenderer(CLIRenderer):
max_column_widths[column.name] = max(
max_column_widths.get(column.name, len(column.name)), field_width
)
line[column] = data.split("\n")
if column not in ignore_columns:
line[column] = data.split("\n")
rendered_line.append(data)
if self.filter and self.filter.filter(rendered_line):
return accumulator
accumulator.append((node.path_depth, line))
return accumulator
@@ -333,44 +390,49 @@ class PrettyTextRenderer(CLIRenderer):
format_string_list = [
"{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"
]
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
format_string_list.append(
"{"
+ str(column_index + 1)
+ ":"
+ display_alignment
+ str(max_column_widths[column.name])
+ "s}"
)
column_offset = 0
for column_index, column in enumerate(grid.columns):
if column not in ignore_columns:
format_string_list.append(
"{"
+ str(column_index - column_offset + 1)
+ ":"
+ display_alignment
+ str(max_column_widths[column.name])
+ "s}"
)
else:
column_offset += 1
format_string = column_separator.join(format_string_list) + "\n"
column_titles = [""] + [column.name for column in grid.columns]
column_titles = [""] + [
column.name for column in grid.columns if column not in ignore_columns
]
outfd.write(format_string.format(*column_titles))
for depth, line in final_output:
nums_line = max([len(line[column]) for column in line])
for column in line:
line[column] = line[column] + ([""] * (nums_line - len(line[column])))
if column in ignore_columns:
del line[column]
else:
line[column] = line[column] + (
[""] * (nums_line - len(line[column]))
)
for index in range(nums_line):
if index == 0:
outfd.write(
format_string.format(
"*" * depth,
*[
self.tab_stop(line[column][index])
for column in grid.columns
],
*[self.tab_stop(line[column][index]) for column in line],
)
)
else:
outfd.write(
format_string.format(
" " * depth,
*[
self.tab_stop(line[column][index])
for column in grid.columns
],
*[self.tab_stop(line[column][index]) for column in line],
)
)
@@ -388,10 +450,12 @@ class JsonRenderer(CLIRenderer):
format_hints.HexBytes: quoted_optional(hex_bytes_as_text),
interfaces.renderers.Disassembly: quoted_optional(display_disassembly),
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
datetime.datetime: lambda x: x.isoformat()
if not isinstance(x, interfaces.renderers.BaseAbsentValue)
else None,
bytes: optional(lambda x: " ".join(f"{b:02x}" for b in x)),
datetime.datetime: lambda x: (
x.isoformat()
if not isinstance(x, interfaces.renderers.BaseAbsentValue)
else None
),
"default": lambda x: x,
}
@@ -403,7 +467,7 @@ class JsonRenderer(CLIRenderer):
def output_result(self, outfd, result):
"""Outputs the JSON data to a file in a particular format"""
outfd.write("{}\n".format(json.dumps(result, indent=2, sort_keys=True)))
outfd.write(f"{json.dumps(result, indent=2, sort_keys=True)}\n")
def render(self, grid: interfaces.renderers.TreeGrid):
outfd = sys.stdout
@@ -414,6 +478,8 @@ class JsonRenderer(CLIRenderer):
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]]],
@@ -421,8 +487,10 @@ class JsonRenderer(CLIRenderer):
# 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": []}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
line = []
for column_index, column in enumerate(grid.columns):
if column in ignore_columns:
continue
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
@@ -430,6 +498,11 @@ class JsonRenderer(CLIRenderer):
if isinstance(data, interfaces.renderers.BaseAbsentValue):
data = None
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)
else:
+18 -3
View File
@@ -5,7 +5,7 @@
import argparse
import gettext
import re
from typing import List, Optional, Sequence, Any, Union
from typing import Optional, Sequence, Any, Union
# This effectively overrides/monkeypatches the core argparse module to provide more helpful output around choices
@@ -21,8 +21,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__
self.choices = None
def __call__(
self,
@@ -100,3 +98,20 @@ class HelpfulArgParser(argparse.ArgumentParser):
# return the number of arguments matched
return len(match.group(1))
def _check_value(self, action: argparse.Action, value: Any) -> None:
"""This is called to ensure a value is correct/valid
In normal operation, it would check that a value provided is valid and return None
If it was not valid, it would throw an ArgumentError
When people provide a partial plugin name, we want to look for a matching plugin name
which happens in the HelpfulSubparserAction's __call_method
To get there without tripping the check_value failure, we have to prevent the exception
being thrown when the value is a HelpfulSubparserAction. This therefore affects no other
checks for normal parameters.
"""
if not isinstance(action, HelpfulSubparserAction):
super()._check_value(action, value)
return None
+66 -22
View File
@@ -21,13 +21,23 @@ from volatility3.framework import (
plugins,
)
try:
import argcomplete
HAS_ARGCOMPLETE = True
except ImportError:
HAS_ARGCOMPLETE = False
# Make sure we log everything
rootlog = logging.getLogger()
vollog = logging.getLogger()
vollog.setLevel(0)
# Trim the console down by default
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
# Trim the console down by default
console.setFormatter(formatter)
vollog.addHandler(console)
@@ -39,7 +49,7 @@ class VolShell(cli.CommandLine):
python terminal with all the volatility support calls available.
"""
CLI_NAME = "volshell"
CLI_NAME = os.path.basename(sys.argv[0]) # volshell
def __init__(self):
super().__init__()
@@ -53,6 +63,9 @@ class VolShell(cli.CommandLine):
framework.require_interface_version(2, 0, 0)
# Load up system defaults
delayed_logs, default_config = self.load_system_defaults("volshell.json")
parser = argparse.ArgumentParser(
prog=self.CLI_NAME,
description="A tool for interactivate forensic analysis of memory images",
@@ -146,6 +159,21 @@ class VolShell(cli.CommandLine):
default=constants.CACHE_PATH,
type=str,
)
isf_group = parser.add_mutually_exclusive_group()
isf_group.add_argument(
"--offline",
help="Do not search online for additional JSON files",
default=False,
action="store_true",
)
isf_group.add_argument(
"-u",
"--remote-isf-url",
metavar="URL",
help="Search online for ISF json files",
default=constants.REMOTE_ISF_URL,
type=str,
)
# Volshell specific flags
os_specific = parser.add_mutually_exclusive_group(required=False)
@@ -167,10 +195,35 @@ class VolShell(cli.CommandLine):
"-m", "--mac", default=False, action="store_true", help="Run a Mac volshell"
)
parser.set_defaults(**default_config)
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
# processed the plugin choice or had the plugin subparser added.
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
partial_args, _ = parser.parse_known_args(known_args)
### Start up logging
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
file_logger.setLevel(0)
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
vollog.addHandler(file_logger)
vollog.info("Logging started")
self.order_extra_verbose_levels()
if partial_args.verbosity < 3:
console.setLevel(logging.WARNING - (partial_args.verbosity * 10))
else:
console.setLevel(logging.DEBUG - (partial_args.verbosity - 2))
for level, msg in delayed_logs:
vollog.log(level, msg)
### Alter constants if necessary
if partial_args.plugin_dirs:
volatility3.plugins.__path__ = [
os.path.abspath(p) for p in partial_args.plugin_dirs.split(";")
@@ -187,25 +240,14 @@ class VolShell(cli.CommandLine):
vollog.info(f"Volatility plugins path: {volatility3.plugins.__path__}")
vollog.info(f"Volatility symbols path: {volatility3.symbols.__path__}")
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
file_logger.setLevel(0)
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
vollog.addHandler(file_logger)
vollog.info("Logging started")
if partial_args.verbosity < 3:
console.setLevel(30 - (partial_args.verbosity * 10))
else:
console.setLevel(10 - (partial_args.verbosity - 2))
if partial_args.clear_cache:
framework.clear_cache()
if partial_args.offline:
constants.OFFLINE = partial_args.offline
elif partial_args.remote_isf_url:
constants.REMOTE_ISF_URL = partial_args.remote_isf_url
# Do the initialization
ctx = contexts.Context() # Construct a blank context
failures = framework.import_files(
@@ -240,9 +282,7 @@ class VolShell(cli.CommandLine):
for plugin in volshell_plugin_list:
subparser = parser.add_argument_group(
title=plugin.capitalize(),
description="Configuration options based on {} options".format(
plugin.capitalize()
),
description=f"Configuration options based on {plugin.capitalize()} options",
)
self.populate_requirements_argparse(subparser, volshell_plugin_list[plugin])
configurables_list[plugin] = volshell_plugin_list[plugin]
@@ -253,6 +293,10 @@ class VolShell(cli.CommandLine):
# Hand the plugin requirements over to the CLI (us) and let it construct the config tree
# Run the argparser
if HAS_ARGCOMPLETE:
# The autocompletion line must be after the partial_arg handling, so that it doesn't trip it
# before all the plugins have been added
argcomplete.autocomplete(parser)
args = parser.parse_args()
vollog.log(
@@ -285,7 +329,7 @@ class VolShell(cli.CommandLine):
# UI fills in the config, here we load it from the config file and do it before we process the CL parameters
if args.config:
with open(args.config, "r") as f:
with open(args.config) as f:
json_val = json.load(f)
ctx.config.splice(
plugin_config_path,
+57 -29
View File
@@ -14,7 +14,7 @@ from urllib import parse, request
from volatility3.cli import text_renderer, volshell
from volatility3.framework import exceptions, interfaces, objects, plugins, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import intel, physical, resources
from volatility3.framework.layers import intel, physical, resources, scanners
try:
import capstone
@@ -29,6 +29,8 @@ class Volshell(interfaces.plugins.PluginInterface):
_required_framework_version = (2, 0, 0)
DEFAULT_NUM_DISPLAY_BYTES = 128
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__current_layer: Optional[str] = None
@@ -58,7 +60,7 @@ class Volshell(interfaces.plugins.PluginInterface):
]
def run(
self, additional_locals: Dict[str, Any] = None
self, additional_locals: Dict[str, Any] = {}
) -> interfaces.renderers.TreeGrid:
"""Runs the interactive volshell plugin.
@@ -94,7 +96,10 @@ class Volshell(interfaces.plugins.PluginInterface):
"""
sys.ps1 = f"({self.current_layer}) >>> "
self.__console = code.InteractiveConsole(locals=self._construct_locals_dict())
# Dict self._construct_locals_dict() will have priority on keys
combined_locals = additional_locals.copy()
combined_locals.update(self._construct_locals_dict())
self.__console = code.InteractiveConsole(locals=combined_locals)
# Since we have to do work to add the option only once for all different modes of volshell, we can't
# rely on the default having been set
if self.config.get("script", None) is not None:
@@ -112,7 +117,7 @@ class Volshell(interfaces.plugins.PluginInterface):
variables = []
print("\nMethods:")
for aliases, item in self.construct_locals():
for aliases, item in sorted(self.construct_locals()):
name = ", ".join(aliases)
if item.__doc__ and callable(item):
print(f"* {name}")
@@ -125,8 +130,7 @@ class Volshell(interfaces.plugins.PluginInterface):
print(f" {var}")
def construct_locals(self) -> List[Tuple[List[str], Any]]:
"""Returns a dictionary listing the functions to be added to the
environment."""
"""Returns a listing of the functions to be added to the environment."""
return [
(["dt", "display_type"], self.display_type),
(["db", "display_bytes"], self.display_bytes),
@@ -147,6 +151,7 @@ class Volshell(interfaces.plugins.PluginInterface):
(["cc", "create_configurable"], self.create_configurable),
(["lf", "load_file"], self.load_file),
(["rs", "run_script"], self.run_script),
(["rx", "regex_scan"], self.regex_scan),
]
def _construct_locals_dict(self) -> Dict[str, Any]:
@@ -198,7 +203,7 @@ class Volshell(interfaces.plugins.PluginInterface):
connector = " "
if chunk_size < 2:
connector = ""
ascii_data = connector.join([self._ascii_bytes(x) for x in valid_data])
ascii_data = connector.join(self._ascii_bytes(x) for x in valid_data)
print(hex(offset), " ", hex_data, " ", ascii_data)
offset += 16
@@ -235,7 +240,7 @@ class Volshell(interfaces.plugins.PluginInterface):
return None
return self.context.modules[self.current_kernel_name]
def change_layer(self, layer_name: str = None):
def change_layer(self, layer_name: Optional[str] = None):
"""Changes the current default layer"""
if not layer_name:
layer_name = self.current_layer
@@ -245,7 +250,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_layer = layer_name
sys.ps1 = f"({self.current_layer}) >>> "
def change_symbol_table(self, symbol_table_name: str = None):
def change_symbol_table(self, symbol_table_name: Optional[str] = None):
"""Changes the current_symbol_table"""
if not symbol_table_name:
print("No symbol table provided, not changing current symbol table")
@@ -257,7 +262,7 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_symbol_table = symbol_table_name
print(f"Current Symbol Table: {self.current_symbol_table}")
def change_kernel(self, kernel_name: str = None):
def change_kernel(self, kernel_name: Optional[str] = None):
if not kernel_name:
print("No kernel module name provided, not changing current kernel")
if kernel_name not in self.context.modules:
@@ -266,27 +271,52 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_kernel_name = kernel_name
print(f"Current kernel : {self.current_kernel_name}")
def display_bytes(self, offset, count=128, layer_name=None):
def display_bytes(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None):
"""Displays byte values and ASCII characters"""
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data)
def display_quadwords(self, offset, count=128, layer_name=None):
def display_quadwords(
self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None
):
"""Displays quad-word values (8 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="Q")
def display_doublewords(self, offset, count=128, layer_name=None):
def display_doublewords(
self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None
):
"""Displays double-word values (4 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="I")
def display_words(self, offset, count=128, layer_name=None):
def display_words(self, offset, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None):
"""Displays word values (2 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="H")
def disassemble(self, offset, count=128, layer_name=None, architecture=None):
def regex_scan(self, pattern, count=DEFAULT_NUM_DISPLAY_BYTES, layer_name=None):
"""Scans for regex pattern in layer using RegExScanner."""
if not isinstance(pattern, bytes):
raise TypeError("pattern must be bytes, e.g. rx(b'pattern')")
layer_name_to_scan = layer_name or self.current_layer
for offset in self.context.layers[layer_name_to_scan].scan(
scanner=scanners.RegExScanner(pattern),
context=self.context,
):
remaining_data = self._read_data(
offset, count=count, layer_name=layer_name_to_scan
)
self._display_data(offset, remaining_data)
print("")
def disassemble(
self,
offset,
count=DEFAULT_NUM_DISPLAY_BYTES,
layer_name=None,
architecture=None,
):
"""Disassembles a number of instructions from the code at offset"""
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
if not has_capstone:
@@ -317,7 +347,7 @@ class Volshell(interfaces.plugins.PluginInterface):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if not isinstance(
@@ -449,7 +479,7 @@ class Volshell(interfaces.plugins.PluginInterface):
if treegrid is not None:
self.render_treegrid(treegrid)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
print("No symbol table provided")
@@ -523,17 +553,16 @@ class Volshell(interfaces.plugins.PluginInterface):
if argname in kwargs:
del kwargs[argname]
for keyword in kwargs:
val = kwargs[keyword]
if not isinstance(
val, interfaces.configuration.BasicTypes
) and not isinstance(val, list):
if not isinstance(val, list) or all(
[isinstance(x, interfaces.configuration.BasicTypes) for x in val]
):
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
for keyword, val in kwargs.items():
BasicType_or_list_of_BasicType = False # excludes list of lists
if isinstance(val, interfaces.configuration.BasicTypes):
BasicType_or_list_of_BasicType = True
if all(isinstance(x, interfaces.configuration.BasicTypes) for x in val):
BasicType_or_list_of_BasicType = True
if not BasicType_or_list_of_BasicType:
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
self.context.config[config_path + "." + keyword] = val
constructed = clazz(self.context, config_path, **constructor_args)
@@ -555,7 +584,6 @@ class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface):
def writelines(self, lines: Iterable[bytes]):
"""Dummy method"""
pass
def write(self, b: bytes):
"""Dummy method"""
+81 -4
View File
@@ -2,7 +2,8 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from enum import Enum
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -10,6 +11,16 @@ from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
# Could import the enum from psscan.py to avoid code duplication
class DescExitStateEnum(Enum):
"""Enum for linux task exit_state as defined in include/linux/sched.h"""
TASK_RUNNING = 0x00000000
EXIT_DEAD = 0x00000010
EXIT_ZOMBIE = 0x00000020
EXIT_TRACE = EXIT_ZOMBIE | EXIT_DEAD
class Volshell(generic.Volshell):
"""Shell environment to directly interact with a linux memory image."""
@@ -20,7 +31,7 @@ class Volshell(generic.Volshell):
name="kernel", description="Linux kernel module"
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
@@ -40,6 +51,71 @@ class Volshell(generic.Volshell):
return None
print(f"No task with task ID {pid} found")
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Return the task_struct object that matches the pid. If a physical or a virtual address is provided, construct the task_struct object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID to search for
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: task_struct Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
vmlinux_module_name = self.config["kernel"]
vmlinux = self.context.modules[vmlinux_module_name]
kernel_layer_name = vmlinux.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
task_struct_symbol = vmlinux.symbol_table_name + constants.BANG + "task_struct"
if virtaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
if physaddr is not None:
task = self.context.object(
task_struct_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
if physaddr is not None or virtaddr is not None:
try:
DescExitStateEnum(task.exit_state)
except ValueError:
print(
f"task_struct @ {hex(task.vol.offset)} as exit_state {task.exit_state} is likely not valid"
)
if not (0 < task.pid < 65535):
print(
f"task_struct @ {hex(task.vol.offset)} as pid {task.pid} is likely not valid"
)
return task
if pid is not None:
tasks = self.list_tasks()
for task in tasks:
if task.pid == pid:
return task
print(f"No task with task ID {pid} found")
return None
def list_tasks(self):
"""Returns a list of task objects from the primary layer"""
# We always use the main kernel memory and associated symbols
@@ -50,6 +126,7 @@ class Volshell(generic.Volshell):
result += [
(["ct", "change_task", "cp"], self.change_task),
(["lt", "list_tasks", "ps"], self.list_tasks),
(["gp", "get_process", "get_task"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
@@ -61,7 +138,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -69,7 +146,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+3 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -63,7 +63,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -71,7 +71,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+59 -3
View File
@@ -2,7 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from typing import Any, List, Tuple, Union
from typing import Any, List, Optional, Tuple, Union
from volatility3.cli.volshell import generic
from volatility3.framework import constants, interfaces
@@ -44,11 +44,67 @@ class Volshell(generic.Volshell):
)
)
def get_process(self, pid=None, virtaddr=None, physaddr=None):
"""Returns the _EPROCESS object that matches the pid. If a physical or a virtual address is provided, construct the _EPROCESS object at said address. Only one parameter is allowed.
Args:
pid (int, optional): PID / UniqueProcessId to search for.
virtaddr (int, optional): Virtual address to construct object at
physaddr (int, optional): Physical address to construct object at
Returns:
ObjectInterface: _EPROCESS Object
"""
if sum(1 if x is not None else 0 for x in [pid, virtaddr, physaddr]) != 1:
print("Only one parameter is accepted")
return None
kernel_name = self.config["kernel"]
kernel = self.context.modules[kernel_name]
kernel_layer_name = kernel.layer_name
kernel_layer = self.context.layers[kernel_layer_name]
memory_layer_name = kernel_layer.dependencies[0]
eprocess_symbol = kernel.symbol_table_name + constants.BANG + "_EPROCESS"
if virtaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=kernel_layer_name,
offset=virtaddr,
)
return eproc
if physaddr is not None:
eproc = self.context.object(
eprocess_symbol,
layer_name=memory_layer_name,
offset=physaddr,
native_layer_name=kernel_layer_name,
)
return eproc
if pid is not None:
processes = self.list_processes()
for process in processes:
if process.UniqueProcessId == pid:
return process
print(f"No process with process ID {pid} found")
return None
return None
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(["cp", "change_process"], self.change_process),
(["lp", "list_processes", "ps"], self.list_processes),
(["gp", "get_process"], self.get_process),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get("pid", None) is not None:
@@ -60,7 +116,7 @@ class Volshell(generic.Volshell):
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
offset: Optional[int] = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
@@ -68,7 +124,7 @@ class Volshell(generic.Volshell):
object = self.current_symbol_table + constants.BANG + object
return super().display_type(object, offset)
def display_symbols(self, symbol_table: str = None):
def display_symbols(self, symbol_table: Optional[str] = None):
"""Prints an alphabetical list of symbols for a symbol table"""
if symbol_table is None:
symbol_table = self.current_symbol_table
+91 -42
View File
@@ -6,31 +6,29 @@
import glob
import sys
import zipfile
required_python_version = (3, 7, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
"Volatility framework requires python version {}.{}.{} or greater".format(
*required_python_version
)
)
import importlib
import inspect
import logging
import os
import traceback
from typing import Any, Dict, Generator, List, Tuple, Type, TypeVar
import functools
import warnings
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar
from volatility3.framework import constants, interfaces
from volatility3.framework import constants, exceptions, interfaces
from volatility3.framework.configuration import requirements
if (
sys.version_info.major != constants.REQUIRED_PYTHON_VERSION[0]
or sys.version_info.minor < constants.REQUIRED_PYTHON_VERSION[1]
or (
sys.version_info.minor == constants.REQUIRED_PYTHON_VERSION[1]
and sys.version_info.micro < constants.REQUIRED_PYTHON_VERSION[2]
)
):
raise RuntimeError(
f"Volatility framework requires python version {'.'.join(str(x) for x in constants.REQUIRED_PYTHON_VERSION)} or greater"
)
# ##
#
@@ -56,27 +54,80 @@ def require_interface_version(*args) -> None:
if len(args):
if args[0] != interface_version()[0]:
raise RuntimeError(
"Framework interface version {} is incompatible with required version {}".format(
interface_version()[0], args[0]
)
f"Framework interface version {interface_version()[0]} is incompatible with required version {args[0]}"
)
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]]),
".".join(str(x) for x in interface_version()[0:2]),
".".join(str(x) for x in args[0:2]),
)
)
class NonInheritable(object):
class Deprecation:
"""Deprecation related methods."""
@staticmethod
def deprecated_method(
replacement: Callable,
replacement_version: Tuple[int, int, int] = None,
additional_information: str = "",
):
"""A decorator for marking functions as deprecated.
Args:
replacement: The replacement function overriding the deprecated API, in the form of a Callable (typically a method)
replacement_version: The "replacement" base class version that the deprecated method expects before proxying to it. This implies that "replacement" is a method from a class that inherits from VersionableInterface.
additional_information: Information appended at the end of the deprecation message
"""
def decorator(deprecated_func):
@functools.wraps(deprecated_func)
def wrapper(*args, **kwargs):
nonlocal replacement, replacement_version, additional_information
# Prevent version mismatches between deprecated (proxy) methods and the ones they proxy
if (
replacement_version is not None
and callable(replacement)
and hasattr(replacement, "__self__")
):
replacement_base_class = replacement.__self__
# Verify that the base class inherits from VersionableInterface
if inspect.isclass(replacement_base_class) and issubclass(
replacement_base_class,
interfaces.configuration.VersionableInterface,
):
# SemVer check
if not requirements.VersionRequirement.matches_required(
replacement_version, replacement_base_class.version
):
raise exceptions.VersionMismatchException(
deprecated_func,
replacement_base_class,
replacement_version,
"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, 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)
return wrapper
return decorator
class NonInheritable:
def __init__(self, value: Any, cls: Type) -> None:
self.default_value = value
self.cls = cls
def __get__(self, obj: Any, get_type: Type = None) -> Any:
if type == self.cls:
def __get__(self, obj: Any, get_type: Type = Optional[None]) -> Any:
if type is self.cls:
if hasattr(self.default_value, "__get__"):
return self.default_value.__get__(obj, get_type)
return self.default_value
@@ -99,8 +150,7 @@ def class_subclasses(cls: Type[T]) -> Generator[Type[T], None, None]:
# The typing system is not clever enough to realize that clazz has a hidden attr after the hasattr check
if not hasattr(clazz, "hidden") or not clazz.hidden: # type: ignore
yield clazz
for return_value in class_subclasses(clazz):
yield return_value
yield from class_subclasses(clazz)
def import_files(base_module, ignore_errors: bool = False) -> List[str]:
@@ -161,11 +211,7 @@ def import_files(base_module, ignore_errors: bool = False) -> List[str]:
def _filter_files(filename: str):
"""Ensures that a filename traversed is an importable python file"""
return (
filename.endswith(".py")
or filename.endswith(".pyc")
or filename.endswith(".pyo")
) and not filename.startswith("__")
return (filename.endswith((".py", ".pyc"))) and not filename.startswith("__")
def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str]:
@@ -189,9 +235,7 @@ def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str
traceback.TracebackException.from_exception(e).format(chain=True)
)
)
vollog.debug(
"Failed to import module {} based on file: {}".format(module, path)
)
vollog.debug(f"Failed to import module {module} based on file: {path}")
failures.append(module)
if not ignore_errors:
raise
@@ -206,11 +250,10 @@ def _zipwalk(path: str):
if not file.is_dir():
dirlist = zip_results.get(os.path.dirname(file.filename), [])
dirlist.append(os.path.basename(file.filename))
zip_results[
os.path.join(path, os.path.dirname(file.filename))
] = dirlist
for value in zip_results:
yield value, zip_results[value]
zip_results[os.path.join(path, os.path.dirname(file.filename))] = (
dirlist
)
yield from zip_results.items()
def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
@@ -223,8 +266,14 @@ def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
return plugin_list
def clear_cache(complete=False):
def clear_cache(complete=True):
try:
if complete:
glob_pattern = "*.cache"
for cache_filename in glob.glob(
os.path.join(constants.CACHE_PATH, glob_pattern)
):
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")
+259 -29
View File
@@ -3,8 +3,7 @@
#
import logging
import os
from typing import Optional, Tuple, Type
from typing import Optional, Tuple
from volatility3.framework import constants, interfaces
from volatility3.framework.automagic import symbol_cache, symbol_finder
@@ -27,16 +26,6 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempts to identify linux within this layer."""
# Version check the SQlite cache
required = (1, 0, 0)
if not requirements.VersionRequirement.matches_required(
required, symbol_cache.SqliteCache.version
):
vollog.info(
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
)
return None
# Bail out by default unless we can stack properly
layer = context.layers[layer_name]
join = interfaces.configuration.path_join
@@ -46,12 +35,9 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
if isinstance(layer, intel.Intel):
return None
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
linux_banners = symbol_cache.load_cache_manager().get_identifier_dictionary(
operating_system="linux"
)
linux_banners = symbol_cache.SqliteCache(
identifiers_path
).get_identifier_dictionary(operating_system="linux")
# If we have no banners, don't bother scanning
if not linux_banners:
vollog.info(
@@ -76,18 +62,27 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
isf_url=isf_path,
)
context.symbol_space.append(table)
kaslr_shift, aslr_shift = cls.find_aslr(
context, table_name, layer_name, progress_callback=progress_callback
context,
table_name,
layer_name,
progress_callback=progress_callback,
)
layer_class: Type = intel.Intel
if "init_top_pgt" in table.symbols:
layer_class = intel.Intel32e
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_top_pgt"
elif "init_level4_pgt" in table.symbols:
layer_class = intel.Intel32e
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_level4_pgt"
elif "pkmap_count" in table.symbols and table.get_symbol(
"pkmap_count"
).type.count in (512, 2048):
layer_class = intel.LinuxIntelPAE
dtb_symbol_name = "swapper_pg_dir"
else:
layer_class = intel.LinuxIntel
dtb_symbol_name = "swapper_pg_dir"
dtb = cls.virtual_to_physical_address(
@@ -126,7 +121,17 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Tuple[int, int]:
"""Determines the offset of the actual DTB in physical space and its
symbol offset."""
symbol offset.
Args:
context: The context to retrieve required elements (layers, symbol tables) from
symbol_table: The name of the kernel module on which to operate
layer_name: The layer within the context in which the module exists
progress_callback: A function that takes a percentage (and an optional description) that will be called periodically
Returns:
kaslr_shirt and aslr_shift
"""
init_task_symbol = symbol_table + constants.BANG + "init_task"
init_task_json_address = context.symbol_space.get_symbol(
init_task_symbol
@@ -156,10 +161,25 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
and init_task.state.cast("unsigned int") != 0
):
continue
elif init_task.active_mm.cast("long unsigned int") == module.get_symbol(
"init_mm"
).address and init_task.tasks.next.cast(
"long unsigned int"
) == init_task.tasks.prev.cast(
"long unsigned int"
):
# The idle task steals `mm` from previously running task, i.e.,
# `init_mm` is only used as long as no CPU has ever been idle.
# This catches cases where we found a fragment of the
# unrelocated ELF file instead of the running kernel.
continue
# This we get for free
aslr_shift = (
init_task.files.cast("long unsigned int")
int.from_bytes(
init_task.files.cast("bytes", length=init_task.files.vol.size),
byteorder=init_task.files.vol.data_format.byteorder,
)
- module.get_symbol("init_files").address
)
kaslr_shift = init_task_address - cls.virtual_to_physical_address(
@@ -171,9 +191,7 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
if aslr_shift & 0xFFF != 0 or kaslr_shift & 0xFFF != 0:
continue
vollog.debug(
"Linux ASLR shift values determined: physical {:0x} virtual {:0x}".format(
kaslr_shift, aslr_shift
)
f"Linux ASLR shift values determined: physical {kaslr_shift:0x} virtual {aslr_shift:0x}"
)
return kaslr_shift, aslr_shift
@@ -181,8 +199,8 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
vollog.debug("Scanners could not determine any ASLR shifts, using 0 for both")
return 0, 0
@classmethod
def virtual_to_physical_address(cls, addr: int) -> int:
@staticmethod
def virtual_to_physical_address(addr: int) -> int:
"""Converts a virtual linux address to a physical one (does not account
of ASLR)"""
if addr > 0xFFFFFFFF80000000:
@@ -196,5 +214,217 @@ class LinuxSymbolFinder(symbol_finder.SymbolFinder):
banner_config_key = "kernel_banner"
operating_system = "linux"
symbol_class = "volatility3.framework.symbols.linux.LinuxKernelIntermedSymbols"
find_aslr = lambda cls, *args: LinuxIntelStacker.find_aslr(*args)[1]
exclusion_list = ["mac", "windows"]
@classmethod
def find_aslr(cls, *args):
return LinuxIntelStacker.find_aslr(*args)[1]
class LinuxIntelVMCOREINFOStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 34
exclusion_list = ["mac", "windows"]
@staticmethod
def _check_versions() -> bool:
"""Verify the versions of the required modules"""
# Check VMCOREINFO API version
vmcoreinfo_version_required = (1, 0, 0)
if not requirements.VersionRequirement.matches_required(
vmcoreinfo_version_required, linux.VMCoreInfo.version
):
vollog.info(
"VMCOREINFO version not suitable: required %s found %s",
vmcoreinfo_version_required,
linux.VMCoreInfo.version,
)
return False
return True
@classmethod
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempts to identify linux within this layer."""
# Verify the versions of the required modules
if not cls._check_versions():
return None
# Bail out by default unless we can stack properly
layer = context.layers[layer_name]
# Never stack on top of an intel layer
# FIXME: Find a way to improve this check
if isinstance(layer, intel.Intel):
return None
linux_banners = symbol_cache.load_cache_manager().get_identifier_dictionary(
operating_system="linux"
)
if not linux_banners:
# If we have no banners, don't bother scanning
vollog.info(
"No Linux banners found - if this is a linux plugin, please check your "
"symbol files location"
)
return None
vmcoreinfo_elf_notes_iter = linux.VMCoreInfo.search_vmcoreinfo_elf_note(
context=context,
layer_name=layer_name,
progress_callback=progress_callback,
)
# Iterate through each VMCOREINFO ELF note found, using the first one that is valid.
for _vmcoreinfo_offset, vmcoreinfo in vmcoreinfo_elf_notes_iter:
shifts = cls._vmcoreinfo_find_aslr(vmcoreinfo)
if not shifts:
# Let's try the next VMCOREINFO, in case this one isn't correct.
continue
kaslr_shift, aslr_shift = shifts
dtb = cls._vmcoreinfo_get_dtb(vmcoreinfo, aslr_shift, kaslr_shift)
if dtb is None:
# Discard this VMCOREINFO immediately
continue
is_32bit, is_pae = cls._vmcoreinfo_is_32bit(vmcoreinfo)
if is_32bit:
layer_class = intel.IntelPAE if is_pae else intel.Intel
else:
layer_class = intel.Intel32e
uts_release = vmcoreinfo["OSRELEASE"]
# See how linux_banner constant is built in the linux kernel
linux_version_prefix = f"Linux version {uts_release} (".encode()
valid_banners = [
x for x in linux_banners if x and x.startswith(linux_version_prefix)
]
if not valid_banners:
# There's no banner matching this VMCOREINFO, keep trying with the next one
continue
elif len(valid_banners) == 1:
# Usually, we narrow down the Linux banner list to a single element.
# Using BytesScanner here is slightly faster than MultiStringScanner.
scanner = scanners.BytesScanner(valid_banners[0])
else:
scanner = scanners.MultiStringScanner(valid_banners)
join = interfaces.configuration.path_join
for match in layer.scan(
context=context, scanner=scanner, progress_callback=progress_callback
):
# Unfortunately, the scanners do not maintain a consistent interface
banner = match[1] if isinstance(match, Tuple) else valid_banners[0]
isf_path = linux_banners.get(banner, None)
if not isf_path:
vollog.warning(
"Identified banner %r, but no matching ISF is available.",
banner,
)
continue
vollog.debug("Identified banner: %r", banner)
table_name = context.symbol_space.free_table_name("LintelStacker")
table = linux.LinuxKernelIntermedSymbols(
context,
f"temporary.{table_name}",
name=table_name,
isf_url=isf_path,
)
context.symbol_space.append(table)
# Build the new layer
new_layer_name = context.layers.free_layer_name("primary")
config_path = join("vmcoreinfo", new_layer_name)
kernel_banner = LinuxSymbolFinder.banner_config_key
banner_str = banner.decode(encoding="latin-1")
context.config[join(config_path, kernel_banner)] = banner_str
context.config[join(config_path, "memory_layer")] = layer_name
context.config[join(config_path, "page_map_offset")] = dtb
context.config[join(config_path, "kernel_virtual_offset")] = aslr_shift
layer = layer_class(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "Linux"},
)
if layer:
vollog.debug(
"Values found in VMCOREINFO: KASLR=0x%x, ASLR=0x%x, DTB=0x%x",
kaslr_shift,
aslr_shift,
dtb,
)
return layer
vollog.debug("No suitable linux banner could be matched")
return None
@staticmethod
def _vmcoreinfo_find_aslr(vmcoreinfo) -> Tuple[int, int]:
phys_base = vmcoreinfo.get("NUMBER(phys_base)")
if phys_base is None:
# In kernel < 4.10, there may be a SYMBOL(phys_base), but as noted in the
# c401721ecd1dcb0a428aa5d6832ee05ffbdbffbbe commit comment, this value
# isn't useful for calculating the physical address.
# There's nothing we can do here, so let's try with the next VMCOREINFO or
# the next Stacker.
return None
# kernels 3.14 (b6085a865762236bb84934161273cdac6dd11c2d) KERNELOFFSET was added
kerneloffset = vmcoreinfo.get("KERNELOFFSET")
if kerneloffset is None:
# kernels < 3.14 if KERNELOFFSET is missing, KASLR might not be implemented.
# Oddly, NUMBER(phys_base) is present without it. To be safe, proceed only
# if both are present.
return None
aslr_shift = kerneloffset
kaslr_shift = phys_base + aslr_shift
return kaslr_shift, aslr_shift
@staticmethod
def _vmcoreinfo_get_dtb(vmcoreinfo, aslr_shift, kaslr_shift) -> int:
"""Returns the page global directory physical address (a.k.a DTB or PGD)"""
# In x86-64, since kernels 2.5.22 swapper_pg_dir is a macro to the respective pgd.
# First, in e3ebadd95cb621e2c7436f3d3646447ac9d5c16d to init_level4_pgt, and later
# in kernels 4.13 in 65ade2f872b474fa8a04c2d397783350326634e6) to init_top_pgt.
# In x86-32, the pgd is swapper_pg_dir. So, in any case, for VMCOREINFO
# SYMBOL(swapper_pg_dir) will always have the right value.
dtb_vaddr = vmcoreinfo.get("SYMBOL(swapper_pg_dir)")
if dtb_vaddr is None:
# Abort, it should be present
return None
dtb_paddr = (
LinuxIntelStacker.virtual_to_physical_address(dtb_vaddr)
- aslr_shift
+ kaslr_shift
)
return dtb_paddr
@staticmethod
def _vmcoreinfo_is_32bit(vmcoreinfo) -> Tuple[bool, bool]:
"""Returns a tuple of booleans with is_32bit and is_pae values"""
is_pae = vmcoreinfo.get("CONFIG_X86_PAE", "n") == "y"
if is_pae:
is_32bit = True
else:
# Check the swapper_pg_dir virtual address size
dtb_vaddr = vmcoreinfo["SYMBOL(swapper_pg_dir)"]
is_32bit = dtb_vaddr <= 2**32
return is_32bit, is_pae
+6 -21
View File
@@ -3,13 +3,11 @@
#
import logging
import os
import struct
from typing import Optional
from volatility3.framework import constants, exceptions, interfaces, layers
from volatility3.framework.automagic import symbol_cache, symbol_finder
from volatility3.framework.configuration import requirements
from volatility3.framework.layers import intel, scanners
from volatility3.framework.symbols import mac
@@ -28,16 +26,6 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempts to identify mac within this layer."""
# Version check the SQlite cache
required = (1, 0, 0)
if not requirements.VersionRequirement.matches_required(
required, symbol_cache.SqliteCache.version
):
vollog.info(
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
)
return None
# Bail out by default unless we can stack properly
layer = context.layers[layer_name]
new_layer = None
@@ -48,12 +36,9 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
if isinstance(layer, intel.Intel):
return None
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
mac_banners = symbol_cache.load_cache_manager().get_identifier_dictionary(
operating_system="mac"
)
mac_banners = symbol_cache.SqliteCache(
identifiers_path
).get_identifier_dictionary(operating_system="mac")
# If we have no banners, don't bother scanning
if not mac_banners:
vollog.info(
@@ -138,9 +123,9 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
config_path = join("automagic", "MacIntelHelper", new_layer_name)
context.config[join(config_path, "memory_layer")] = layer_name
context.config[join(config_path, "page_map_offset")] = dtb
context.config[
join(config_path, MacSymbolFinder.banner_config_key)
] = str(banner, "latin-1")
context.config[join(config_path, MacSymbolFinder.banner_config_key)] = (
str(banner, "latin-1")
)
new_layer = intel.Intel32e(
context,
@@ -197,7 +182,7 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
aslr_shift = 0
for offset, banner in offset_generator:
banner_major, banner_minor = [int(x) for x in banner[22:].split(b".")[0:2]]
banner_major, banner_minor = (int(x) for x in banner[22:].split(b".")[0:2])
tmp_aslr_shift = offset - cls.virtual_to_physical_address(
version_json_address
+3 -3
View File
@@ -34,9 +34,9 @@ class KernelModule(interfaces.automagic.AutomagicInterface):
return None
# The requirement is unfulfilled and is a ModuleRequirement
context.config[
interfaces.configuration.path_join(new_config_path, "class")
] = "volatility3.framework.contexts.Module"
context.config[interfaces.configuration.path_join(new_config_path, "class")] = (
"volatility3.framework.contexts.Module"
)
for req in requirement.requirements:
if (
+73 -7
View File
@@ -209,17 +209,13 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
try:
kvp = vlayer.mapping(kvo, 0)
if any(
[
(p == kernel["mz_offset"] and layer_name == physical_layer_name)
for (_, _, p, _, layer_name) in kvp
]
(p == kernel["mz_offset"] and layer_name == physical_layer_name)
for (_, _, p, _, layer_name) in kvp
):
return (virtual_layer_name, kvo, kernel)
else:
vollog.debug(
"Potential kernel_virtual_offset did not map to expected location: {}".format(
hex(kvo)
)
f"Potential kernel_virtual_offset did not map to expected location: {hex(kvo)}"
)
except exceptions.InvalidAddressException:
vollog.debug(
@@ -272,6 +268,10 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
progress_callback=progress_callback,
)
for kernel in kernels:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Testing potential kernel for {kernel.get('pdb_name', 'Unknown')} at {kernel.get('signature_offset', -1):x} with MZ offset at {(kernel.get('mz_offset', -1) or -1):x}",
)
valid_kernel = test_kernel(physical_layer_name, virtual_layer_name, kernel)
if valid_kernel is not None:
break
@@ -376,8 +376,74 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
valid_kernel = (virtual_layer_name, address, res[0])
return valid_kernel
def method_low_stub_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
# This method is only valid for x64 systems
if not isinstance(vlayer, intel.Intel32e):
return None
kernel_hint = 0
kernel_base = 0
physical_layer = context.layers.get("memory_layer")
# Try locating kernel base via x64 Low Stub in lower 1MB starting from second page (4KB)
# If "Discard Low Memory" setting is disabled in BIOS, the Low Stub may be at the third/fourth or further pages
for offset in range(0x1000, 0x100000, 0x1000):
try:
jmp_and_completion_values = int.from_bytes(
physical_layer.read(offset, 0x8), "little"
)
if (
0xFFFFFFFFFFFF00FF & jmp_and_completion_values
!= constants.windows.JMP_AND_COMPLETION_SIGNATURE
):
continue
cr3_value = int.from_bytes(
physical_layer.read(
offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8
),
"little",
)
# Compare previously observed valid page table address that's stored in vlayer._initial_entry
# with PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3
# which was observed to be an invalid page address, so add 1 (to make it valid too)
if (cr3_value + 1) != vlayer._initial_entry:
continue
potential_kernel_hint = int.from_bytes(
physical_layer.read(
offset
+ constants.windows.PROCESSOR_START_BLOCK_LM_TARGET_OFFSET,
0x8,
),
"little",
)
if 0x3 & potential_kernel_hint:
continue
kernel_hint = potential_kernel_hint & 0xFFFFFFFFFFFF
kernel_base = kernel_hint & (~0x1FFFFF) & 0xFFFFFFFFFFFF
break
except exceptions.InvalidAddressException:
continue
if kernel_base:
# Scanning 32mb in 2mb chunks for the 'ntoskrnl' base address
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
)
if valid_kernel:
return valid_kernel
kernel_base -= 0x200000
return None
# List of methods to be run, in order, to determine the valid kernels
methods = [
method_low_stub_offset,
method_kdbg_offset,
method_module_offset,
method_fixed_mapping,
+3 -1
View File
@@ -166,7 +166,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
cls,
context: interfaces.context.ContextInterface,
initial_layer: str,
stack_set: List[Type[interfaces.automagic.StackerLayerInterface]] = None,
stack_set: Optional[
List[Type[interfaces.automagic.StackerLayerInterface]]
] = None,
progress_callback: constants.ProgressCallback = None,
):
"""Stacks as many possible layers on top of the initial layer as can be done.
@@ -104,10 +104,11 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
for subclazz in framework.class_subclasses(IdentifierProcessor):
self._classifiers[subclazz.operating_system] = subclazz
@abstractmethod
def add_identifier(self, location: str, operating_system: str, identifier: str):
"""Adds an identifier to the store"""
pass
@abstractmethod
def find_location(
self, identifier: bytes, operating_system: Optional[str]
) -> Optional[str]:
@@ -120,19 +121,19 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
Returns:
The location of the symbols file that matches the identifier
"""
pass
@abstractmethod
def get_local_locations(self) -> Iterable[str]:
"""Returns a list of all the local locations"""
pass
@abstractmethod
def update(self):
"""Locates all files under the symbol directories. Updates the cache with additions, modifications and removals.
This also updates remote locations based on a cache timeout.
"""
pass
@abstractmethod
def get_identifier_dictionary(
self, operating_system: Optional[str] = None, local_only: bool = False
) -> Dict[bytes, str]:
@@ -145,16 +146,16 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
Returns:
A dictionary of identifiers mapped to a location
"""
pass
@abstractmethod
def get_identifier(self, location: str) -> Optional[bytes]:
"""Returns an identifier based on a specific location or None"""
pass
@abstractmethod
def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]:
"""Returns all identifiers for a particular operating system"""
pass
@abstractmethod
def get_location_statistics(
self, location: str
) -> Optional[Tuple[int, int, int, int]]:
@@ -164,6 +165,7 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
A tuple of base_types, types, enums, symbols, or None is location not found
"""
@abstractmethod
def get_hash(self, location: str) -> Optional[str]:
"""Returns the hash of the JSON from within a location ISF"""
@@ -299,6 +301,13 @@ class SqliteCache(CacheManagerInterface):
This also updates remote locations based on a cache timeout.
"""
if progress_callback is None:
def dummy_progress(*args, **kargs) -> None:
return None
progress_callback = dummy_progress
on_disk_locations = set(
[
filename
@@ -492,6 +501,21 @@ class SqliteCache(CacheManagerInterface):
return output
def load_cache_manager(cache_file: Optional[str] = None) -> CacheManagerInterface:
"""Loads a cache manager based on a specific cache file"""
if cache_file is None:
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")
with open(cache_file, "rb") as fp:
header = fp.read(4)
if header not in [b"SQLi"]:
raise ValueError("Identifier file not in recognized format")
# Currently only one choice, so use that
return SqliteCache(cache_file)
### Automagic
@@ -557,6 +581,6 @@ class RemoteIdentifierFormat:
try:
subrbf = RemoteIdentifierFormat(location)
yield from subrbf.process(identifiers, operating_system)
except IOError:
except OSError:
vollog.debug(f"Remote file not found: {location}")
return identifiers
@@ -4,7 +4,7 @@
import logging
import os
from typing import Any, Callable, Iterable, List, Optional, Tuple
from typing import Callable, List, Optional, Tuple
from volatility3.framework import constants, interfaces, layers
from volatility3.framework.automagic import symbol_cache
@@ -142,26 +142,49 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
)
for _, banner in banner_list:
vollog.debug(f"Identified banner: {repr(banner)}")
symbol_files = self.banners.get(banner, None)
if symbol_files:
isf_path = symbol_files
vollog.debug(f"Using symbol library: {symbol_files}")
vollog.debug(f"Identified banner: {banner!r}")
symbols_file = self.banners.get(banner, None)
if symbols_file:
isf_path = symbols_file
vollog.debug(f"Using symbol library: {symbols_file}")
clazz = self.symbol_class
# Set the discovered options
path_join = interfaces.configuration.path_join
context.config[
path_join(config_path, requirement.name, "class")
] = clazz
context.config[
path_join(config_path, requirement.name, "isf_url")
] = isf_path
context.config[path_join(config_path, requirement.name, "class")] = (
clazz
)
context.config[path_join(config_path, requirement.name, "isf_url")] = (
isf_path
)
context.config[
path_join(config_path, requirement.name, "symbol_mask")
] = layer.address_mask
# Keep track of the existing table names so we know which ones were added
old_table_names = set(context.symbol_space)
# Construct the appropriate symbol table
requirement.construct(context, config_path)
new_table_names = set(context.symbol_space) - old_table_names
# It should add only one symbol table. Ignore the next steps if it doesn't
if len(new_table_names) == 1:
new_table_name = new_table_names.pop()
symbol_table = context.symbol_space[new_table_name]
producer_metadata = symbol_table.producer
vollog.debug(
f"producer_name: {producer_metadata.name}, producer_version: {producer_metadata.version_string}"
)
symbol_metadata = symbol_table.metadata
vollog.debug("Types:")
for types_source_dict in symbol_metadata.get_types_sources():
vollog.debug(f"\t{types_source_dict}")
vollog.debug("Symbols:")
for symbol_source_dict in symbol_metadata.get_symbols_sources():
vollog.debug(f"\t{symbol_source_dict}")
break
else:
vollog.debug(f"Symbol library path not found for: {banner}")
+10 -10
View File
@@ -402,19 +402,19 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
if swap_location:
context.config[current_layer_path] = current_layer_name
try:
context.config[
layer_loc_path
] = requirements.URIRequirement.location_from_file(
swap_location
context.config[layer_loc_path] = (
requirements.URIRequirement.location_from_file(
swap_location
)
)
except ValueError:
vollog.warning(
f"Volatility swap_location {swap_location} could not be validated - swap layer disabled"
)
continue
context.config[
layer_class_path
] = "volatility3.framework.layers.physical.FileLayer"
context.config[layer_class_path] = (
"volatility3.framework.layers.physical.FileLayer"
)
# Add the requirement
new_req = requirements.TranslationLayerRequirement(
@@ -424,9 +424,9 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
)
swap_req.add_requirement(new_req)
context.config[
path_join(swap_sub_config, "number_of_elements")
] = counter
context.config[path_join(swap_sub_config, "number_of_elements")] = (
counter
)
context.config[swap_sub_config] = True
swap_req.construct(context, swap_config)
@@ -2,4 +2,4 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
from volatility3.framework.configuration import requirements
from volatility3.framework.configuration import requirements as requirements
@@ -11,7 +11,7 @@ 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, Tuple, Type
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type
from urllib import parse, request
from volatility3.framework import constants, interfaces
@@ -111,7 +111,7 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
Args:
element_type: The (requirement) type of each element within the list
max_elements; The maximum number of acceptable elements this list can contain
max_elements: The maximum number of acceptable elements this list can contain
min_elements: The minimum number of acceptable elements this list can contain
"""
super().__init__(*args, **kwargs)
@@ -161,7 +161,7 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
"TypeError - Too many values provided to list option.",
)
return {config_path: self}
if not all([isinstance(element, self.element_type) for element in value]):
if not all(isinstance(element, self.element_type) for element in value):
vollog.log(
constants.LOGLEVEL_V,
"TypeError - At least one element in the list is not of the correct type.",
@@ -181,7 +181,7 @@ class ChoiceRequirement(interfaces.configuration.RequirementInterface):
"""
super().__init__(*args, **kwargs)
if not isinstance(choices, list) or any(
[not isinstance(choice, str) for choice in choices]
not isinstance(choice, str) for choice in choices
):
raise TypeError("ChoiceRequirement takes a list of strings as choices")
self.choices = choices
@@ -314,11 +314,11 @@ class TranslationLayerRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: interfaces.configuration.ConfigSimpleType = None,
optional: bool = False,
oses: List = None,
architectures: List = None,
oses: Optional[List] = None,
architectures: Optional[List[str]] = None,
) -> None:
"""Constructs a Translation Layer Requirement.
@@ -410,11 +410,9 @@ class TranslationLayerRequirement(
args = {"context": context, "config_path": config_path, "name": name}
if any(
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
):
return None
@@ -485,11 +483,9 @@ class SymbolTableRequirement(
args = {"context": context, "config_path": config_path, "name": name}
if any(
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
):
return None
@@ -527,32 +523,70 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
component: Optional[Type[interfaces.configuration.VersionableInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
if version is None:
raise TypeError("Version cannot be None")
if component is None:
raise TypeError("Component cannot be None")
if description is None:
description = f"Version {'.'.join(str(x) for x in version)} dependency on {component.__module__}.{component.__name__} unmet"
super().__init__(
name=name, description=description, default=default, optional=optional
)
if component is None:
raise TypeError("Component cannot be None")
self._component: Type[interfaces.configuration.VersionableInterface] = component
if version is None:
raise TypeError("Version cannot be None")
self._version = version
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
self,
context: interfaces.context.ContextInterface,
config_path: str,
accumulator: Optional[
Set[interfaces.configuration.VersionableInterface]
] = None,
) -> 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):
return {config_path: self}
context.config[
interfaces.configuration.path_join(config_path, self.name)
] = True
recurse = True
if accumulator is None:
accumulator = set([self._component])
else:
if self._component in accumulator:
recurse = False
else:
accumulator.add(self._component)
# Check for child requirements
if (
issubclass(self._component, interfaces.configuration.ConfigurableInterface)
and recurse
):
result = {}
for requirement in self._component.get_requirements():
if not requirement.optional and isinstance(
requirement, VersionRequirement
):
result.update(
requirement.unsatisfied(
context, config_path, accumulator.copy()
)
)
if result:
result[config_path] = self
return result
context.config[interfaces.configuration.path_join(config_path, self.name)] = (
True
)
return {}
@classmethod
@@ -570,10 +604,10 @@ class PluginRequirement(VersionRequirement):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
optional: bool = False,
plugin: Type[interfaces.plugins.PluginInterface] = None,
plugin: Optional[Type[interfaces.plugins.PluginInterface]] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
super().__init__(
@@ -593,7 +627,7 @@ class ModuleRequirement(
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: bool = False,
architectures: Optional[List[str]] = None,
optional: bool = False,
@@ -630,9 +664,7 @@ class ModuleRequirement(
if value is not None:
vollog.log(
constants.LOGLEVEL_V,
"TypeError - Module Requirement only accepts string labels: {}".format(
repr(value)
),
f"TypeError - Module Requirement only accepts string labels: {repr(value)}",
)
return {config_path: self}
@@ -672,11 +704,9 @@ class ModuleRequirement(
args = {"context": context, "config_path": config_path, "name": name}
if any(
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
):
return None
+27 -22
View File
@@ -6,14 +6,24 @@
Stores all the constant values that are generally fixed throughout
volatility This includes default scanning block sizes, etc.
"""
import enum
import os.path
import sys
import warnings
from typing import Callable, Optional
import volatility3.framework.constants.linux
import volatility3.framework.constants.windows
from volatility3.framework.constants import linux as linux
from volatility3.framework.constants import windows as windows
from volatility3.framework.constants._version import (
PACKAGE_VERSION as PACKAGE_VERSION,
VERSION_MAJOR as VERSION_MAJOR,
VERSION_MINOR as VERSION_MINOR,
VERSION_PATCH as VERSION_PATCH,
VERSION_SUFFIX as VERSION_SUFFIX,
)
REQUIRED_PYTHON_VERSION = (3, 8, 0)
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
@@ -42,33 +52,27 @@ if hasattr(sys, "frozen") and sys.frozen:
BANG = "!"
"""Constant used to delimit table names from type names when referring to a symbol"""
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 5 # Number of changes that only add to the interface
VERSION_PATCH = 2 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
# TODO: At version 2.0.0, remove the symbol_shift feature
PACKAGE_VERSION = (
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
AUTOMAGIC_CONFIG_PATH = "automagic"
"""The root section within the context configuration for automagic values"""
LOGLEVEL_INFO = 20
"""Logging level for information data, showed when use the requests any logging: -v"""
LOGLEVEL_DEBUG = 10
"""Logging level for debugging data, showed when the user requests more logging detail: -vv"""
LOGLEVEL_V = 9
"""Logging level for a single -v"""
"""Logging level for the lowest "extra" level of logging: -vvv"""
LOGLEVEL_VV = 8
"""Logging level for -vv"""
"""Logging level for two levels of detail: -vvvv"""
LOGLEVEL_VVV = 7
"""Logging level for -vvv"""
"""Logging level for three levels of detail: -vvvvv"""
LOGLEVEL_VVVV = 6
"""Logging level for -vvvv"""
"""Logging level for four levels of detail: -vvvvvv"""
CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3")
CACHE_PATH = os.path.join(
os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache"),
"volatility3",
)
"""Default path to store cached data"""
SQLITE_CACHE_PERIOD = "-3 days"
@@ -137,4 +141,5 @@ def __getattr__(name):
]:
warnings.warn(f"{name} is deprecated", FutureWarning)
return globals()[f"{deprecated_tag}{name}"]
return None
return getattr(__import__(__name__), name)
@@ -0,0 +1,11 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 19 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
PACKAGE_VERSION = (
".".join(str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
@@ -0,0 +1,21 @@
from volatility3.framework.layers import intel
WIN_ARCHS = ["Intel32", "Intel64"]
"""Windows supported architectures"""
WIN_ARCHS_LAYERS = [intel.Intel]
"""Windows supported architectures layers"""
LINUX_ARCHS = ["Intel32", "Intel64"]
"""Linux supported architectures"""
LINUX_ARCHS_LAYERS = [intel.Intel]
"""Linux supported architectures layers"""
MAC_ARCHS = ["Intel32", "Intel64"]
"""Mac supported architectures"""
MAC_ARCHS_LAYERS = [intel.Intel]
"""Mac supported architectures layers"""
FRAMEWORK_ARCHS = ["Intel32", "Intel64"]
"""Framework supported architectures"""
FRAMEWORK_ARCHS_LAYERS = [intel.Intel]
"""Framework supported architectures layers"""
@@ -5,12 +5,11 @@
Linux-specific values that aren't found in debug symbols
"""
from enum import Enum
from enum import IntEnum, Flag
from dataclasses import dataclass
KERNEL_NAME = "__kernel__"
# arch/x86/include/asm/page_types.h
PAGE_SHIFT = 12
"""The value hard coded from the Linux Kernel (hence not extracted from the layer itself)"""
# include/linux/sched.h
@@ -328,3 +327,136 @@ class IF_OPER_STATES(Enum):
TESTING = 4
DORMANT = 5
UP = 6
class ELF_IDENT(IntEnum):
"""ELF header e_ident indexes"""
EI_MAG0 = 0
EI_MAG1 = 1
EI_MAG2 = 2
EI_MAG3 = 3
EI_CLASS = 4
EI_DATA = 5
EI_VERSION = 6
EI_OSABI = 7
EI_PAD = 8
class ELF_CLASS(IntEnum):
"""ELF header class types"""
ELFCLASSNONE = 0
ELFCLASS32 = 1
ELFCLASS64 = 2
# PTrace
PT_OPT_FLAG_SHIFT = 3
PTRACE_EVENT_FORK = 1
PTRACE_EVENT_VFORK = 2
PTRACE_EVENT_CLONE = 3
PTRACE_EVENT_EXEC = 4
PTRACE_EVENT_VFORK_DONE = 5
PTRACE_EVENT_EXIT = 6
PTRACE_EVENT_SECCOMP = 7
PTRACE_O_EXITKILL = 1 << 20
PTRACE_O_SUSPEND_SECCOMP = 1 << 21
class PT_FLAGS(Flag):
"PTrace flags"
PT_PTRACED = 0x00001
PT_SEIZED = 0x10000
PT_TRACESYSGOOD = 1 << (PT_OPT_FLAG_SHIFT + 0)
PT_TRACE_FORK = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_FORK)
PT_TRACE_VFORK = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_VFORK)
PT_TRACE_CLONE = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_CLONE)
PT_TRACE_EXEC = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_EXEC)
PT_TRACE_VFORK_DONE = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_VFORK_DONE)
PT_TRACE_EXIT = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_EXIT)
PT_TRACE_SECCOMP = 1 << (PT_OPT_FLAG_SHIFT + PTRACE_EVENT_SECCOMP)
PT_EXITKILL = PTRACE_O_EXITKILL << PT_OPT_FLAG_SHIFT
PT_SUSPEND_SECCOMP = PTRACE_O_SUSPEND_SECCOMP << PT_OPT_FLAG_SHIFT
@property
def flags(self) -> str:
"""Returns the ptrace flags string"""
return str(self).replace(self.__class__.__name__ + ".", "")
# Boot time
NSEC_PER_SEC = 1e9
# Valid sizes for modules. Note that the Linux kernel does not define these values; they
# are based on empirical observations of typical memory allocations for kernel modules.
# We use this to verify that the found module falls within reasonable limits.
MODULE_MAXIMUM_CORE_SIZE = 20000000
MODULE_MAXIMUM_CORE_TEXT_SIZE = 20000000
MODULE_MINIMUM_SIZE = 4096
# Kallsyms
KSYM_NAME_LEN = 512
# VMCOREINFO
VMCOREINFO_MAGIC = b"VMCOREINFO\x00"
# Aligned to 4 bytes. See storenote() in kernels < 4.19 or append_kcore_note() in kernels >= 4.19
VMCOREINFO_MAGIC_ALIGNED = VMCOREINFO_MAGIC + b"\x00"
OSRELEASE_TAG = b"OSRELEASE="
@dataclass
class TaintFlag:
shift: int
desc: str
when_present: bool
module: bool
TAINT_FLAGS = {
"P": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=True, module=True
),
"G": TaintFlag(
shift=1 << 0, desc="PROPRIETARY_MODULE", when_present=False, module=True
),
"F": TaintFlag(shift=1 << 1, desc="FORCED_MODULE", when_present=True, module=False),
"S": TaintFlag(
shift=1 << 2, desc="CPU_OUT_OF_SPEC", when_present=True, module=False
),
"R": TaintFlag(shift=1 << 3, desc="FORCED_RMMOD", when_present=True, module=False),
"M": TaintFlag(shift=1 << 4, desc="MACHINE_CHECK", when_present=True, module=False),
"B": TaintFlag(shift=1 << 5, desc="BAD_PAGE", when_present=True, module=False),
"U": TaintFlag(shift=1 << 6, desc="USER", when_present=True, module=False),
"D": TaintFlag(shift=1 << 7, desc="DIE", when_present=True, module=False),
"A": TaintFlag(
shift=1 << 8, desc="OVERRIDDEN_ACPI_TABLE", when_present=True, module=False
),
"W": TaintFlag(shift=1 << 9, desc="WARN", when_present=True, module=False),
"C": TaintFlag(shift=1 << 10, desc="CRAP", when_present=True, module=True),
"I": TaintFlag(
shift=1 << 11, desc="FIRMWARE_WORKAROUND", when_present=True, module=False
),
"O": TaintFlag(shift=1 << 12, desc="OOT_MODULE", when_present=True, module=True),
"E": TaintFlag(
shift=1 << 13, desc="UNSIGNED_MODULE", when_present=True, module=True
),
"L": TaintFlag(shift=1 << 14, desc="SOFTLOCKUP", when_present=True, module=False),
"K": TaintFlag(shift=1 << 15, desc="LIVEPATCH", when_present=True, module=True),
"X": TaintFlag(shift=1 << 16, desc="AUX", when_present=True, module=True),
"T": TaintFlag(shift=1 << 17, desc="RANDSTRUCT", when_present=True, module=True),
"N": TaintFlag(shift=1 << 18, desc="TEST", when_present=True, module=True),
}
"""Flags used to taint kernel and modules, for debugging purposes.
Map based on 6.12-rc5.
Documentation :
- https://www.kernel.org/doc/Documentation/admin-guide/sysctl/kernel.rst#:~:text=guide/sysrq.rst.-,tainted,-%3D%3D%3D%3D%3D%3D%3D%0A%0ANon%2Dzero%20if
- https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst#:~:text=More%20detailed%20explanation%20for%20tainting
- taint_flag kernel struct
- taint_flags kernel constant
"""
@@ -10,3 +10,21 @@ KERNEL_MODULE_NAMES = ["ntkrnlmp", "ntkrnlpa", "ntkrpamp", "ntoskrnl"]
"""The list of names that kernel modules can have within the windows OS"""
PE_MAX_EXTRACTION_SIZE = 1024 * 1024 * 256
"""
The following constants represent the layout of the Low Stub which exists only on x64 machines with no virtualization/emulation,
responsible for transitioning from Real Mode(16 bit) to Protected Mode(32 bit) and Long Mode(64 bit) on boot/return from sleep.
Contains offsets to fields and structures within the undocumented structure _PROCESSOR_START_BLOCK.
Here's a reference: https://github.com/mic101/windows/blob/master/WRK-v1.2/base/ntos/inc/amd64.h#L3334
"""
# Expected signature for validation, constructed from:
# PROCESSOR_START_BLOCK->Jmp->OpCode | PROCESSOR_START_BLOCK->Jmp->Offset | PROCESSOR_START_BLOCK->CompletionFlag
JMP_AND_COMPLETION_SIGNATURE = 0x00000001000600E9
# Address of LmTarget (Long Mode target)
PROCESSOR_START_BLOCK_LM_TARGET_OFFSET = (
0x70 # PROCESSOR_START_BLOCK->LmTarget, PVOID 8 bytes
)
# CR3 register within structures describing initial processor state to be started
PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes
+18 -19
View File
@@ -11,7 +11,8 @@ without them interfering with each other.
import functools
import hashlib
import logging
from typing import Callable, Iterable, List, Optional, Set, Tuple, Union
import re
from typing import Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
from volatility3.framework import constants, interfaces, symbols, exceptions
from volatility3.framework.objects import templates
@@ -229,7 +230,7 @@ class Module(interfaces.context.ModuleInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -245,7 +246,7 @@ class Module(interfaces.context.ModuleInterface):
"""
if constants.BANG not in object_type:
object_type = self.symbol_table_name + constants.BANG + object_type
else:
elif not object_type.startswith(self.symbol_table_name + constants.BANG):
raise ValueError(
"Cannot reference another module when constructing an object"
)
@@ -256,12 +257,13 @@ class Module(interfaces.context.ModuleInterface):
if not absolute:
offset += self._offset
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if "layer_name" in kwargs:
del kwargs["layer_name"]
# We have to allow using an alternative layer name due to pool scanners switching
# to the memory layer for scanning samples prior to Windows 10.
layer_name = kwargs.pop("layer_name", self._layer_name)
return self._context.object(
object_type=object_type,
layer_name=self._layer_name,
layer_name=layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
**kwargs,
@@ -336,7 +338,7 @@ class Module(interfaces.context.ModuleInterface):
)
@property
def symbols(self):
def symbols(self) -> Iterable[str]:
return self.context.symbol_space[self.symbol_table_name].symbols
get_symbol = get_module_wrapper("get_symbol")
@@ -355,7 +357,7 @@ class SizedModule(Module):
return size or 0
@property # type: ignore # FIXME: mypy #5107
@functools.lru_cache()
@functools.lru_cache
def hash(self) -> str:
"""Hashes the module for equality checks.
@@ -385,10 +387,8 @@ class ModuleCollection(interfaces.context.ModuleContainer):
"""Class to contain a collection of SizedModules and reason about their
contents."""
def __init__(
self, modules: Optional[List[interfaces.context.ModuleInterface]] = None
) -> None:
self._prefix_count = {}
def __init__(self, modules: Optional[List[SizedModule]] = None) -> None:
self._modules: Dict[str, SizedModule] = {}
super().__init__(modules)
def deduplicate(self) -> "ModuleCollection":
@@ -401,20 +401,19 @@ class ModuleCollection(interfaces.context.ModuleContainer):
new_modules = []
seen: Set[str] = set()
for mod in self._modules:
if mod.hash not in seen or mod.size == 0:
if self._modules[mod].hash not in seen or self._modules[mod].size == 0:
new_modules.append(mod)
seen.add(mod.hash) # type: ignore # FIXME: mypy #5107
seen.add(self._modules[mod].hash)
return ModuleCollection(new_modules)
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused module name"""
if prefix not in self._prefix_count:
self._prefix_count[prefix] = 1
existing_names = [name for name in self if re.match(rf"^{prefix}[0-9]*$", name)]
if not existing_names:
return prefix
count = self._prefix_count[prefix]
count = len(existing_names)
while prefix + str(count) in self:
count += 1
self._prefix_count[prefix] = count
return prefix + str(count)
@property
+38 -1
View File
@@ -8,9 +8,10 @@ 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 Dict, Optional
from typing import Callable, Dict, Optional, Tuple
from volatility3.framework import interfaces
from volatility3.framework.interfaces.configuration import VersionableInterface
class VolatilityException(Exception):
@@ -126,3 +127,39 @@ class OfflineException(VolatilityException):
def __str__(self):
return f"Volatility 3 is offline: unable to access {self._url}"
class RenderException(VolatilityException):
"""Thrown if there is an error during rendering"""
class LinuxPageCacheException(VolatilityException):
"""Thrown if there is an error during Linux Page Cache processing"""
class VersionMismatchException(VolatilityException):
"""Thrown if a version mismatch has been encountered between two components."""
def __init__(
self,
source_component: Callable,
target_component: VersionableInterface,
target_version: Tuple[int, int, int],
failure_reason: str = None,
*args,
):
"""
Args:
source_component: The component that required the target component
target_component: The component that is required. Must inherit from VersionableInterface
target_version: The version of the target component that was required, and ultimately was not satisfied
failure_reason: A detailed failure reason to enhance debugging and bug tracking
"""
super().__init__(*args)
self.source_component = source_component
self.target_component = target_component
self.target_version = target_version
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."
+8 -8
View File
@@ -13,12 +13,12 @@ components of volatility to write plugins.
# This will also avoid namespace issues, because people can use interfaces.layers to
# avoid clashing with the layers package
from volatility3.framework.interfaces import (
renderers,
configuration,
context,
layers,
objects,
plugins,
symbols,
automagic,
renderers as renderers,
configuration as configuration,
context as context,
layers as layers,
objects as objects,
plugins as plugins,
symbols as symbols,
automagic as automagic,
)
@@ -42,7 +42,7 @@ class AutomagicInterface(
priority = 10
"""An ordering to indicate how soon this automagic should be run"""
exclusion_list = []
exclusion_list: List[str] = []
"""A list of plugin categories (typically operating systems) which the plugin will not operate on"""
def __init__(
@@ -50,7 +50,7 @@ class AutomagicInterface(
context: interfaces.context.ContextInterface,
config_path: str,
*args,
**kwargs
**kwargs,
) -> None:
super().__init__(context, config_path)
for requirement in self.get_requirements():
@@ -53,7 +53,7 @@ ConfigSimpleType = Optional[Union[SimpleTypes, List[SimpleTypes]]]
def path_join(*args) -> str:
"""Joins configuration paths together."""
# If a path element (particularly the first) is empty, then remove it from the list
args = tuple([arg for arg in args if arg])
args = tuple(arg for arg in args if arg)
return CONFIG_SEPARATOR.join(args)
@@ -82,7 +82,7 @@ class HierarchicalDict(collections.abc.Mapping):
def __init__(
self,
initial_dict: Dict[str, "SimpleTypeRequirement"] = None,
initial_dict: Optional[Dict[str, "SimpleTypeRequirement"]] = None,
separator: str = CONFIG_SEPARATOR,
) -> None:
"""
@@ -94,7 +94,7 @@ class HierarchicalDict(collections.abc.Mapping):
raise TypeError(f"Separator must be a one character string: {separator}")
self._separator = separator
self._data: Dict[str, ConfigSimpleType] = {}
self._subdict: Dict[str, "HierarchicalDict"] = {}
self._subdict: Dict[str, HierarchicalDict] = {}
if isinstance(initial_dict, str):
initial_dict = json.loads(initial_dict)
if isinstance(initial_dict, dict):
@@ -182,9 +182,7 @@ class HierarchicalDict(collections.abc.Mapping):
else:
if not isinstance(value, HierarchicalDict):
raise TypeError(
"HierarchicalDicts can only store HierarchicalDicts within their structure: {}".format(
type(value)
)
f"HierarchicalDicts can only store HierarchicalDicts within their structure: {type(value)}"
)
self._subdict[key] = value
@@ -330,7 +328,7 @@ class RequirementInterface(metaclass=ABCMeta):
def __init__(
self,
name: str,
description: str = None,
description: Optional[str] = None,
default: ConfigSimpleType = None,
optional: bool = False,
) -> None:
@@ -494,14 +492,11 @@ class SimpleTypeRequirement(RequirementInterface):
"""Validates the instance requirement based upon its
`instance_type`."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
if not isinstance(value, self.instance_type):
vollog.log(
constants.LOGLEVEL_V,
"TypeError - {} requirements only accept {} type: {}".format(
self.name, self.instance_type.__name__, repr(value)
),
f"TypeError - {self.name} requirements only accept {self.instance_type.__name__} type: {repr(value)}",
)
return {config_path: self}
return {}
@@ -536,7 +531,7 @@ class ClassRequirement(RequirementInterface):
"""Checks to see if a class can be recovered."""
config_path = path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
value = self.config_value(context, config_path, self.default)
self._cls = None
if value is not None and isinstance(value, str):
if "." in value:
@@ -623,7 +618,7 @@ class ConstructableRequirementInterface(RequirementInterface):
self,
context: "interfaces.context.ContextInterface",
config_path: str,
requirement_dict: Dict[str, object] = None,
requirement_dict: Optional[Dict[str, object]] = None,
) -> Optional["interfaces.objects.ObjectInterface"]:
"""Constructs the class, handing args and the subrequirements as
parameters to __init__"""
@@ -657,6 +652,7 @@ class ConstructableRequirementInterface(RequirementInterface):
class ConfigurableRequirementInterface(RequirementInterface):
"""Simple Abstract class to provide build_required_config."""
@abstractmethod
def build_configuration(
self,
context: "interfaces.context.ContextInterface",
@@ -776,17 +772,16 @@ class ConfigurableInterface(metaclass=ABCMeta):
str: The newly generated full configuration path
"""
random_config_dict = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
random.SystemRandom().choices(string.ascii_uppercase + string.digits, k=8)
)
new_config_path = path_join(base_config_path, random_config_dict)
# TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in
# This should check that each k corresponds to a requirement and each v is of the appropriate type
# This would require knowledge of the new configurable itself to verify, and they should do validation in the
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a basic type
for k, v in kwargs.items():
if not isinstance(v, (int, str, bool, float, bytes)):
if not isinstance(v, BasicTypes):
raise TypeError(
"Config values passed to make_subconfig can only be simple types"
)
+20 -5
View File
@@ -85,9 +85,9 @@ class ContextInterface(metaclass=ABCMeta):
object_type: Union[str, "interfaces.objects.Template"],
layer_name: str,
offset: int,
native_layer_name: str = None,
native_layer_name: Optional[str] = None,
**arguments,
):
) -> "interfaces.objects.ObjectInterface":
"""Object factory, takes a context, symbol, offset and optional
layer_name.
@@ -114,6 +114,7 @@ class ContextInterface(metaclass=ABCMeta):
"""
return copy.deepcopy(self)
@abstractmethod
def module(
self,
module_name: str,
@@ -191,6 +192,9 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
self._native_layer_name
].build_configuration()
# Modules are constructable, and therefore require a class configuration variable
config["class"] = self.__class__.__module__ + "." + self.__class__.__name__
for subconfig in subconfigs:
for req in subconfigs[subconfig]:
config[interfaces.configuration.path_join(subconfig, req)] = subconfigs[
@@ -229,7 +233,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
def object(
self,
object_type: str,
offset: int = None,
offset: Optional[int] = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
@@ -274,27 +278,37 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol = self.get_symbol(name)
return self.offset + symbol.address
@abstractmethod
def get_type(self, name: str) -> "interfaces.objects.Template":
"""Returns a type from the module's symbol table."""
@abstractmethod
def get_symbol(self, name: str) -> "interfaces.symbols.SymbolInterface":
"""Returns a symbol object from the module's symbol table."""
@abstractmethod
def get_enumeration(self, name: str) -> "interfaces.objects.Template":
"""Returns an enumeration from the module's symbol table."""
@abstractmethod
def has_type(self, name: str) -> bool:
"""Determines whether a type is present in the module's symbol table."""
@abstractmethod
def has_symbol(self, name: str) -> bool:
"""Determines whether a symbol is present in the module's symbol table."""
@abstractmethod
def has_enumeration(self, name: str) -> bool:
"""Determines whether an enumeration is present in the module's symbol table."""
def symbols(self) -> List:
"""Lists the symbols contained in the symbol table for this module"""
@property
@abstractmethod
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the symbols contained in the symbol table for this module"""
raise NotImplementedError("Symbols property has not been implemented.")
@abstractmethod
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
"""Returns the symbols within table_name (or this module if not specified) that live at the specified
absolute offset provided."""
@@ -340,6 +354,7 @@ class ModuleContainer(collections.abc.Mapping):
def __iter__(self):
return iter(self._modules)
@abstractmethod
def free_module_name(self, prefix: str = "module") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
+4 -7
View File
@@ -136,7 +136,7 @@ class DataLayerInterface(
def minimum_address(self) -> int:
"""Returns the minimum valid address of the space."""
@property
@functools.cached_property
def address_mask(self) -> int:
"""Returns a mask which encapsulates all the active bits of an address
for this layer."""
@@ -188,7 +188,6 @@ class DataLayerInterface(
the object unreadable (exceptions will be thrown using a
DataLayer after destruction)
"""
pass
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -211,7 +210,7 @@ class DataLayerInterface(
context: interfaces.context.ContextInterface,
scanner: ScannerInterface,
progress_callback: constants.ProgressCallback = None,
sections: Iterable[Tuple[int, int]] = None,
sections: Optional[Iterable[Tuple[int, int]]] = None,
) -> Iterable[Any]:
"""Scans a Translation layer by chunk.
@@ -361,9 +360,7 @@ class DataLayerInterface(
data += self.context.layers[layer_name].read(address, chunk_size)
except exceptions.InvalidAddressException:
vollog.debug(
"Invalid address in layer {} found scanning {} at address {:x}".format(
layer_name, self.name, address
)
f"Invalid address in layer {layer_name} found scanning {self.name} at address {address:x}"
)
if len(data) > scanner.chunk_size + scanner.overlap:
@@ -721,7 +718,7 @@ class LayerContainer(collections.abc.Mapping):
raise NotImplementedError("Cycle checking has not yet been implemented")
class DummyProgress(object):
class DummyProgress:
"""A class to emulate Multiprocessing/threading Value objects."""
def __init__(self) -> None:
+2 -1
View File
@@ -216,7 +216,7 @@ class ObjectInterface(metaclass=abc.ABCMeta):
Args:
member_names: List of names to test as to members with those names validity
"""
return all([self.has_valid_member(member_name) for member_name in member_names])
return all(self.has_valid_member(member_name) for member_name in member_names)
class VolTemplateProxy(metaclass=abc.ABCMeta):
"""A container for proxied methods that the ObjectTemplate of this
@@ -374,6 +374,7 @@ class Template:
f"{self.__class__.__name__} object has no attribute {attr}"
)
@abc.abstractmethod
def __call__(
self,
context: "interfaces.context.ContextInterface",
+4 -4
View File
@@ -46,7 +46,7 @@ class FileHandlerInterface(io.RawIOBase):
def preferred_filename(self, filename: str):
"""Sets the preferred filename"""
if self.closed:
raise IOError("FileHandler name cannot be changed once closed")
raise OSError("FileHandler name cannot be changed once closed")
if not isinstance(filename, str):
raise TypeError("FileHandler preferred filenames must be strings")
if os.path.sep in filename:
@@ -59,14 +59,14 @@ class FileHandlerInterface(io.RawIOBase):
@staticmethod
def sanitize_filename(filename: str) -> str:
"""Sanititizes the filename to ensure only a specific whitelist of characters is allowed through"""
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]\{\}!$%^:#~?<>,|"
"""Sanititizes the filename to ensure only a specific allow list of characters is allowed through"""
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]{}!$%^#~,"
result = ""
for char in filename:
if char in allowed:
result += char
else:
result += "?"
result += "_" # change unwanted chars to an underscore
return result
def __enter__(self):
+10 -6
View File
@@ -26,7 +26,11 @@ from typing import (
Union,
)
Column = NamedTuple("Column", [("name", str), ("type", Any)])
class Column(NamedTuple):
name: str
type: Any
RenderOption = Any
@@ -98,11 +102,11 @@ class TreeNode(abc.Sequence, metaclass=ABCMeta):
"""
class BaseAbsentValue(object):
class BaseAbsentValue:
"""Class that represents values which are not present for some reason."""
class Disassembly(object):
class Disassembly:
"""A class to indicate that the bytes provided should be disassembled
(based on the architecture)"""
@@ -137,7 +141,7 @@ ColumnsType = List[Tuple[str, BaseTypes]]
VisitorSignature = Callable[[TreeNode, _Type], _Type]
class TreeGrid(object, metaclass=ABCMeta):
class TreeGrid(metaclass=ABCMeta):
"""Class providing the interface for a TreeGrid (which contains TreeNodes)
The structure of a TreeGrid is designed to maintain the structure of the tree in a single object.
@@ -179,7 +183,7 @@ class TreeGrid(object, metaclass=ABCMeta):
@abstractmethod
def populate(
self,
function: VisitorSignature = None,
function: Optional[VisitorSignature] = None,
initial_accumulator: Any = None,
fail_on_errors: bool = True,
) -> Optional[Exception]:
@@ -231,7 +235,7 @@ class TreeGrid(object, metaclass=ABCMeta):
node: Optional[TreeNode],
function: VisitorSignature,
initial_accumulator: _Type,
sort_key: ColumnSortKey = None,
sort_key: Optional[ColumnSortKey] = None,
) -> None:
"""Visits all the nodes in a tree, calling function on each one.
+13 -6
View File
@@ -122,7 +122,7 @@ class BaseSymbolTableInterface:
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterator of the Symbol names."""
"""Returns an iterable of the available symbol names."""
raise NotImplementedError(
"Abstract property symbols not implemented by subclass."
)
@@ -131,7 +131,7 @@ class BaseSymbolTableInterface:
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the Symbol type names."""
"""Returns an iterable of the available symbol type names."""
raise NotImplementedError(
"Abstract property types not implemented by subclass."
)
@@ -149,7 +149,7 @@ class BaseSymbolTableInterface:
@property
def enumerations(self) -> Iterable[Any]:
"""Returns an iterator of the Enumeration names."""
"""Returns an iterable of the available enumerations."""
raise NotImplementedError(
"Abstract property enumerations not implemented by subclass."
)
@@ -250,13 +250,13 @@ class BaseSymbolTableInterface:
def clear_symbol_cache(self) -> None:
"""Clears the symbol cache of this symbol table."""
pass
class SymbolSpaceInterface(collections.abc.Mapping):
"""An interface for the container that holds all the symbol-containing
tables for use within a context."""
@abstractmethod
def free_table_name(self, prefix: str = "layer") -> str:
"""Returns an unused table name to ensure no collision occurs when
inserting a symbol table."""
@@ -366,6 +366,7 @@ class NativeTableInterface(BaseSymbolTableInterface):
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterable of the available symbol names."""
return []
def get_enumeration(self, name: str) -> objects.Template:
@@ -374,11 +375,17 @@ class NativeTableInterface(BaseSymbolTableInterface):
)
@property
def enumerations(self) -> Iterable[str]:
def enumerations(self) -> Iterable[Any]:
"""Returns an iterable of the available enumerations."""
return []
@property
def types(self) -> Iterable[str]:
"""Returns an iterable of the available symbol type names."""
return []
class MetadataInterface(object):
class MetadataInterface:
"""Interface for accessing metadata stored within a symbol table."""
def __init__(self, json_data: Dict) -> None:
+3 -3
View File
@@ -224,7 +224,7 @@ class AVMLStacker(interfaces.automagic.StackerLayerInterface):
except exceptions.LayerException:
return None
new_name = context.layers.free_layer_name("AVMLLayer")
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
return AVMLLayer(context, new_name, new_name)
+67 -48
View File
@@ -1,7 +1,6 @@
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import contextlib
import logging
import struct
from typing import Tuple, Optional
@@ -96,12 +95,13 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
def get_summary_header(self) -> interfaces.objects.ObjectInterface:
return self.context.object(
self._crash_common_table_name + constants.BANG + "_SUMMARY_DUMP",
offset=0x1000 * self.headerpages,
offset=self._page_size * self.headerpages,
layer_name=self._base_layer,
)
def _load_segments(self) -> None:
"""Loads up the segments from the meta_layer."""
"""Loads up the segments from the meta_layer.
A segment is a set of contiguous memory pages."""
segments = []
@@ -119,70 +119,87 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
for run in header.PhysicalMemoryBlockBuffer.Run:
segments.append(
(
run.BasePage * 0x1000,
offset * 0x1000,
run.PageCount * 0x1000,
run.PageCount * 0x1000,
run.BasePage * self._page_size,
offset * self._page_size,
run.PageCount * self._page_size,
run.PageCount * self._page_size,
)
)
offset += run.PageCount
elif self.dump_type == 0x05:
summary_header = self.get_summary_header()
first_bit = None # First bit in a run
first_offset = 0 # File offset of first bit
last_bit_seen = 0 # Most recent bit processed
offset = summary_header.HeaderSize # Size of file headers
buffer_char = summary_header.get_buffer_char()
buffer_long = summary_header.get_buffer_long()
for outer_index in range(0, ((summary_header.BitmapSize + 31) // 32)):
if buffer_long[outer_index] == 0:
if first_bit is not None:
last_bit = ((outer_index - 1) * 32) + 31
segment_length = (last_bit - first_bit + 1) * 0x1000
seg_first_bit = None # First bit in a run
seg_first_offset = 0 # File offset of first bit
offset = (
summary_header.HeaderSize
) # Offset to the start of actual memory dump
ulong_bitmap_array = summary_header.get_buffer_long()
# outer_index points to a 32 bits array inside a list of arrays,
# each bit indicating a page mapping state
for outer_index in range(ulong_bitmap_array.vol.count):
ulong_bitmap = ulong_bitmap_array[outer_index]
# All pages in this 32 bits array are mapped (speedup iteration process)
if ulong_bitmap == 0xFFFFFFFF:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = outer_index * 32
offset += 32 * self._page_size
# No pages in this 32 bits array are mapped (speedup iteration process)
elif ulong_bitmap == 0:
# End of segment
if seg_first_bit is not None:
last_bit = (outer_index - 1) * 32 + 31
segment_length = (
last_bit - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
elif buffer_long[outer_index] == 0xFFFFFFFF:
if first_bit is None:
first_offset = offset
first_bit = outer_index * 32
offset = offset + (32 * 0x1000)
seg_first_bit = None
# Some pages in this 32 bits array are mapped and some aren't
else:
for inner_index in range(0, 32):
bit_addr = outer_index * 32 + inner_index
if (buffer_char[bit_addr >> 3] >> (bit_addr & 0x7)) & 1:
if first_bit is None:
first_offset = offset
first_bit = bit_addr
offset = offset + 0x1000
for inner_bit_position in range(32):
current_bit = outer_index * 32 + inner_bit_position
page_mapped = ulong_bitmap & (1 << inner_bit_position)
if page_mapped:
# New segment
if seg_first_bit is None:
seg_first_offset = offset
seg_first_bit = current_bit
offset += self._page_size
else:
if first_bit is not None:
# End of segment
if seg_first_bit is not None:
segment_length = (
(bit_addr - 1) - first_bit + 1
) * 0x1000
current_bit - 1 - seg_first_bit + 1
) * self._page_size
segments.append(
(
first_bit * 0x1000,
first_offset,
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
first_bit = None
last_bit_seen = (outer_index * 32) + 31
seg_first_bit = None
last_bit_seen = outer_index * 32 + 31
if first_bit is not None:
segment_length = (last_bit_seen - first_bit + 1) * 0x1000
if seg_first_bit is not None:
segment_length = (last_bit_seen - seg_first_bit + 1) * self._page_size
segments.append(
(first_bit * 0x1000, first_offset, segment_length, segment_length)
(
seg_first_bit * self._page_size,
seg_first_offset,
segment_length,
segment_length,
)
)
else:
vollog.log(
@@ -202,9 +219,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
for idx, (start_position, mapped_offset, length, _) in enumerate(segments):
vollog.log(
constants.LOGLEVEL_VVVV,
"Segment {}: Position {:#x} Offset {:#x} Length {:#x}".format(
idx, start_position, mapped_offset, length
),
f"Segment {idx}: Position {start_position:#x} Offset {mapped_offset:#x} Length {length:#x}",
)
self._segments = segments
@@ -261,11 +276,15 @@ class WindowsCrashDumpStacker(interfaces.automagic.StackerLayerInterface):
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
for layer in [WindowsCrashDump32Layer, WindowsCrashDump64Layer]:
with contextlib.suppress(WindowsCrashDumpFormatException):
try:
layer.check_header(context.layers[layer_name])
new_name = context.layers.free_layer_name(layer.__name__)
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return layer(context, new_name, new_name)
except WindowsCrashDumpFormatException as excp:
vollog.log(
constants.LOGLEVEL_VVVV, f"Exception reading crashdump: {excp}"
)
return None
+16 -5
View File
@@ -6,9 +6,11 @@ import struct
from typing import Optional
from volatility3.framework import exceptions, interfaces, constants
from volatility3.framework.constants.linux import ELF_CLASS
from volatility3.framework.layers import segmented
from volatility3.framework.symbols import intermed
vollog = logging.getLogger(__name__)
@@ -21,7 +23,7 @@ class Elf64Layer(segmented.SegmentedLayer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -50,8 +52,17 @@ class Elf64Layer(segmented.SegmentedLayer):
offset=ehdr.e_phoff + (pindex * ehdr.e_phentsize),
)
# We only want PT_TYPES with valid sizes
try:
ptype = phdr.p_type.description
except ValueError:
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping unknown ELF program header type: {phdr.p_type}",
)
continue
if (
phdr.p_type.lookup() == "PT_LOAD"
ptype == "PT_LOAD"
and phdr.p_filesz == phdr.p_memsz
and phdr.p_filesz > 0
):
@@ -115,9 +126,9 @@ class Elf64Stacker(interfaces.automagic.StackerLayerInterface):
vollog.log(constants.LOGLEVEL_VVVV, f"Exception: {excp}")
return None
new_name = context.layers.free_layer_name("Elf64Layer")
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
try:
return Elf64Layer(context, new_name, new_name)
+131 -30
View File
@@ -22,6 +22,16 @@ INTEL_TRANSLATION_DEBUGGING = False
class Intel(linear.LinearlyMappedLayer):
"""Translation Layer for the Intel IA32 memory mapping."""
_PAGE_BIT_PRESENT = 0
_PAGE_BIT_PSE = 7 # Page Size Extension: 4 MB (or 2MB) page
_PAGE_BIT_PROTNONE = 8
_PAGE_BIT_PAT_LARGE = 12 # 2MB or 1GB pages
_PAGE_PRESENT = 1 << _PAGE_BIT_PRESENT
_PAGE_PSE = 1 << _PAGE_BIT_PSE
_PAGE_PROTNONE = 1 << _PAGE_BIT_PROTNONE
_PAGE_PAT_LARGE = 1 << _PAGE_BIT_PAT_LARGE
_entry_format = "<I"
_page_size_in_bits = 12
_bits_per_register = 32
@@ -63,12 +73,16 @@ class Intel(linear.LinearlyMappedLayer):
)
# These can vary depending on the type of space
self._index_shift = int(
math.ceil(math.log2(struct.calcsize(self._entry_format)))
)
self._index_shift = math.ceil(math.log2(struct.calcsize(self._entry_format)))
@classproperty
@functools.lru_cache()
@functools.lru_cache
def page_shift(cls) -> int:
"""Page shift for the intel memory layers."""
return cls._page_size_in_bits
@classproperty
@functools.lru_cache
def page_size(cls) -> int:
"""Page size for the intel memory layers.
@@ -77,19 +91,25 @@ class Intel(linear.LinearlyMappedLayer):
return 1 << cls._page_size_in_bits
@classproperty
@functools.lru_cache()
@functools.lru_cache
def page_mask(cls) -> int:
"""Page mask for the intel memory layers."""
return ~(cls.page_size - 1)
@classproperty
@functools.lru_cache
def bits_per_register(cls) -> int:
"""Returns the bits_per_register to determine the range of an
IntelTranslationLayer."""
return cls._bits_per_register
@classproperty
@functools.lru_cache()
@functools.lru_cache
def minimum_address(cls) -> int:
return 0
@classproperty
@functools.lru_cache()
@functools.lru_cache
def maximum_address(cls) -> int:
return (1 << cls._maxvirtaddr) - 1
@@ -103,7 +123,6 @@ class Intel(linear.LinearlyMappedLayer):
high_mask = (1 << (high_bit + 1)) - 1
low_mask = (1 << low_bit) - 1
mask = high_mask ^ low_mask
# print(high_bit, low_bit, bin(mask), bin(value))
return value & mask
@staticmethod
@@ -125,7 +144,7 @@ class Intel(linear.LinearlyMappedLayer):
return self._mask(addr, self._maxvirtaddr, 0) + self._canonical_prefix
def decanonicalize(self, addr: int) -> int:
"""Removes canonicalization to ensure an adress fits within the correct range if it has been canonicalized
"""Removes canonicalization to ensure an address fits within the correct range if it has been canonicalized
This will produce an address outside the range if the canonicalization is incorrect
"""
@@ -151,12 +170,17 @@ class Intel(linear.LinearlyMappedLayer):
entry,
f"Page Fault at entry {hex(entry)} in page entry",
)
page = self._mask(entry, self._maxphyaddr - 1, position + 1) | self._mask(
offset, position, 0
)
pfn = self._pte_pfn(entry)
page_offset = self._mask(offset, position, 0)
page = pfn << self.page_shift | page_offset
return page, 1 << (position + 1), self._base_layer
def _pte_pfn(self, entry: int) -> int:
"""Extracts the page frame number (PFN) from the page table entry (PTE) entry"""
return self._mask(entry, self._maxphyaddr - 1, 0) >> self.page_shift
def _translate_entry(self, offset: int) -> Tuple[int, int]:
"""Translates a specific offset based on paging tables.
@@ -168,7 +192,9 @@ class Intel(linear.LinearlyMappedLayer):
position = self._initial_position
entry = self._initial_entry
if self.minimum_address > offset > self.maximum_address:
if not (
self.minimum_address <= (offset & self.address_mask) <= self.maximum_address
):
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
@@ -189,10 +215,10 @@ class Intel(linear.LinearlyMappedLayer):
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Check if we're a large page
if large_page and (entry & (1 << 7)):
if large_page and (entry & self._PAGE_PSE):
# Mask off the PAT bit
if entry & (1 << 12):
entry -= 1 << 12
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
@@ -225,12 +251,7 @@ class Intel(linear.LinearlyMappedLayer):
if INTEL_TRANSLATION_DEBUGGING:
vollog.log(
constants.LOGLEVEL_VVVV,
"Entry {} at index {} gives data {} as {}".format(
hex(entry),
hex(index),
hex(struct.unpack(self._entry_format, entry_data)[0]),
name,
),
f"Entry {hex(entry)} at index {hex(index)} gives data {hex(struct.unpack(self._entry_format, entry_data)[0])} as {name}",
)
# Read out the new entry from memory
@@ -238,7 +259,7 @@ class Intel(linear.LinearlyMappedLayer):
return entry, position
@functools.lru_cache(1025)
@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(
@@ -256,10 +277,8 @@ class Intel(linear.LinearlyMappedLayer):
try:
# TODO: Consider reimplementing this, since calls to mapping can call is_valid
return all(
[
self._context.layers[layer].is_valid(mapped_offset)
for _, _, mapped_offset, _, layer in self.mapping(offset, length)
]
self._context.layers[layer].is_valid(mapped_offset)
for _, _, mapped_offset, _, layer in self.mapping(offset, length)
)
except exceptions.InvalidAddressException:
return False
@@ -277,9 +296,9 @@ class Intel(linear.LinearlyMappedLayer):
This allows translation layers to provide maps of contiguous
regions in one layer
"""
stashed_offset = (
stashed_mapped_offset
) = stashed_size = stashed_mapped_size = stashed_map_layer = None
stashed_offset = stashed_mapped_offset = stashed_size = stashed_mapped_size = (
stashed_map_layer
) = None
for offset, size, mapped_offset, mapped_size, map_layer in self._mapping(
offset, length, ignore_errors
):
@@ -489,3 +508,85 @@ class WindowsIntel32e(WindowsMixin, Intel32e):
def _translate(self, offset: int) -> Tuple[int, int, str]:
return self._translate_swap(self, offset, self._bits_per_register // 2)
class LinuxMixin(Intel):
@functools.cached_property
def _register_mask(self) -> int:
return (1 << self._bits_per_register) - 1
@functools.cached_property
def _physical_mask(self) -> int:
# From kernels 4.18 the physical mask is dynamic: See AMD SME, Intel Multi-Key Total
# Memory Encryption and CONFIG_DYNAMIC_PHYSICAL_MASK: 94d49eb30e854c84d1319095b5dd0405a7da9362
physical_mask = (1 << self._maxphyaddr) - 1
# TODO: Come back once SME support is available in the framework
return physical_mask
@functools.cached_property
def page_mask(self) -> int:
# Note that within the Intel class it's a class method. However, since it uses
# complement operations and we are working in Python, it would be more careful to
# limit it to the architecture's pointer size.
return ~(self.page_size - 1) & self._register_mask
@functools.cached_property
def _physical_page_mask(self) -> int:
return self.page_mask & self._physical_mask
@functools.cached_property
def _pte_pfn_mask(self) -> int:
return self._physical_page_mask
@functools.cached_property
def _pte_flags_mask(self) -> int:
return ~self._pte_pfn_mask & self._register_mask
def _pte_flags(self, pte) -> int:
return pte & self._pte_flags_mask
def _is_pte_present(self, entry: int) -> bool:
return (
self._pte_flags(entry) & (self._PAGE_PRESENT | self._PAGE_PROTNONE)
) != 0
def _page_is_valid(self, entry: int) -> bool:
# Overrides the Intel static method with the Linux-specific implementation
return self._is_pte_present(entry)
def _pte_needs_invert(self, entry) -> bool:
# Entries that were set to PROT_NONE (PAGE_PRESENT) are inverted
# A clear PTE shouldn't be inverted. See f19f5c4
return entry and not (entry & self._PAGE_PRESENT)
def _protnone_mask(self, entry: int) -> int:
"""Gets a mask to XOR with the page table entry to get the correct PFN"""
return self._register_mask if self._pte_needs_invert(entry) else 0
def _pte_pfn(self, entry: int) -> int:
"""Extracts the page frame number from the page table entry"""
pfn = entry ^ self._protnone_mask(entry)
return (pfn & self._pte_pfn_mask) >> self.page_shift
class LinuxIntel(LinuxMixin, Intel):
pass
class LinuxIntelPAE(LinuxMixin, IntelPAE):
pass
class LinuxIntel32e(LinuxMixin, Intel32e):
# In the Linux kernel, the __PHYSICAL_MASK_SHIFT is a mask used to extract the
# physical address from a PTE. In Volatility3, this is referred to as _maxphyaddr.
#
# Until kernel version 4.17, Linux x86-64 used a 46-bit mask. With commit
# b83ce5ee91471d19c403ff91227204fb37c95fb2, this was extended to 52 bits,
# applying to both 4 and 5-level page tables.
#
# We initially used 52 bits for all Intel 64-bit systems, but this produced incorrect
# results for PROT_NONE pages. Since the mask value is defined by a preprocessor macro,
# it's difficult to detect the exact bit shift used in the current kernel.
# Using 46 bits has proven reliable for our use case, as seen in tools like crashtool.
_maxphyaddr = 46
+3 -1
View File
@@ -48,7 +48,7 @@ if HAS_LEECHCORE:
try:
self._handle = leechcorepyc.LeechCore(self._device)
except TypeError:
raise IOError(f"Unable to open LeechCore device {self._device}")
raise OSError(f"Unable to open LeechCore device {self._device}")
return self._handle
def fileno(self):
@@ -129,6 +129,8 @@ if HAS_LEECHCORE:
def readline(self, __size: Optional[int] = ...) -> bytes:
data = b""
if not __size:
__size = 0
while __size > self._chunk_size or __size < 0:
data += self.read(self._chunk_size)
index = data.find(b"\n")
+3 -3
View File
@@ -104,7 +104,7 @@ class LimeStacker(interfaces.automagic.StackerLayerInterface):
except LimeFormatException:
return None
new_name = context.layers.free_layer_name("LimeLayer")
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
return LimeLayer(context, new_name, new_name)
+2 -2
View File
@@ -194,7 +194,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
) -> None:
super().__init__(context, config_path, name, metadata)
self._base_layer = self.config["base_layer"]
self._pages = self.config.get("pages", None)
self._pages = self.config.get("pages", [])
self._pages_len = len(self._pages)
if not self._pages:
raise PDBFormatException(name, "Invalid/no pages specified")
@@ -225,7 +225,7 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
returned = 0
page_size = self._pdb_layer.page_size
while length > 0:
page = math.floor((offset + returned) / page_size)
page = (offset + returned) // page_size
page_position = (offset + returned) % page_size
chunk_size = min(page_size - page_position, length)
if page >= self._pages_len:
+4 -4
View File
@@ -236,7 +236,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
if self._architecture is None:
vollog.log(
constants.LOGLEVEL_VV,
f"QEVM architecture could not be determined",
"QEVM architecture could not be determined",
)
# Once all segments have been read, determine the PCI hole if any
@@ -486,9 +486,9 @@ class QemuStacker(interfaces.automagic.StackerLayerInterface):
except exceptions.LayerException:
return None
new_name = context.layers.free_layer_name("QemuSuspendLayer")
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
layer = QemuSuspendLayer(context, new_name, new_name)
cls.stacker_slow_warning()
return layer
+17 -14
View File
@@ -140,7 +140,13 @@ class RegistryHive(linear.LinearlyMappedLayer):
"""Returns the appropriate Node, interpreted from the Cell based on its
Signature."""
cell = self.get_cell(cell_offset)
signature = cell.cast("string", max_length=2, encoding="latin-1")
try:
signature = cell.cast("string", max_length=2, encoding="latin-1")
except (RegistryInvalidIndex, exceptions.InvalidAddressException):
vollog.debug(
f"Failed to get cell signature for cell (0x{cell.vol.offset:x})"
)
return cell
if signature == "nk":
return cell.u.KeyNode
elif signature == "sk":
@@ -156,9 +162,7 @@ class RegistryHive(linear.LinearlyMappedLayer):
else:
# It doesn't matter that we use KeyNode, we're just after the first two bytes
vollog.debug(
"Unknown Signature {} (0x{:x}) at offset {}".format(
signature, cell.u.KeyNode.Signature, cell_offset
)
f"Unknown Signature {signature} (0x{cell.u.KeyNode.Signature:x}) at offset {cell_offset}"
)
return cell
@@ -170,14 +174,15 @@ class RegistryHive(linear.LinearlyMappedLayer):
return_list specifies whether the return result will be a single
node (default) or a list of nodes from root to the current node
(if return_list is true).
Raises RegistryFormatException if an invalid structure is encountered
Raises KeyError if the key is not found
"""
root_node = self.get_node(self.root_cell_offset)
if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"):
raise RegistryFormatException(
self.name,
"Encountered {} instead of _CM_KEY_NODE".format(
root_node.vol.type_name
),
f"Encountered {root_node.vol.type_name} instead of _CM_KEY_NODE",
)
node_key = [root_node]
if key.endswith("\\"):
@@ -187,9 +192,9 @@ class RegistryHive(linear.LinearlyMappedLayer):
while key_array and node_key:
subkeys = node_key[-1].get_subkeys()
for subkey in subkeys:
# registry keys are not case sensitive so compare lowercase
# https://msdn.microsoft.com/en-us/library/windows/desktop/ms724946(v=vs.85).aspx
if subkey.get_name().lower() == key_array[0].lower():
# registry keys are not case sensitive so compare likewise
# https://learn.microsoft.com/en-us/windows/win32/sysinfo/structure-of-the-registry
if subkey.get_name().casefold() == key_array[0].casefold():
node_key = node_key + [subkey]
found_key, key_array = found_key + [key_array[0]], key_array[1:]
break
@@ -318,10 +323,8 @@ class RegistryHive(linear.LinearlyMappedLayer):
with contextlib.suppress(exceptions.InvalidAddressException):
# Pass this to the lower layers for now
return all(
[
self.context.layers[layer].is_valid(offset, length)
for (_, _, offset, length, layer) in self.mapping(offset, length)
]
self.context.layers[layer].is_valid(offset, length)
for (_, _, offset, length, layer) in self.mapping(offset, length)
)
return False
+8 -2
View File
@@ -29,7 +29,7 @@ except ImportError:
try:
# Import so that the handler is found by the framework.class_subclasses callc
import smb.SMBHandler # lgtm [py/unused-import]
from smb import SMBHandler as SMBHandler # lgtm [py/unused-import]
except ImportError:
# If we fail to import this, it means that SMB handling won't be available
pass
@@ -57,7 +57,7 @@ def cascadeCloseFile(new_fp: IO[bytes], original_fp: IO[bytes]) -> IO[bytes]:
return new_fp
class ResourceAccessor(object):
class ResourceAccessor:
"""Object for opening URLs as files (downloading locally first if
necessary)"""
@@ -151,6 +151,12 @@ class ResourceAccessor(object):
raise excp
else:
raise excp
except ValueError as excp:
# Reraise errors such as proxy auth errors as offline exception errors
# Example Proxy auth error - ValueError: AbstractDigestAuthHandler does not support the following scheme: 'Negotiate'
vollog.info(f"Cannot access {url} due to {excp} - Setting OFFLINE")
constants.OFFLINE = True
raise exceptions.OfflineException(url)
except exceptions.OfflineException:
vollog.info(f"Not accessing {url} in offline mode")
raise
@@ -5,7 +5,7 @@ import re
from typing import Generator, List, Tuple, Dict, Optional
from volatility3.framework.interfaces import layers
from volatility3.framework.layers.scanners import multiregexp
from volatility3.framework.layers.scanners import multiregexp as multiregexp
class BytesScanner(layers.ScannerInterface):
@@ -72,7 +72,7 @@ class MultiStringScanner(layers.ScannerInterface):
return None
for char in value:
trie[char] = trie.get(char, {})
trie.setdefault(char, {})
trie = trie[char]
# Mark the end of a string
@@ -6,7 +6,7 @@ import re
from typing import Generator, List, Tuple
class MultiRegexp(object):
class MultiRegexp:
"""Algorithm for multi-string matching."""
def __init__(self) -> None:
+3 -5
View File
@@ -51,10 +51,8 @@ class NonLinearlySegmentedLayer(
try:
base_layer = self._context.layers[self._base_layer]
return all(
[
base_layer.is_valid(mapped_offset)
for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length)
]
base_layer.is_valid(mapped_offset)
for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length)
)
except exceptions.InvalidAddressException:
return False
@@ -152,7 +150,7 @@ class NonLinearlySegmentedLayer(
raise ValueError("SegmentedLayer must contain some segments")
if self._maxaddr is None:
mapped, _, length, _ = self._segments[-1]
self._maxaddr = mapped + length
self._maxaddr = mapped + length - 1
return self._maxaddr
@property
+4
View File
@@ -57,6 +57,10 @@ class VmwareLayer(segmented.SegmentedLayer):
)
meta_layer = self.context.layers.get(self._meta_layer, None)
if meta_layer is None:
raise exceptions.LayerException(
self._meta_layer, "VMware: Meta layer not found"
)
header_size = struct.calcsize(self.header_structure)
data = meta_layer.read(0, header_size)
magic, unknown, groupCount = struct.unpack(self.header_structure, data)
+8 -8
View File
@@ -5,6 +5,7 @@ from typing import Optional
from volatility3.framework import constants, interfaces, exceptions
from volatility3.framework.layers import elf
from volatility3.framework.symbols import intermed
from volatility3.framework.constants.linux import ELF_CLASS
vollog = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
ELF_CLASS = ELF_CLASS.ELFCLASS64
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
@@ -53,6 +54,7 @@ class XenCoreDumpLayer(elf.Elf64Layer):
segments = []
self._segment_headers = []
segment_names = None
for sindex in range(ehdr.e_shnum):
shdr = self.context.object(
@@ -115,12 +117,10 @@ class XenCoreDumpLayer(elf.Elf64Layer):
)
)
elif p2m_data and pfn_data:
raise elf.ElfFormatException(
self.name, f"Both P2M and PFN in Xen Core Dump"
)
raise elf.ElfFormatException(self.name, "Both P2M and PFN in Xen Core Dump")
else:
raise elf.ElfFormatException(
self.name, f"Neither P2M nor PFN in Xen Core Dump"
self.name, "Neither P2M nor PFN in Xen Core Dump"
)
if len(segments) == 0:
@@ -173,8 +173,8 @@ class XenCoreDumpStacker(elf.Elf64Stacker):
vollog.log(constants.LOGLEVEL_VVVV, f"Exception: {excp}")
return None
new_name = context.layers.free_layer_name("XenCoreDumpLayer")
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
return XenCoreDumpLayer(context, new_name, new_name)
+13 -17
View File
@@ -35,13 +35,13 @@ def convert_data_to_value(
data_format: DataFormatInfo,
) -> TUnion[int, float, bytes, str, bool]:
"""Converts a series of bytes to a particular type of value."""
if struct_type == int:
if struct_type is int:
return int.from_bytes(
data, byteorder=data_format.byteorder, signed=data_format.signed
)
if struct_type == bool:
if struct_type is bool:
struct_format = "?"
elif struct_type == float:
elif struct_type is float:
float_vals = "zzezfzzzd"
if (
data_format.length > len(float_vals)
@@ -70,7 +70,7 @@ def convert_value_to_data(
f"Written value is not of the correct type for {struct_type.__name__}"
)
if struct_type == int and isinstance(value, int):
if struct_type is int and isinstance(value, int):
# Doubling up on the isinstance is for mypy
return int.to_bytes(
value,
@@ -78,9 +78,9 @@ def convert_value_to_data(
byteorder=data_format.byteorder,
signed=data_format.signed,
)
if struct_type == bool:
if struct_type is bool:
struct_format = "?"
elif struct_type == float:
elif struct_type is float:
float_vals = "zzezfzzzd"
if (
data_format.length > len(float_vals)
@@ -152,7 +152,7 @@ class PrimitiveObject(interfaces.objects.ObjectInterface):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
data_format: DataFormatInfo,
new_value: TUnion[int, float, bool, bytes, str] = None,
new_value: Optional[TUnion[int, float, bool, bytes, str]] = None,
**kwargs,
) -> "PrimitiveObject":
"""Creates the appropriate class and returns it so that the native type
@@ -601,7 +601,7 @@ class Enumeration(interfaces.objects.ObjectInterface, int):
inverse_choices[v] = k
return inverse_choices
def lookup(self, value: int = None) -> str:
def lookup(self, value: Optional[int] = None) -> str:
"""Looks up an individual value and returns the associated name.
If multiple identifiers map to the same value, the first matching identifier will be returned
@@ -690,7 +690,7 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence):
type_name: str,
object_info: interfaces.objects.ObjectInformation,
count: int = 0,
subtype: templates.ObjectTemplate = None,
subtype: Optional[templates.ObjectTemplate] = None,
) -> None:
super().__init__(context=context, type_name=type_name, object_info=object_info)
self._vol["count"] = count
@@ -768,12 +768,10 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence):
raise IndexError(f"Member not present in array template: {child}")
@overload
def __getitem__(self, i: int) -> interfaces.objects.Template:
...
def __getitem__(self, i: int) -> interfaces.objects.Template: ...
@overload
def __getitem__(self, s: slice) -> List[interfaces.objects.Template]:
...
def __getitem__(self, s: slice) -> List[interfaces.objects.Template]: ...
def __getitem__(self, i):
"""Returns the i-th item from the array."""
@@ -930,10 +928,8 @@ class AggregateType(interfaces.objects.ObjectInterface):
members, collections.abc.Mapping
), f"{agg_name} members parameter must be a mapping: {type(members)}"
assert all(
[
(isinstance(member, tuple) and len(member) == 2)
for member in members.values()
]
(isinstance(member, tuple) and len(member) == 2)
for member in members.values()
), f"{agg_name} members must be a tuple of relative_offsets and templates"
def member(self, attr: str = "member") -> object:
+26 -2
View File
@@ -7,16 +7,38 @@ from typing import Optional, Union
from volatility3.framework import interfaces, objects, constants
def rol(value: int, count: int, max_bits: int = 64) -> int:
"""A rotate-left instruction in Python"""
max_bits_mask = (1 << max_bits) - 1
return (value << count % max_bits) & max_bits_mask | (
(value & max_bits_mask) >> (max_bits - (count % max_bits))
)
def bswap_32(value: int) -> int:
value = ((value << 8) & 0xFF00FF00) | ((value >> 8) & 0x00FF00FF)
return ((value << 16) | (value >> 16)) & 0xFFFFFFFF
def bswap_64(value: int) -> int:
low = bswap_32(value >> 32)
high = bswap_32(value & 0xFFFFFFFF)
return ((high << 32) | low) & 0xFFFFFFFFFFFFFFFF
def array_to_string(
array: "objects.Array", count: Optional[int] = None, errors: str = "replace"
) -> interfaces.objects.ObjectInterface:
"""Takes a volatility Array of characters and returns a string."""
# TODO: Consider checking the Array's target is a native char
if count is None:
count = array.vol.count
if not isinstance(array, objects.Array):
raise TypeError("Array_to_string takes an Array of char")
if count is None:
count = array.vol.count
return array.cast("string", max_length=count, errors=errors)
@@ -24,8 +46,10 @@ def pointer_to_string(pointer: "objects.Pointer", count: int, errors: str = "rep
"""Takes a volatility Pointer to characters and returns a string."""
if not isinstance(pointer, objects.Pointer):
raise TypeError("pointer_to_string takes a Pointer")
if count < 1:
raise ValueError("pointer_to_string requires a positive count")
char = pointer.dereference()
return char.cast("string", max_length=count, errors=errors)
@@ -14,8 +14,8 @@ vollog = logging.getLogger(__name__)
class ConfigWriter(plugins.PluginInterface):
"""Runs the automagics and both prints and outputs configuration in the
output directory."""
"""Runs the automagics and both prints and outputs configuration in the \
output directory."""
_required_framework_version = (2, 0, 0)

Some files were not shown because too many files have changed in this diff Show More