mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-17 20:35:40 +02:00
Merge branch 'develop' into fbdev_plugin
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: Black python linter
|
||||
name: Black python formatter
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "."
|
||||
@@ -16,10 +16,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install Cmake
|
||||
pip install build
|
||||
pip install -r ./test/requirements-testing.txt
|
||||
python -m pip install --upgrade pip Cmake build
|
||||
pip install .[test]
|
||||
|
||||
- name: Build PyPi packages
|
||||
run: |
|
||||
@@ -27,10 +25,13 @@ jobs:
|
||||
|
||||
- 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: |
|
||||
@@ -41,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 *.bin
|
||||
rm -rf *.img
|
||||
rm -rf test_images
|
||||
cd volatility3/symbols
|
||||
rm -rf linux
|
||||
rm -rf linux.zip
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -18,62 +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.8.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 pip. However, as noted in the Quick Start section below, Volatility 3 does not *need* to be installed 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
|
||||
pip3 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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,9 +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
|
||||
yara-x
|
||||
pycryptodome
|
||||
pefile
|
||||
@@ -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.
|
||||
|
||||
+5
-7
@@ -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']
|
||||
|
||||
@@ -27,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.
|
||||
|
||||
@@ -40,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
|
||||
@@ -80,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
|
||||
~~~~~~~~~~~~
|
||||
@@ -157,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
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -152,7 +150,7 @@ but can be overridden by creating a JSON file (`%APPDATA%/volatility3/vol.json`
|
||||
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 (`<` means is overridden by):
|
||||
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`
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[mypy]
|
||||
mypy_path = ./stubs
|
||||
show_traceback = True
|
||||
ignore_missing_imports = True
|
||||
+1
-3
@@ -36,6 +36,7 @@ dev = [
|
||||
"jsonschema>=4.23.0,<5",
|
||||
"pyinstaller>=6.11.0,<7",
|
||||
"pyinstaller-hooks-contrib>=2024.9",
|
||||
"types-jsonschema>=4.23.0,<5",
|
||||
]
|
||||
|
||||
test = [
|
||||
@@ -72,9 +73,6 @@ include = ["volatility3*"]
|
||||
mypy_path = "./stubs"
|
||||
show_traceback = true
|
||||
|
||||
[tool.mypy.overrides]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
-r requirements.txt
|
||||
|
||||
# This can improve error messages regarding improperly configured ISF files,
|
||||
# but is only recommended for development
|
||||
jsonschema>=2.3.0
|
||||
|
||||
# Used to build executable file
|
||||
pyinstaller>=6.5.0
|
||||
pyinstaller-hooks-contrib>=2024.3
|
||||
@@ -1,2 +0,0 @@
|
||||
# These packages are required for core functionality.
|
||||
pefile>=2023.2.7 #foo
|
||||
@@ -1,24 +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
|
||||
|
||||
|
||||
def get_requires(filename):
|
||||
requirements = []
|
||||
with open(filename, "r", encoding="utf-8") as fh:
|
||||
for line in fh.readlines():
|
||||
stripped_line = line.strip()
|
||||
if stripped_line == "" or stripped_line.startswith(("#", "-r")):
|
||||
continue
|
||||
requirements.append(stripped_line)
|
||||
return requirements
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
extras_require={
|
||||
"dev": get_requires("requirements-dev.txt"),
|
||||
"full": get_requires("requirements.txt"),
|
||||
},
|
||||
)
|
||||
+2
-4
@@ -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:
|
||||
|
||||
+28
-5
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ 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
|
||||
@@ -84,8 +86,7 @@ class TestActionsDecoding(unittest.TestCase):
|
||||
self.assertEqual(actions[0].action_type, scheduled_tasks.ActionType.Exe)
|
||||
except Exception:
|
||||
self.fail(
|
||||
"ActionDecoder.decode should not raise exception:\n%s"
|
||||
% traceback.format_exc()
|
||||
f"ActionDecoder.decode should not raise exception:\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +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
|
||||
yara-x>=0.5.0
|
||||
|
||||
pytest>=7.0.0
|
||||
+540
-84
@@ -14,6 +14,7 @@ import tempfile
|
||||
import hashlib
|
||||
import ntpath
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
#
|
||||
# HELPER FUNCTIONS
|
||||
@@ -38,7 +39,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
|
||||
+ [
|
||||
@@ -53,15 +56,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
|
||||
@@ -69,7 +127,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()
|
||||
@@ -79,7 +137,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
|
||||
@@ -89,21 +147,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()
|
||||
@@ -136,7 +194,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,
|
||||
@@ -166,7 +224,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"]
|
||||
)
|
||||
|
||||
@@ -183,7 +241,7 @@ 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
|
||||
@@ -191,17 +249,19 @@ def test_windows_svcscan(image, volatility, python):
|
||||
|
||||
|
||||
def test_windows_thrdscan(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("windows.thrdscan.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 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"]
|
||||
)
|
||||
|
||||
@@ -213,7 +273,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"]
|
||||
)
|
||||
|
||||
@@ -225,7 +285,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
|
||||
@@ -237,7 +297,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
|
||||
)
|
||||
|
||||
@@ -249,7 +309,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
|
||||
@@ -260,7 +320,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
|
||||
)
|
||||
|
||||
@@ -273,117 +333,511 @@ 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
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_sockstat(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("linux.sockstat.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
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_library_list(image, volatility, python):
|
||||
rc, out, err = runvol_plugin(
|
||||
"linux.library_list.LibraryList", 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 re.search(
|
||||
rb"gnome-settings-\s3807\s0x7f7e660b5000\s/lib/x86_64-linux-gnu/libbz2.so.1.0",
|
||||
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 re.search(
|
||||
rb"gdu-notificatio\s3878\s0x7f25ce33e000\s/usr/lib/x86_64-linux-gnu/libXau.so.6",
|
||||
out,
|
||||
|
||||
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"bash\s8600\s0x7fe78a85f000\s/lib/x86_64-linux-gnu/libnss_files.so.2",
|
||||
rb"146829\s0x88001ab5c270.*?/etc/passwd",
|
||||
out,
|
||||
)
|
||||
|
||||
assert out.count(b"\n") >= 2677
|
||||
|
||||
def test_linux_page_cache_inodepages(image, volatility, python):
|
||||
|
||||
inode_address = hex(0x88001AB5C270)
|
||||
inode_dump_filename = f"inode_{inode_address}.dmp"
|
||||
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
|
||||
|
||||
# PageVAddr PagePAddr MappingAddr .. DumpSafe
|
||||
assert re.search(
|
||||
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
|
||||
out,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -392,7 +846,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()
|
||||
@@ -405,7 +859,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()
|
||||
@@ -416,7 +870,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()
|
||||
@@ -427,7 +881,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
|
||||
@@ -437,7 +891,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
|
||||
@@ -446,7 +900,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
|
||||
@@ -454,7 +908,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
|
||||
@@ -462,7 +916,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
|
||||
@@ -471,7 +925,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
|
||||
@@ -481,7 +935,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
|
||||
@@ -490,7 +944,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
|
||||
@@ -499,7 +953,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()
|
||||
@@ -509,7 +963,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
|
||||
@@ -517,7 +971,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
|
||||
|
||||
+16
-21
@@ -19,7 +19,7 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from typing import Any, Dict, List, Tuple, Type, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
||||
from urllib import parse, request
|
||||
|
||||
try:
|
||||
@@ -57,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.
|
||||
@@ -81,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()
|
||||
@@ -126,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",
|
||||
@@ -360,10 +358,9 @@ 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):
|
||||
plugin_parser = subparser.add_parser(
|
||||
@@ -385,7 +382,9 @@ class CommandLine:
|
||||
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}"
|
||||
@@ -413,7 +412,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,
|
||||
@@ -719,9 +718,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]):
|
||||
@@ -794,7 +791,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 (
|
||||
@@ -867,9 +864,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
|
||||
@@ -884,7 +879,7 @@ class CommandLine:
|
||||
volatility3.framework.configuration.requirements.ListRequirement,
|
||||
):
|
||||
# Allow a list of integers, specified with the convenient 0x hexadecimal format
|
||||
if requirement.element_type == int:
|
||||
if requirement.element_type is int:
|
||||
additional["type"] = lambda x: int(x, 0)
|
||||
else:
|
||||
additional["type"] = requirement.element_type
|
||||
|
||||
@@ -74,9 +74,9 @@ class ColumnFilter:
|
||||
"""Identifies whether an item is found in the appropriate column"""
|
||||
try:
|
||||
if self.regex:
|
||||
return re.search(self.pattern, f"{item}")
|
||||
return bool(re.search(self.pattern, f"{item}"))
|
||||
return self.pattern in f"{item}"
|
||||
except IOError:
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def found(self, row: List[Any]) -> bool:
|
||||
|
||||
@@ -49,10 +49,12 @@ def hex_bytes_as_text(value: bytes, width: int = 16) -> str:
|
||||
output += "\n"
|
||||
printables = ""
|
||||
|
||||
# Handle leftovers when the lenght is not mutiple of width
|
||||
# Handle leftovers when the length is not mutiple of width
|
||||
if printables:
|
||||
output += " " * (width - len(printables))
|
||||
padding = width - len(printables)
|
||||
output += " " * padding
|
||||
output += printables
|
||||
output += " " * padding
|
||||
|
||||
return output
|
||||
|
||||
@@ -130,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)
|
||||
|
||||
@@ -174,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}"),
|
||||
}
|
||||
@@ -254,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}"),
|
||||
}
|
||||
@@ -340,7 +342,7 @@ class PrettyTextRenderer(CLIRenderer):
|
||||
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]
|
||||
@@ -448,7 +450,7 @@ 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])),
|
||||
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)
|
||||
@@ -465,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,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__()
|
||||
@@ -282,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]
|
||||
@@ -331,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,
|
||||
|
||||
@@ -203,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
|
||||
@@ -240,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
|
||||
@@ -250,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")
|
||||
@@ -262,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:
|
||||
@@ -347,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(
|
||||
@@ -479,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")
|
||||
@@ -585,7 +585,6 @@ class NullFileHandler(io.BytesIO, interfaces.plugins.FileHandlerInterface):
|
||||
|
||||
def writelines(self, lines: Iterable[bytes]):
|
||||
"""Dummy method"""
|
||||
pass
|
||||
|
||||
def write(self, b: bytes):
|
||||
"""Dummy method"""
|
||||
|
||||
@@ -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
|
||||
@@ -20,7 +20,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
|
||||
@@ -61,7 +61,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 +69,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -60,7 +60,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 +68,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
|
||||
|
||||
@@ -6,28 +6,12 @@
|
||||
import glob
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
required_python_version = (3, 8, 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
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Type, TypeVar
|
||||
|
||||
from volatility3.framework import constants, interfaces
|
||||
|
||||
@@ -56,27 +40,25 @@ 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 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: Optional[Type] = 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 +81,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 +142,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 +166,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
|
||||
@@ -209,8 +184,7 @@ def _zipwalk(path: str):
|
||||
zip_results[os.path.join(path, os.path.dirname(file.filename))] = (
|
||||
dirlist
|
||||
)
|
||||
for value in zip_results:
|
||||
yield value, zip_results[value]
|
||||
yield from zip_results.items()
|
||||
|
||||
|
||||
def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
#
|
||||
|
||||
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
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import intel, scanners
|
||||
from volatility3.framework.symbols import linux
|
||||
|
||||
@@ -27,16 +25,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 +34,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(
|
||||
@@ -80,14 +65,14 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
|
||||
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"
|
||||
else:
|
||||
layer_class = intel.LinuxIntel
|
||||
dtb_symbol_name = "swapper_pg_dir"
|
||||
|
||||
dtb = cls.virtual_to_physical_address(
|
||||
@@ -156,6 +141,18 @@ 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 = (
|
||||
@@ -174,9 +171,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
|
||||
|
||||
@@ -199,5 +194,8 @@ 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]
|
||||
|
||||
@@ -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(
|
||||
@@ -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
|
||||
|
||||
@@ -215,9 +215,7 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
|
||||
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(
|
||||
@@ -270,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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -492,6 +494,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 +574,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,11 +142,11 @@ 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
|
||||
@@ -160,8 +160,31 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
|
||||
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}")
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import sys
|
||||
|
||||
required_python_version = (3, 8, 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(
|
||||
f"Volatility framework requires python version {required_python_version[0]}.{required_python_version[1]}.{required_python_version[2]} or greater"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -526,19 +526,19 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
|
||||
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"
|
||||
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(
|
||||
@@ -546,7 +546,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
|
||||
context: interfaces.context.ContextInterface,
|
||||
config_path: str,
|
||||
accumulator: Optional[
|
||||
List[interfaces.configuration.VersionableInterface]
|
||||
Set[interfaces.configuration.VersionableInterface]
|
||||
] = None,
|
||||
) -> Dict[str, interfaces.configuration.RequirementInterface]:
|
||||
# Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type
|
||||
@@ -580,7 +580,7 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
|
||||
)
|
||||
|
||||
if result:
|
||||
result.update({config_path: self})
|
||||
result[config_path] = self
|
||||
return result
|
||||
|
||||
context.config[interfaces.configuration.path_join(config_path, self.name)] = (
|
||||
@@ -604,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__(
|
||||
@@ -627,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,
|
||||
@@ -664,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}
|
||||
|
||||
|
||||
@@ -6,20 +6,21 @@
|
||||
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,
|
||||
VERSION_MAJOR,
|
||||
VERSION_MINOR,
|
||||
VERSION_PATCH,
|
||||
VERSION_SUFFIX,
|
||||
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,
|
||||
)
|
||||
|
||||
PLUGINS_PATH = [
|
||||
@@ -65,7 +66,11 @@ LOGLEVEL_VVV = 7
|
||||
LOGLEVEL_VVVV = 6
|
||||
"""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"
|
||||
|
||||
@@ -1,11 +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 = 11 # Number of changes that only add to the interface
|
||||
VERSION_MINOR = 13 # 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]])
|
||||
".".join(str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH])
|
||||
+ VERSION_SUFFIX
|
||||
)
|
||||
"""The canonical version of the volatility3 package"""
|
||||
|
||||
@@ -229,7 +229,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,
|
||||
@@ -356,7 +356,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.
|
||||
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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:
|
||||
@@ -498,9 +496,7 @@ class SimpleTypeRequirement(RequirementInterface):
|
||||
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 {}
|
||||
@@ -622,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__"""
|
||||
@@ -656,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",
|
||||
|
||||
@@ -85,7 +85,7 @@ 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
|
||||
@@ -114,6 +114,7 @@ class ContextInterface(metaclass=ABCMeta):
|
||||
"""
|
||||
return copy.deepcopy(self)
|
||||
|
||||
@abstractmethod
|
||||
def module(
|
||||
self,
|
||||
module_name: str,
|
||||
@@ -232,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,
|
||||
@@ -277,27 +278,35 @@ 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."""
|
||||
|
||||
@abstractmethod
|
||||
def symbols(self) -> List:
|
||||
"""Lists the symbols contained in the symbol table for this module"""
|
||||
|
||||
@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."""
|
||||
@@ -343,6 +352,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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -374,6 +374,7 @@ class Template:
|
||||
f"{self.__class__.__name__} object has no attribute {attr}"
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
context: "interfaces.context.ContextInterface",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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."""
|
||||
@@ -378,7 +378,7 @@ class NativeTableInterface(BaseSymbolTableInterface):
|
||||
return []
|
||||
|
||||
|
||||
class MetadataInterface(object):
|
||||
class MetadataInterface:
|
||||
"""Interface for accessing metadata stored within a symbol table."""
|
||||
|
||||
def __init__(self, json_data: Dict) -> None:
|
||||
|
||||
@@ -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
|
||||
@@ -138,7 +137,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
|
||||
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(0, ulong_bitmap_array.vol.count):
|
||||
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:
|
||||
@@ -166,7 +165,7 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
|
||||
seg_first_bit = None
|
||||
# Some pages in this 32 bits array are mapped and some aren't
|
||||
else:
|
||||
for inner_bit_position in range(0, 32):
|
||||
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:
|
||||
@@ -220,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
|
||||
|
||||
@@ -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,18 +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()
|
||||
@functools.lru_cache
|
||||
def page_size(cls) -> int:
|
||||
"""Page size for the intel memory layers.
|
||||
|
||||
@@ -83,25 +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()
|
||||
@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
|
||||
|
||||
@@ -115,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
|
||||
@@ -137,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
|
||||
"""
|
||||
@@ -163,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.
|
||||
|
||||
@@ -203,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
|
||||
@@ -239,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
|
||||
@@ -252,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(
|
||||
@@ -501,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -156,9 +156,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
|
||||
|
||||
@@ -178,9 +176,7 @@ class RegistryHive(linear.LinearlyMappedLayer):
|
||||
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("\\"):
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -54,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,8 +22,8 @@ def bswap_32(value: int) -> int:
|
||||
|
||||
|
||||
def bswap_64(value: int) -> int:
|
||||
low = bswap_32((value >> 32))
|
||||
high = bswap_32((value & 0xFFFFFFFF))
|
||||
low = bswap_32(value >> 32)
|
||||
high = bswap_32(value & 0xFFFFFFFF)
|
||||
|
||||
return ((high << 32) | low) & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import pathlib
|
||||
import zipfile
|
||||
from typing import Generator, List
|
||||
from importlib.util import find_spec
|
||||
|
||||
from volatility3 import schemas, symbols
|
||||
from volatility3.framework import constants, interfaces, renderers
|
||||
@@ -96,16 +97,12 @@ class IsfInfo(plugins.PluginInterface):
|
||||
if filter_item in isf_file:
|
||||
filtered_list.append(isf_file)
|
||||
|
||||
try:
|
||||
import jsonschema
|
||||
|
||||
if not self.config["validate"]:
|
||||
raise ImportError # Act as if we couldn't import if validation is turned off
|
||||
if find_spec("jsonschema") and self.config["validate"]:
|
||||
|
||||
def check_valid(data):
|
||||
return "True" if schemas.validate(data, True) else "False"
|
||||
|
||||
except ImportError:
|
||||
else:
|
||||
|
||||
def check_valid(data):
|
||||
return "Unknown"
|
||||
|
||||
@@ -119,7 +119,7 @@ class LayerWriter(plugins.PluginInterface):
|
||||
# Update the filename, which may have changed if a file
|
||||
# with the same name already existed.
|
||||
output_name = file_handle.preferred_filename
|
||||
except IOError as excp:
|
||||
except OSError as excp:
|
||||
yield 0, (f"Layer cannot be written to {output_name}: {excp}",)
|
||||
|
||||
yield 0, (f"Layer has been written to {output_name}",)
|
||||
|
||||
@@ -22,6 +22,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Recovers bash command history from memory."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -32,7 +33,7 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
|
||||
@@ -15,8 +15,7 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
|
||||
"""Shows the time the system was started"""
|
||||
|
||||
_required_framework_version = (2, 11, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -27,7 +26,7 @@ class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface)
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 3, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -49,9 +49,8 @@ class CapabilitiesData:
|
||||
class Capabilities(plugins.PluginInterface):
|
||||
"""Lists process capabilities"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_required_framework_version = (2, 13, 0)
|
||||
_version = (1, 1, 1)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -62,7 +61,7 @@ class Capabilities(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pids",
|
||||
@@ -87,7 +86,7 @@ class Capabilities(plugins.PluginInterface):
|
||||
try:
|
||||
kernel_cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap")
|
||||
except exceptions.SymbolError:
|
||||
# It should be a kernel < 3.2
|
||||
# It should be a kernel < 3.2 See 73efc0394e148d0e15583e13712637831f926720
|
||||
return None
|
||||
|
||||
vol2_last_cap = extensions.kernel_cap_struct.get_last_cap_value()
|
||||
@@ -137,7 +136,7 @@ class Capabilities(plugins.PluginInterface):
|
||||
comm=utility.array_to_string(task.comm),
|
||||
pid=int(task.pid),
|
||||
tgid=int(task.tgid),
|
||||
ppid=int(task.parent.pid),
|
||||
ppid=int(task.get_parent_pid()),
|
||||
euid=int(task.cred.euid),
|
||||
)
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
|
||||
"""Checks if any processes are sharing credential structures"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (2, 0, 0)
|
||||
_version = (2, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
@@ -24,7 +23,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
]
|
||||
|
||||
@@ -56,7 +55,7 @@ class Check_creds(interfaces.plugins.PluginInterface):
|
||||
|
||||
for cred_addr, pids in creds.items():
|
||||
if len(pids) > 1:
|
||||
pid_str = ", ".join([str(pid) for pid in pids])
|
||||
pid_str = ", ".join(str(pid) for pid in pids)
|
||||
|
||||
fields = [
|
||||
format_hints.Hex(cred_addr),
|
||||
|
||||
@@ -53,7 +53,7 @@ class Check_idt(interfaces.plugins.PluginInterface):
|
||||
address_mask = self.context.layers[vmlinux.layer_name].address_mask
|
||||
|
||||
# hw handlers + system call
|
||||
check_idxs = list(range(0, 20)) + [128]
|
||||
check_idxs = list(range(20)) + [128]
|
||||
|
||||
if is_32bit:
|
||||
if vmlinux.has_type("gate_struct"):
|
||||
|
||||
@@ -103,7 +103,7 @@ class Check_syscall(plugins.PluginInterface):
|
||||
|
||||
try:
|
||||
func_addr = vmlinux.get_symbol(syscall_entry_func).address
|
||||
except exceptions.SymbolError as e:
|
||||
except exceptions.SymbolError:
|
||||
# if we can't find the disassemble function then bail and rely on a different method
|
||||
return 0
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class Elfs(plugins.PluginInterface):
|
||||
"""Lists all memory mapped ELF files for all processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (2, 0, 1)
|
||||
_version = (2, 0, 3)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -36,7 +36,7 @@ class Elfs(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import Iterable, Tuple
|
||||
|
||||
from volatility3.framework import exceptions, renderers
|
||||
from volatility3.framework import renderers, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.objects import utility
|
||||
@@ -16,7 +17,8 @@ vollog = logging.getLogger(__name__)
|
||||
class Envars(plugins.PluginInterface):
|
||||
"""Lists processes with their environment variables"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_required_framework_version = (2, 13, 0)
|
||||
_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
@@ -28,7 +30,7 @@ class Envars(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
@@ -38,84 +40,98 @@ class Envars(plugins.PluginInterface):
|
||||
),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_task_env_variables(
|
||||
context: interfaces.context.ContextInterface,
|
||||
task: interfaces.objects.ObjectInterface,
|
||||
env_area_max_size: int = 8192,
|
||||
) -> Iterable[Tuple[str, str]]:
|
||||
"""Yields environment variables for a given task.
|
||||
|
||||
Args:
|
||||
context: The plugin's operational context.
|
||||
task: The task object from which to extract environment variables.
|
||||
env_area_max_size: Maximum allowable size for the environment variables area.
|
||||
Tasks exceeding this size will be skipped. Default is 8192.
|
||||
|
||||
Yields:
|
||||
Tuples of (key, value) representing each environment variable.
|
||||
"""
|
||||
|
||||
task_name = utility.array_to_string(task.comm)
|
||||
task_pid = task.pid
|
||||
env_start = task.mm.env_start
|
||||
env_end = task.mm.env_end
|
||||
env_area_size = env_end - env_start
|
||||
if not (0 < env_area_size <= env_area_max_size):
|
||||
vollog.debug(
|
||||
f"Task {task_pid} {task_name} appears to have environment variables of size "
|
||||
f"{env_area_size} bytes which fails the sanity checking, will not extract "
|
||||
"any envars."
|
||||
)
|
||||
return None
|
||||
|
||||
# Get process layer to read envars from
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if proc_layer_name is None:
|
||||
return None
|
||||
proc_layer = context.layers[proc_layer_name]
|
||||
|
||||
# Ensure the entire buffer is readable to prevent relying on exception handling
|
||||
if not proc_layer.is_valid(env_start, env_area_size):
|
||||
# Not mapped / swapped out
|
||||
vollog.debug(
|
||||
f"Unable to read environment variables for {task_pid} {task_name} starting at "
|
||||
f" virtual address 0x{env_start:x} for {env_area_size} bytes, will not "
|
||||
"extract any envars."
|
||||
)
|
||||
return None
|
||||
|
||||
# Read the full task environment variable buffer.
|
||||
envar_data = proc_layer.read(env_start, env_area_size)
|
||||
|
||||
# Parse envar data, envars are null terminated, keys and values are separated by '='
|
||||
envar_data = envar_data.rstrip(b"\x00")
|
||||
for envar_pair in envar_data.split(b"\x00"):
|
||||
try:
|
||||
env_key, env_value = envar_pair.decode().split("=", 1)
|
||||
except ValueError:
|
||||
# Some legitimate programs, like 'avahi-daemon', avoid reallocating the args
|
||||
# and instead exploit the fact that the environment variables area is contiguous
|
||||
# to the args. This allows them to include a longer process name in the listing,
|
||||
# causing overwrites and incorrect results. In such cases, it's better to abort
|
||||
# the current task rather than displaying misleading or incorrect output.
|
||||
break
|
||||
|
||||
yield env_key, env_value
|
||||
|
||||
def _generator(self, tasks):
|
||||
"""Generates a listing of processes along with environment variables"""
|
||||
|
||||
# walk the process list and return the envars
|
||||
for task in tasks:
|
||||
pid = task.pid
|
||||
|
||||
# get process name as string
|
||||
name = utility.array_to_string(task.comm)
|
||||
|
||||
# try and get task parent
|
||||
try:
|
||||
ppid = task.parent.pid
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.debug(
|
||||
f"Unable to read parent pid for task {pid} {name}, setting ppid to 0."
|
||||
)
|
||||
ppid = 0
|
||||
|
||||
# kernel threads never have an mm as they do not have userland mappings
|
||||
try:
|
||||
mm = task.mm
|
||||
except exceptions.InvalidAddressException:
|
||||
# no mm so cannot get envars
|
||||
vollog.debug(
|
||||
f"Unable to access mm for task {pid} {name} it is likely a kernel thread, will not extract any envars."
|
||||
)
|
||||
mm = None
|
||||
if task.is_kernel_thread:
|
||||
continue
|
||||
|
||||
# if mm exists attempt to get envars
|
||||
if mm:
|
||||
# get process layer to read envars from
|
||||
proc_layer_name = task.add_process_layer()
|
||||
if proc_layer_name is None:
|
||||
vollog.debug(
|
||||
f"Unable to construct process layer for task {pid} {name}, will not extract any envars."
|
||||
)
|
||||
continue
|
||||
proc_layer = self.context.layers[proc_layer_name]
|
||||
task_pid = task.pid
|
||||
task_name = utility.array_to_string(task.comm)
|
||||
task_ppid = task.get_parent_pid()
|
||||
|
||||
# get the size of the envars with sanity checking
|
||||
envars_size = task.mm.env_end - task.mm.env_start
|
||||
if not (0 < envars_size <= 8192):
|
||||
vollog.debug(
|
||||
f"Task {pid} {name} appears to have envars of size {envars_size} bytes which fails the sanity checking, will not extract any envars."
|
||||
)
|
||||
continue
|
||||
|
||||
# attempt to read all envars data
|
||||
try:
|
||||
envar_data = proc_layer.read(task.mm.env_start, envars_size)
|
||||
except exceptions.InvalidAddressException:
|
||||
vollog.debug(
|
||||
f"Unable to read full envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)} for {envars_size} bytes, will not extract any envars."
|
||||
)
|
||||
continue
|
||||
|
||||
# parse envar data, envars are null terminated, keys and values are separated by '='
|
||||
envar_data = envar_data.rstrip(b"\x00")
|
||||
for envar_pair in envar_data.split(b"\x00"):
|
||||
try:
|
||||
key, value = envar_pair.decode().split("=", 1)
|
||||
except ValueError:
|
||||
vollog.debug(
|
||||
f"Unable to extract envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)}, they don't appear to be '=' separated"
|
||||
)
|
||||
continue
|
||||
yield (0, (pid, ppid, name, key, value))
|
||||
for env_key, env_value in self.get_task_env_variables(self.context, task):
|
||||
yield (0, (task_pid, task_ppid, task_name, env_key, env_value))
|
||||
|
||||
def run(self):
|
||||
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
|
||||
|
||||
return renderers.TreeGrid(
|
||||
[("PID", int), ("PPID", int), ("COMM", str), ("KEY", str), ("VALUE", str)],
|
||||
self._generator(
|
||||
pslist.PsList.list_tasks(
|
||||
self.context, self.config["kernel"], filter_func=filter_func
|
||||
)
|
||||
),
|
||||
tasks = pslist.PsList.list_tasks(
|
||||
self.context, self.config["kernel"], filter_func=filter_func
|
||||
)
|
||||
|
||||
headers = [
|
||||
("PID", int),
|
||||
("PPID", int),
|
||||
("COMM", str),
|
||||
("KEY", str),
|
||||
("VALUE", str),
|
||||
]
|
||||
|
||||
return renderers.TreeGrid(headers, self._generator(tasks))
|
||||
|
||||
@@ -149,7 +149,7 @@ class ABCKmsg(ABC):
|
||||
# This might seem insignificant but it could cause some issues
|
||||
# when compared with userland tool results or when used in
|
||||
# timelines.
|
||||
return "%lu.%06lu" % (nsec / 1000000000, (nsec % 1000000000) / 1000)
|
||||
return f"{nsec / 1000000000:lu}.{(nsec % 1000000000) / 1000:06lu}"
|
||||
|
||||
def get_timestamp_in_sec_str(self, obj) -> str:
|
||||
# obj could be log, printk_log or printk_info
|
||||
@@ -166,7 +166,7 @@ class ABCKmsg(ABC):
|
||||
|
||||
def get_caller_text(self, caller_id):
|
||||
caller_name = "CPU" if caller_id & 0x80000000 else "Task"
|
||||
caller = "%s(%u)" % (caller_name, caller_id & ~0x80000000)
|
||||
caller = f"{caller_name}({caller_id & ~0x80000000:u})"
|
||||
return caller
|
||||
|
||||
def get_prefix(self, obj) -> Tuple[int, int, str, str]:
|
||||
|
||||
@@ -20,8 +20,7 @@ class Kthreads(plugins.PluginInterface):
|
||||
"""Enumerates kthread functions"""
|
||||
|
||||
_required_framework_version = (2, 11, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -35,7 +34,7 @@ class Kthreads(plugins.PluginInterface):
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 3, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
|
||||
|
||||
@@ -21,8 +21,7 @@ class LibraryList(interfaces.plugins.PluginInterface):
|
||||
"""Enumerate libraries loaded into processes"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
@@ -33,7 +32,7 @@ class LibraryList(interfaces.plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 2, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pids",
|
||||
|
||||
@@ -54,8 +54,7 @@ class Lsmod(plugins.PluginInterface):
|
||||
|
||||
table_name = modules.vol.type_name.split(constants.BANG)[0]
|
||||
|
||||
for module in modules.to_list(table_name + constants.BANG + "module", "list"):
|
||||
yield module
|
||||
yield from modules.to_list(table_name + constants.BANG + "module", "list")
|
||||
|
||||
def _generator(self):
|
||||
try:
|
||||
|
||||
@@ -110,7 +110,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists open files for each processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (2, 0, 0)
|
||||
_version = (2, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -121,7 +121,7 @@ class Lsof(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from typing import List
|
||||
import logging
|
||||
from volatility3.framework import constants, interfaces
|
||||
from volatility3.framework import interfaces
|
||||
from volatility3.framework import renderers, symbols
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.objects import utility
|
||||
@@ -18,6 +18,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
"""Lists process memory ranges that potentially contain injected code."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -28,7 +29,7 @@ class Malfind(interfaces.plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
|
||||
@@ -36,8 +36,7 @@ class MountInfo(plugins.PluginInterface):
|
||||
"""Lists mount points on processes mount namespaces"""
|
||||
|
||||
_required_framework_version = (2, 2, 0)
|
||||
|
||||
_version = (1, 2, 1)
|
||||
_version = (1, 2, 3)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -48,7 +47,7 @@ class MountInfo(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
|
||||
|
||||
@@ -389,7 +389,7 @@ class InodePages(plugins.PluginInterface):
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 1)
|
||||
_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -412,9 +412,10 @@ class InodePages(plugins.PluginInterface):
|
||||
description="Inode address",
|
||||
optional=True,
|
||||
),
|
||||
requirements.StringRequirement(
|
||||
requirements.BooleanRequirement(
|
||||
name="dump",
|
||||
description="Output file path",
|
||||
description="Extract inode content",
|
||||
default=False,
|
||||
optional=True,
|
||||
),
|
||||
]
|
||||
@@ -436,7 +437,7 @@ class InodePages(plugins.PluginInterface):
|
||||
"""
|
||||
if not inode.is_reg:
|
||||
vollog.error("The inode is not a regular file")
|
||||
return
|
||||
return None
|
||||
|
||||
# By using truncate/seek, provided the filesystem supports it, a sparse file will be
|
||||
# created, saving both disk space and I/O time.
|
||||
@@ -461,7 +462,7 @@ class InodePages(plugins.PluginInterface):
|
||||
f.seek(current_fp)
|
||||
f.write(page_bytes)
|
||||
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
vollog.error("Unable to write to file (%s): %s", filename, e)
|
||||
|
||||
def _generator(self):
|
||||
@@ -471,7 +472,7 @@ class InodePages(plugins.PluginInterface):
|
||||
|
||||
if self.config["inode"] and self.config["find"]:
|
||||
vollog.error("Cannot use --inode and --find simultaneously")
|
||||
return
|
||||
return None
|
||||
|
||||
if self.config["find"]:
|
||||
inodes_iter = Files.get_inodes(
|
||||
@@ -482,20 +483,23 @@ class InodePages(plugins.PluginInterface):
|
||||
if inode_in.path == self.config["find"]:
|
||||
inode = inode_in.inode
|
||||
break # Only the first match
|
||||
else:
|
||||
vollog.error("Unable to find inode with path %s", self.config["find"])
|
||||
return None
|
||||
|
||||
elif self.config["inode"]:
|
||||
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
|
||||
else:
|
||||
vollog.error("You must use either --inode or --find")
|
||||
return
|
||||
return None
|
||||
|
||||
if not inode.is_valid():
|
||||
vollog.error("Invalid inode at 0x%x", inode.vol.offset)
|
||||
return
|
||||
return None
|
||||
|
||||
if not inode.is_reg:
|
||||
vollog.error("The inode is not a regular file")
|
||||
return
|
||||
return None
|
||||
|
||||
inode_size = inode.i_size
|
||||
for page_obj in inode.get_pages():
|
||||
@@ -519,9 +523,13 @@ class InodePages(plugins.PluginInterface):
|
||||
yield 0, fields
|
||||
|
||||
if self.config["dump"]:
|
||||
filename = self.config["dump"]
|
||||
vollog.info("[*] Writing inode at 0x%x to '%s'", inode.vol.offset, filename)
|
||||
self.write_inode_content_to_file(inode, filename, self.open, vmlinux_layer)
|
||||
open_method = self.open
|
||||
inode_address = inode.vol.offset
|
||||
filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp")
|
||||
vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename)
|
||||
self.write_inode_content_to_file(
|
||||
inode, filename, open_method, vmlinux_layer
|
||||
)
|
||||
|
||||
def run(self):
|
||||
headers = [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Iterable
|
||||
|
||||
from volatility3.framework import renderers, interfaces, constants
|
||||
from volatility3.framework.symbols import linux
|
||||
@@ -19,8 +19,7 @@ class PIDHashTable(plugins.PluginInterface):
|
||||
"""Enumerates processes through the PID hash table"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (1, 0, 1)
|
||||
_version = (1, 0, 3)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -31,7 +30,7 @@ class PIDHashTable(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.VersionRequirement(
|
||||
name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0)
|
||||
@@ -219,7 +218,7 @@ class PIDHashTable(plugins.PluginInterface):
|
||||
|
||||
return None
|
||||
|
||||
def get_tasks(self) -> interfaces.objects.ObjectInterface:
|
||||
def get_tasks(self) -> Iterable[interfaces.objects.ObjectInterface]:
|
||||
"""Enumerates processes through the PID hash table
|
||||
|
||||
Yields:
|
||||
@@ -232,14 +231,16 @@ class PIDHashTable(plugins.PluginInterface):
|
||||
|
||||
yield from sorted(pid_func(), key=lambda t: (t.tgid, t.pid))
|
||||
|
||||
def _generator(
|
||||
self, decorate_comm: bool = False
|
||||
) -> interfaces.objects.ObjectInterface:
|
||||
def _generator(self, decorate_comm: bool = False):
|
||||
for task in self.get_tasks():
|
||||
offset, pid, tid, ppid, name = pslist.PsList.get_task_fields(
|
||||
task, decorate_comm
|
||||
task_fields = pslist.PsList.get_task_fields(task, decorate_comm)
|
||||
fields = (
|
||||
format_hints.Hex(task_fields.offset),
|
||||
task_fields.user_pid,
|
||||
task_fields.user_tid,
|
||||
task_fields.user_ppid,
|
||||
task_fields.name,
|
||||
)
|
||||
fields = format_hints.Hex(offset), pid, tid, ppid, name
|
||||
yield 0, fields
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -21,7 +21,8 @@ class Maps(plugins.PluginInterface):
|
||||
"""Lists all memory maps for all processes."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 0)
|
||||
_version = (1, 0, 2)
|
||||
|
||||
MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb
|
||||
|
||||
@classmethod
|
||||
@@ -34,7 +35,7 @@ class Maps(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
@@ -124,9 +125,7 @@ class Maps(plugins.PluginInterface):
|
||||
proc_layer_name = task.add_process_layer()
|
||||
except exceptions.InvalidAddressException as excp:
|
||||
vollog.debug(
|
||||
"Process {}: invalid address {} in layer {}".format(
|
||||
pid, excp.invalid_address, excp.layer_name
|
||||
)
|
||||
f"Process {pid}: invalid address {excp.invalid_address} in layer {excp.layer_name}"
|
||||
)
|
||||
return None
|
||||
vm_size = vm_end - vm_start
|
||||
@@ -164,7 +163,9 @@ class Maps(plugins.PluginInterface):
|
||||
address_list = self.config.get("address", None)
|
||||
if not address_list:
|
||||
# do not filter as no address_list was supplied
|
||||
vma_filter_func = lambda _: True
|
||||
def vma_filter_func(_):
|
||||
return True
|
||||
|
||||
else:
|
||||
# filter for any vm_start that matches the supplied address config
|
||||
def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool:
|
||||
|
||||
@@ -14,7 +14,8 @@ from volatility3.plugins.linux import pslist
|
||||
class PsAux(plugins.PluginInterface):
|
||||
"""Lists processes with their command line arguments"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_required_framework_version = (2, 13, 0)
|
||||
_version = (1, 1, 1)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
@@ -26,7 +27,7 @@ class PsAux(plugins.PluginInterface):
|
||||
architectures=["Intel32", "Intel64"],
|
||||
),
|
||||
requirements.PluginRequirement(
|
||||
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
|
||||
name="pslist", plugin=pslist.PsList, version=(4, 0, 0)
|
||||
),
|
||||
requirements.ListRequirement(
|
||||
name="pid",
|
||||
@@ -97,14 +98,8 @@ class PsAux(plugins.PluginInterface):
|
||||
# walk the process list and report the arguments
|
||||
for task in tasks:
|
||||
pid = task.pid
|
||||
|
||||
try:
|
||||
ppid = task.parent.pid
|
||||
except exceptions.InvalidAddressException:
|
||||
ppid = 0
|
||||
|
||||
ppid = task.get_parent_pid()
|
||||
name = utility.array_to_string(task.comm)
|
||||
|
||||
args = self._get_command_line_args(task, name)
|
||||
|
||||
yield (0, (pid, ppid, name, args))
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import datetime
|
||||
from typing import Any, Callable, Iterable, List, Tuple
|
||||
import dataclasses
|
||||
import contextlib
|
||||
from typing import Any, Callable, Iterable, List, Optional
|
||||
|
||||
from volatility3.framework import interfaces, renderers
|
||||
from volatility3.framework.configuration import requirements
|
||||
@@ -14,12 +16,25 @@ from volatility3.plugins import timeliner
|
||||
from volatility3.plugins.linux import elfs
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TaskFields:
|
||||
offset: int
|
||||
user_pid: int
|
||||
user_tid: int
|
||||
user_ppid: int
|
||||
name: str
|
||||
uid: Optional[int]
|
||||
gid: Optional[int]
|
||||
euid: Optional[int]
|
||||
egid: Optional[int]
|
||||
creation_time: Optional[datetime.datetime]
|
||||
|
||||
|
||||
class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
"""Lists the processes present in a particular linux memory image."""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
_version = (2, 3, 0)
|
||||
_required_framework_version = (2, 13, 0)
|
||||
_version = (4, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
@@ -59,7 +74,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[Any], bool]:
|
||||
def create_pid_filter(
|
||||
cls, pid_list: Optional[List[int]] = None
|
||||
) -> Callable[[Any], bool]:
|
||||
"""Constructs a filter function for process IDs.
|
||||
|
||||
Args:
|
||||
@@ -83,7 +100,7 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
@classmethod
|
||||
def get_task_fields(
|
||||
cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
|
||||
) -> Tuple[int, int, int, int, str, datetime.datetime]:
|
||||
) -> TaskFields:
|
||||
"""Extract the fields needed for the final output
|
||||
|
||||
Args:
|
||||
@@ -92,21 +109,34 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
and of Kernel threads in square brackets.
|
||||
Defaults to False.
|
||||
Returns:
|
||||
A tuple with the fields to show in the plugin output.
|
||||
A TaskFields object with the fields to show in the plugin output.
|
||||
"""
|
||||
pid = task.tgid
|
||||
tid = task.pid
|
||||
ppid = task.parent.tgid if task.parent else 0
|
||||
name = utility.array_to_string(task.comm)
|
||||
start_time = task.get_create_time()
|
||||
if decorate_comm:
|
||||
if task.is_kernel_thread:
|
||||
name = f"[{name}]"
|
||||
elif task.is_user_thread:
|
||||
name = f"{{{name}}}"
|
||||
|
||||
task_fields = (task.vol.offset, pid, tid, ppid, name, start_time)
|
||||
return task_fields
|
||||
# This function may be called with a partially initialized/uninitialized task.
|
||||
# Ensure it always returns a valid TaskFields object, ready for use in a plugin.
|
||||
valid_cred = task.cred and task.cred.is_readable()
|
||||
creation_time = None
|
||||
with contextlib.suppress(Exception):
|
||||
creation_time = task.get_create_time()
|
||||
|
||||
return TaskFields(
|
||||
offset=task.vol.offset,
|
||||
user_pid=task.tgid,
|
||||
user_tid=task.pid,
|
||||
user_ppid=task.get_parent_pid(),
|
||||
name=name,
|
||||
uid=task.cred.uid if valid_cred else None,
|
||||
gid=task.cred.gid if valid_cred else None,
|
||||
euid=task.cred.euid if valid_cred else None,
|
||||
egid=task.cred.egid if valid_cred else None,
|
||||
creation_time=creation_time,
|
||||
)
|
||||
|
||||
def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str:
|
||||
"""Extract the elf for the process if requested
|
||||
@@ -180,17 +210,19 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
else:
|
||||
file_output = "Disabled"
|
||||
|
||||
offset, pid, tid, ppid, name, creation_time = self.get_task_fields(
|
||||
task, decorate_comm
|
||||
)
|
||||
task_fields = self.get_task_fields(task, decorate_comm)
|
||||
|
||||
yield 0, (
|
||||
format_hints.Hex(offset),
|
||||
pid,
|
||||
tid,
|
||||
ppid,
|
||||
name,
|
||||
creation_time or renderers.NotAvailableValue(),
|
||||
format_hints.Hex(task_fields.offset),
|
||||
task_fields.user_pid,
|
||||
task_fields.user_tid,
|
||||
task_fields.user_ppid,
|
||||
task_fields.name,
|
||||
task_fields.uid or renderers.NotAvailableValue(),
|
||||
task_fields.gid or renderers.NotAvailableValue(),
|
||||
task_fields.euid or renderers.NotAvailableValue(),
|
||||
task_fields.egid or renderers.NotAvailableValue(),
|
||||
task_fields.creation_time or renderers.NotAvailableValue(),
|
||||
file_output,
|
||||
)
|
||||
|
||||
@@ -239,6 +271,10 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
("TID", int),
|
||||
("PPID", int),
|
||||
("COMM", str),
|
||||
("UID", int),
|
||||
("GID", int),
|
||||
("EUID", int),
|
||||
("EGID", int),
|
||||
("CREATION TIME", datetime.datetime),
|
||||
("File output", str),
|
||||
]
|
||||
@@ -252,10 +288,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
for task in self.list_tasks(
|
||||
self.context, self.config["kernel"], filter_func, include_threads=True
|
||||
):
|
||||
offset, user_pid, user_tid, _user_ppid, name, creation_time = (
|
||||
self.get_task_fields(task)
|
||||
task_fields = self.get_task_fields(task)
|
||||
description = f"Process {task_fields.user_pid}/{task_fields.user_tid} {task_fields.name} ({task_fields.offset})"
|
||||
|
||||
yield (
|
||||
description,
|
||||
timeliner.TimeLinerType.CREATED,
|
||||
task_fields.creation_time,
|
||||
)
|
||||
|
||||
description = f"Process {user_pid}/{user_tid} {name} ({offset})"
|
||||
|
||||
yield (description, timeliner.TimeLinerType.CREATED, creation_time)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user