Merge branch 'develop' into linux_sockstats_plugin

This commit is contained in:
ikelos
2023-01-06 09:35:37 +00:00
committed by GitHub
196 changed files with 16817 additions and 8375 deletions
+4 -2
View File
@@ -23,8 +23,10 @@ Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Example output**
Please copy and paste the text demonstrating the issue, ideally with verbose output turned on (`vol.py -vvv ...`).
Text is preferred to screenshots for searching and to talk about specific parts of the output.
**Additional information**
Add any other information about the problem here.
+13
View File
@@ -0,0 +1,13 @@
name: Black python linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: psf/black@stable
with:
options: "--check --diff --verbose"
src: "./volatility3"
+8 -6
View File
@@ -15,14 +15,16 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.x
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: '3.x'
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
+74
View File
@@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "develop" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "develop" ]
schedule:
- cron: '16 8 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
queries: security-and-quality # ,security-extended
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+8 -6
View File
@@ -3,14 +3,16 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7"]
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: '3.6'
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
+1 -1
View File
@@ -20,7 +20,7 @@ more details.
## Requirements
Volatility 3 requires Python 3.6.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.7.0 or later. To install the most minimal set of dependencies (some plugins will not work) use a command such as:
```shell
pip3 install -r requirements-minimal.txt
+41 -48
View File
@@ -17,61 +17,54 @@ def seekread(f, offset = None, length = 0, relative = True):
f.seek(offset, [0, 1, 2][relative])
if length:
return f.read(length)
return None
def parse_pbzx(pbzx_path):
section = 0
xar_out_path = '%s.part%02d.cpio.xz' % (pbzx_path, section)
f = open(pbzx_path, 'rb')
# pbzx = f.read()
# f.close()
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)
# Interpret the flags as a 64-bit big-endian unsigned int
flags = struct.unpack('>Q', flags)[0]
xar_f = open(xar_out_path, 'wb')
while flags & (1 << 24):
# Read in more flags
with open(pbzx_path, 'rb') as f:
# pbzx = f.read()
# f.close()
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)
# Interpret the flags as a 64-bit big-endian unsigned int
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':
# 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)
# ... and split it out ...
f_content = seekread(f, length = f_length)
section += 1
decomp_out = '%s.part%02d.cpio' % (pbzx_path, section)
g = open(decomp_out, 'wb')
g.write(f_content)
g.close()
# Now to start the next section, which should hopefully be .xz (we'll just assume it is ...)
xar_f.close()
section += 1
new_out = '%s.part%02d.cpio.xz' % (pbzx_path, section)
xar_f = open(new_out, 'wb')
else:
f_length -= 6
# This part needs buffering
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':
xar_f.close()
raise RuntimeError("Error: Footer is not xar file footer")
try:
f.close()
xar_f.close()
except IOError:
pass
while flags & (1 << 24):
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]
# 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':
# 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)
# ... and split it out ...
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:
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)
else:
f_length -= 6
# This part needs buffering
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':
raise RuntimeError("Error: Footer is not xar file footer")
def main():
+1
View File
@@ -121,6 +121,7 @@ try:
extensions.append('sphinx_autodoc_typehints')
except ImportError:
# If the autodoc typehints extension isn't available, carry on regardless
pass
# Add any paths that contain templates here, relative to this directory.
+1 -1
View File
@@ -16,7 +16,7 @@ pycryptodome
# This can improve error messages regarding improperly configured ISF files,
# but is only recommended for development
# jsonschema>=2.3.0
jsonschema>=2.3.0
# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
+33 -31
View File
@@ -6,12 +6,13 @@ import setuptools
from volatility3.framework import constants
with open("README.md", "r", encoding = "utf-8") as fh:
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
def get_install_requires():
requirements = []
with open("requirements-minimal.txt", "r", encoding="utf-8") as fh:
with open("requirements-minimal.txt", "r", encoding = "utf-8") as fh:
for line in fh.readlines():
stripped_line = line.strip()
if stripped_line == "" or stripped_line.startswith("#"):
@@ -19,32 +20,33 @@ def get_install_requires():
requirements.append(stripped_line)
return requirements
setuptools.setup(name = "volatility3",
description = "Memory forensics framework",
version = constants.PACKAGE_VERSION,
license = "VSL",
keywords = "volatility memory forensics framework windows linux volshell",
author = "Volatility Foundation",
long_description = long_description,
long_description_content_type = "text/markdown",
author_email = "volatility@volatilityfoundation.org",
url = "https://github.com/volatilityfoundation/volatility3/",
project_urls = {
"Bug Tracker": "https://github.com/volatilityfoundation/volatility3/issues",
"Documentation": "https://volatility3.readthedocs.io/",
"Source Code": "https://github.com/volatilityfoundation/volatility3",
},
python_requires = '>=3.6.0',
include_package_data = True,
exclude_package_data = {
'': ['development', 'development.*'],
'development': ['*']
},
packages = setuptools.find_namespace_packages(exclude = ["development", "development.*"]),
entry_points = {
'console_scripts': [
'vol = volatility3.cli:main',
'volshell = volatility3.cli.volshell:main',
],
},
install_requires = get_install_requires())
setuptools.setup(
name="volatility3",
description="Memory forensics framework",
version=constants.PACKAGE_VERSION,
license="VSL",
keywords="volatility memory forensics framework windows linux volshell",
author="Volatility Foundation",
long_description=long_description,
long_description_content_type="text/markdown",
author_email="volatility@volatilityfoundation.org",
url="https://github.com/volatilityfoundation/volatility3/",
project_urls={
"Bug Tracker": "https://github.com/volatilityfoundation/volatility3/issues",
"Documentation": "https://volatility3.readthedocs.io/",
"Source Code": "https://github.com/volatilityfoundation/volatility3",
},
python_requires=">=3.7.0",
include_package_data=True,
exclude_package_data={"": ["development", "development.*"], "development": ["*"]},
packages=setuptools.find_namespace_packages(
exclude=["development", "development.*"]
),
entry_points={
"console_scripts": [
"vol = volatility3.cli:main",
"volshell = volatility3.cli.volshell:main",
],
},
install_requires=get_install_requires(),
)
+35 -16
View File
@@ -3,38 +3,57 @@
import os
import pytest
def pytest_addoption(parser):
parser.addoption("--volatility", action="store", default=None,
parser.addoption(
"--volatility",
action="store",
default=None,
required=True,
help="path to the volatility script")
help="path to the volatility script",
)
parser.addoption("--python", action="store", default="python3",
help="The name of the interpreter to use when running the volatility script")
parser.addoption(
"--python",
action="store",
default="python3",
help="The name of the interpreter to use when running the volatility script",
)
parser.addoption("--image", action="append", default=[],
help="path to an image to test")
parser.addoption(
"--image", action="append", default=[], help="path to an image to test"
)
parser.addoption(
"--image-dir",
action="append",
default=[],
help="path to a directory containing images to test",
)
parser.addoption("--image-dir", action="append", default=[],
help="path to a directory containing images to test")
def pytest_generate_tests(metafunc):
"""Parameterize tests based on image names"""
images = metafunc.config.getoption('image')
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 = metafunc.config.getoption("image")
for image_dir in metafunc.config.getoption("image_dir"):
images = images + [
os.path.join(image_dir, dir) for dir in os.listdir(image_dir)
]
# tests with "image" parameter are run against images
if 'image' in metafunc.fixturenames:
metafunc.parametrize("image",
images,
ids=[os.path.basename(image) for image in images])
if "image" in metafunc.fixturenames:
metafunc.parametrize(
"image", images, ids=[os.path.basename(image) for image in images]
)
# Fixtures
@pytest.fixture
def volatility(request):
return request.config.getoption("--volatility")
@pytest.fixture
def python(request):
return request.config.getoption("--python")
return request.config.getoption("--python")
+120 -28
View File
@@ -18,6 +18,7 @@ import json
# HELPER FUNCTIONS
#
def runvol(args, volatility, python):
volpy = volatility
python_cmd = python
@@ -35,22 +36,29 @@ def runvol(args, volatility, python):
return p.returncode, stdout, stderr
def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]):
args = globalargs + [
"--single-location",
img,
"-q",
plugin,
] + pluginargs
args = (
globalargs
+ [
"--single-location",
img,
"-q",
plugin,
]
+ pluginargs
)
return runvol(args, volatility, python)
#
# TESTS
#
# WINDOWS
def test_windows_pslist(image, volatility, python):
rc, out, err = runvol_plugin("windows.pslist.PsList", image, volatility, python)
out = out.lower()
@@ -61,12 +69,14 @@ def test_windows_pslist(image, volatility, python):
assert rc == 0
rc, out, err = runvol_plugin(
"windows.pslist.PsList", image, volatility, python, pluginargs=["--pid", "4"])
"windows.pslist.PsList", image, volatility, python, pluginargs=["--pid", "4"]
)
out = out.lower()
assert out.find(b"system") != -1
assert out.count(b"\n") < 10
assert rc == 0
def test_windows_psscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.psscan.PsScan", image, volatility, python)
out = out.lower()
@@ -76,38 +86,46 @@ def test_windows_psscan(image, volatility, python):
assert out.count(b"\n") > 10
assert rc == 0
def test_windows_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)
out = out.lower()
assert out.count(b"\n") > 10
assert rc == 0
def test_windows_hivelist(image, volatility, python):
rc, out, err = runvol_plugin("windows.registry.hivelist.HiveList", image, volatility, python)
rc, out, err = runvol_plugin(
"windows.registry.hivelist.HiveList", image, volatility, python
)
out = out.lower()
not_xp = out.find(b"\\systemroot\\system32\\config\\software")
if not_xp == -1:
assert out.find(b"\\device\\harddiskvolume1\\windows\\system32\\config\\software") != -1
assert (
out.find(b"\\device\\harddiskvolume1\\windows\\system32\\config\\software")
!= -1
)
assert out.count(b"\n") > 10
assert rc == 0
def test_windows_dumpfiles(image, volatility, python):
json_file = open('./test/known_files.json')
known_files = json.load(json_file)
with open("./test/known_files.json") as json_file:
known_files = json.load(json_file)
failed_chksms = 0
if sys.platform == 'win32':
if sys.platform == "win32":
file_name = ntpath.basename(image)
else:
file_name = os.path.basename(image)
@@ -117,11 +135,21 @@ def test_windows_dumpfiles(image, volatility, python):
path = tempfile.mkdtemp()
rc, out, err = runvol_plugin("windows.dumpfiles.DumpFiles", image, volatility, python, globalargs=["-o", path], pluginargs=["--virtaddr", addr])
rc, out, err = runvol_plugin(
"windows.dumpfiles.DumpFiles",
image,
volatility,
python,
globalargs=["-o", path],
pluginargs=["--virtaddr", addr],
)
for file in os.listdir(path):
with open(os.path.join(path, file), "rb") as fp:
if hashlib.md5(fp.read()).hexdigest() not in known_files["windows_dumpfiles"][file_name][addr]:
if (
hashlib.md5(fp.read()).hexdigest()
not in known_files["windows_dumpfiles"][file_name][addr]
):
failed_chksms += 1
shutil.rmtree(path)
@@ -135,16 +163,24 @@ def test_windows_dumpfiles(image, volatility, python):
print("Key Error raised on " + str(e))
assert False
def test_windows_handles(image, volatility, python):
rc, out, err = runvol_plugin(
"windows.handles.Handles", image, volatility, python, pluginargs=["--pid", "4"])
"windows.handles.Handles", image, volatility, python, pluginargs=["--pid", "4"]
)
assert out.find(b"System Pid 4") != -1
assert out.find(b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS") != -1
assert (
out.find(
b"MACHINE\\SYSTEM\\CONTROLSET001\\CONTROL\\SESSION MANAGER\\MEMORY MANAGEMENT\\PREFETCHPARAMETERS"
)
!= -1
)
assert out.find(b"MACHINE\\SYSTEM\\SETUP") != -1
assert out.count(b"\n") > 500
assert rc == 0
def test_windows_svcscan(image, volatility, python):
rc, out, err = runvol_plugin("windows.svcscan.SvcScan", image, volatility, python)
@@ -152,9 +188,11 @@ def test_windows_svcscan(image, volatility, python):
assert out.count(b"\n") > 250
assert rc == 0
def test_windows_privileges(image, volatility, python):
rc, out, err = runvol_plugin(
"windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"])
"windows.privileges.Privs", image, volatility, python, pluginargs=["--pid", "4"]
)
assert out.find(b"SeCreateTokenPrivilege") != -1
assert out.find(b"SeCreateGlobalPrivilege") != -1
@@ -162,9 +200,11 @@ def test_windows_privileges(image, volatility, python):
assert out.count(b"\n") > 20
assert rc == 0
def test_windows_getsids(image, volatility, python):
rc, out, err = runvol_plugin(
"windows.getsids.GetSIDs", image, volatility, python, pluginargs=["--pid", "4"])
"windows.getsids.GetSIDs", image, volatility, python, pluginargs=["--pid", "4"]
)
assert out.find(b"Local System") != -1
assert out.find(b"Administrators") != -1
@@ -172,6 +212,7 @@ def test_windows_getsids(image, volatility, python):
assert out.find(b"Authenticated Users") != -1
assert rc == 0
def test_windows_envars(image, volatility, python):
rc, out, err = runvol_plugin("windows.envars.Envars", image, volatility, python)
@@ -183,8 +224,11 @@ def test_windows_envars(image, volatility, python):
assert out.count(b"\n") > 500
assert rc == 0
def test_windows_callbacks(image, volatility, python):
rc, out, err = runvol_plugin("windows.callbacks.Callbacks", image, volatility, python)
rc, out, err = runvol_plugin(
"windows.callbacks.Callbacks", image, volatility, python
)
assert out.find(b"PspCreateProcessNotifyRoutine") != -1
assert out.find(b"KeBugCheckCallbackListHead") != -1
@@ -192,8 +236,22 @@ def test_windows_callbacks(image, volatility, python):
assert out.count(b"KeBugCheckReasonCallbackListHead ") > 5
assert rc == 0
def test_windows_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
assert out.find(b"Vadl") != -1
assert out.find(b"VadF") != -1
assert out.find(b"0x0") != -1
assert rc == 0
def test_windows_devicetree(image, volatility, python):
rc, out, err = runvol_plugin("windows.devicetree.DeviceTree", image, volatility, python)
rc, out, err = runvol_plugin(
"windows.devicetree.DeviceTree", image, volatility, python
)
assert out.find(b"DEV") != -1
assert out.find(b"DRV") != -1
@@ -203,17 +261,20 @@ def test_windows_devicetree(image, volatility, python):
assert out.find(b"FILE_DEVICE_DISK_FILE_SYSTEM") != -1
assert rc == 0
# LINUX
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"init") != -1) or (out.find(b"systemd") != -1)
assert out.find(b"watchdog") != -1
assert out.count(b"\n") > 10
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()
@@ -222,8 +283,11 @@ def test_linux_check_idt(image, volatility, python):
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)
rc, out, err = runvol_plugin(
"linux.check_syscall.Check_syscall", image, volatility, python
)
out = out.lower()
assert out.find(b"sys_close") != -1
@@ -231,6 +295,7 @@ def test_linux_check_syscall(image, volatility, python):
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()
@@ -238,6 +303,7 @@ def test_linux_lsmod(image, volatility, python):
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()
@@ -246,6 +312,7 @@ def test_linux_lsof(image, volatility, python):
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()
@@ -254,6 +321,7 @@ def test_linux_proc_maps(image, volatility, python):
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()
@@ -262,18 +330,23 @@ def test_linux_tty_check(image, volatility, python):
assert out.count(b"\n") >= 5
assert rc == 0
# MAC
def test_mac_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))
assert (out.find(b"kernel_task") != -1) or (out.find(b"launchd") != -1)
assert out.count(b"\n") > 10
assert rc == 0
def test_mac_check_syscall(image, volatility, python):
rc, out, err = runvol_plugin("mac.check_syscall.Check_syscall", image, volatility, python)
rc, out, err = runvol_plugin(
"mac.check_syscall.Check_syscall", image, volatility, python
)
out = out.lower()
assert out.find(b"chmod") != -1
@@ -282,22 +355,29 @@ def test_mac_check_syscall(image, volatility, python):
assert out.count(b"\n") > 100
assert rc == 0
def test_mac_check_sysctl(image, volatility, python):
rc, out, err = runvol_plugin("mac.check_sysctl.Check_sysctl", image, volatility, python)
rc, out, err = runvol_plugin(
"mac.check_sysctl.Check_sysctl", image, volatility, python
)
out = out.lower()
assert out.find(b"__kernel__") != -1
assert out.count(b"\n") > 250
assert rc == 0
def test_mac_check_trap_table(image, volatility, python):
rc, out, err = runvol_plugin("mac.check_trap_table.Check_trap_table", image, volatility, python)
rc, out, err = runvol_plugin(
"mac.check_trap_table.Check_trap_table", image, volatility, python
)
out = out.lower()
assert out.count(b"kern_invalid") >= 10
assert out.count(b"\n") > 50
assert rc == 0
def test_mac_ifconfig(image, volatility, python):
rc, out, err = runvol_plugin("mac.ifconfig.Ifconfig", image, volatility, python)
out = out.lower()
@@ -307,6 +387,7 @@ def test_mac_ifconfig(image, volatility, python):
assert out.count(b"\n") > 9
assert rc == 0
def test_mac_lsmod(image, volatility, python):
rc, out, err = runvol_plugin("mac.lsmod.Lsmod", image, volatility, python)
out = out.lower()
@@ -315,6 +396,7 @@ def test_mac_lsmod(image, volatility, python):
assert out.count(b"\n") > 10
assert rc == 0
def test_mac_lsof(image, volatility, python):
rc, out, err = runvol_plugin("mac.lsof.Lsof", image, volatility, python)
out = out.lower()
@@ -322,6 +404,7 @@ def test_mac_lsof(image, volatility, python):
assert out.count(b"\n") > 50
assert rc == 0
def test_mac_malfind(image, volatility, python):
rc, out, err = runvol_plugin("mac.malfind.Malfind", image, volatility, python)
out = out.lower()
@@ -329,6 +412,7 @@ def test_mac_malfind(image, volatility, python):
assert out.count(b"\n") > 20
assert rc == 0
def test_mac_mount(image, volatility, python):
rc, out, err = runvol_plugin("mac.mount.Mount", image, volatility, python)
out = out.lower()
@@ -337,6 +421,7 @@ def test_mac_mount(image, volatility, python):
assert out.count(b"\n") > 7
assert rc == 0
def test_mac_netstat(image, volatility, python):
rc, out, err = runvol_plugin("mac.netstat.Netstat", image, volatility, python)
@@ -346,6 +431,7 @@ def test_mac_netstat(image, volatility, python):
assert out.count(b"\n") > 10
assert rc == 0
def test_mac_proc_maps(image, volatility, python):
rc, out, err = runvol_plugin("mac.proc_maps.Maps", image, volatility, python)
out = out.lower()
@@ -354,6 +440,7 @@ def test_mac_proc_maps(image, volatility, python):
assert out.count(b"\n") > 100
assert rc == 0
def test_mac_psaux(image, volatility, python):
rc, out, err = runvol_plugin("mac.psaux.Psaux", image, volatility, python)
out = out.lower()
@@ -362,13 +449,17 @@ def test_mac_psaux(image, volatility, python):
assert out.count(b"\n") > 50
assert rc == 0
def test_mac_socket_filters(image, volatility, python):
rc, out, err = runvol_plugin("mac.socket_filters.Socket_filters", image, volatility, python)
rc, out, err = runvol_plugin(
"mac.socket_filters.Socket_filters", image, volatility, python
)
out = out.lower()
assert out.count(b"\n") > 9
assert rc == 0
def test_mac_timers(image, volatility, python):
rc, out, err = runvol_plugin("mac.timers.Timers", image, volatility, python)
out = out.lower()
@@ -376,6 +467,7 @@ def test_mac_timers(image, volatility, python):
assert out.count(b"\n") > 6
assert rc == 0
def test_mac_trustedbsd(image, volatility, python):
rc, out, err = runvol_plugin("mac.trustedbsd.Trustedbsd", image, volatility, python)
out = out.lower()
+1 -1
View File
@@ -6,5 +6,5 @@
import volatility3.cli
if __name__ == '__main__':
if __name__ == "__main__":
volatility3.cli.main()
+4 -2
View File
@@ -32,14 +32,16 @@ class WarningFindSpec(abc.MetaPathFinder):
used."""
@staticmethod
def find_spec(fullname: str, path: Optional[List[str]], target: None = None, **kwargs) -> None:
def find_spec(
fullname: str, path: Optional[List[str]], target: None = None, **kwargs
) -> None:
"""Mock find_spec method that just checks the name, this must go
first."""
if fullname.startswith("volatility3.framework.plugins."):
warning = "Please do not use the volatility3.framework.plugins namespace directly, only use volatility3.plugins"
# Pyinstaller uses walk_packages/_collect_submodules to import, but needs to read the modules to figure out dependencies
# As such, we only print the warning when directly imported rather than from within walk_packages/_collect_submodules
if inspect.stack()[-2].function in ['walk_packages', '_collect_submodules']:
if inspect.stack()[-2].function in ["walk_packages", "_collect_submodules"]:
raise Warning(warning)
+340 -179
View File
@@ -26,7 +26,15 @@ import volatility3.plugins
import volatility3.symbols
from volatility3 import framework
from volatility3.cli import text_renderer, volargparse
from volatility3.framework import automagic, configuration, constants, contexts, exceptions, interfaces, plugins
from volatility3.framework import (
automagic,
configuration,
constants,
contexts,
exceptions,
interfaces,
plugins,
)
from volatility3.framework.automagic import stacker
from volatility3.framework.configuration import requirements
@@ -36,7 +44,7 @@ rootlog = logging.getLogger()
vollog = logging.getLogger(__name__)
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
# Trim the console down by default
console.setFormatter(formatter)
@@ -59,7 +67,7 @@ class PrintedProgress(object):
message = f"\rProgress: {round(progress, 2): 7.2f}\t\t{description or ''}"
message_len = len(message)
self._max_message_len = max([self._max_message_len, message_len])
sys.stderr.write(message + (' ' * (self._max_message_len - message_len)) + '\r')
sys.stderr.write(message + (" " * (self._max_message_len - message_len)) + "\r")
class MuteProgress(PrintedProgress):
@@ -72,7 +80,7 @@ class MuteProgress(PrintedProgress):
class CommandLine:
"""Constructs a command-line interface object for users to run plugins."""
CLI_NAME = 'volatility'
CLI_NAME = "volatility"
def __init__(self):
self.setup_logging()
@@ -90,93 +98,142 @@ class CommandLine:
volatility3.framework.require_interface_version(2, 0, 0)
renderers = dict([(x.name.lower(), x) for x in framework.class_subclasses(text_renderer.CLIRenderer)])
renderers = dict(
[
(x.name.lower(), x)
for x in framework.class_subclasses(text_renderer.CLIRenderer)
]
)
parser = volargparse.HelpfulArgParser(add_help = False,
prog = self.CLI_NAME,
description = "An open-source memory forensics framework")
parser = volargparse.HelpfulArgParser(
add_help=False,
prog=self.CLI_NAME,
description="An open-source memory forensics framework",
)
parser.add_argument(
"-h",
"--help",
action = "help",
default = argparse.SUPPRESS,
help = "Show this help message and exit, for specific plugin options use '{} <pluginname> --help'".format(
parser.prog))
parser.add_argument("-c",
"--config",
help = "Load the configuration from a json file",
default = None,
type = str)
parser.add_argument("--parallelism",
help = "Enables parallelism (defaults to off if no argument given)",
nargs = '?',
choices = ['processes', 'threads', 'off'],
const = 'processes',
default = None,
type = str)
parser.add_argument("-e",
"--extend",
help = "Extend the configuration with a new (or changed) setting",
default = None,
action = 'append')
parser.add_argument("-p",
"--plugin-dirs",
help = "Semi-colon separated list of paths to find plugins",
default = "",
type = str)
parser.add_argument("-s",
"--symbol-dirs",
help = "Semi-colon separated list of paths to find symbols",
default = "",
type = str)
parser.add_argument("-v", "--verbosity", help = "Increase output verbosity", default = 0, action = "count")
parser.add_argument("-l",
"--log",
help = "Log output to a file as well as the console",
default = None,
type = str)
parser.add_argument("-o",
"--output-dir",
help = "Directory in which to output any generated files",
default = os.getcwd(),
type = str)
parser.add_argument("-q", "--quiet", help = "Remove progress feedback", default = False, action = 'store_true')
parser.add_argument("-r",
"--renderer",
metavar = 'RENDERER',
help = f"Determines how to render the output ({', '.join(list(renderers))})",
default = "quick",
choices = list(renderers))
parser.add_argument("-f",
"--file",
metavar = 'FILE',
default = None,
type = str,
help = "Shorthand for --single-location=file:// if single-location is not defined")
parser.add_argument("--write-config",
help = "Write configuration JSON file out to config.json",
default = False,
action = 'store_true')
parser.add_argument("--save-config",
help = "Save configuration JSON file to a file",
default = None,
type = str)
parser.add_argument("--clear-cache",
help = "Clears out all short-term cached items",
default = False,
action = 'store_true')
parser.add_argument("--cache-path",
help = f"Change the default path ({constants.CACHE_PATH}) used to store the cache",
default = constants.CACHE_PATH,
type = str)
parser.add_argument("--offline",
help = "Do not search online for additional JSON files",
default = False,
action = 'store_true')
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit, for specific plugin options use '{} <pluginname> --help'".format(
parser.prog
),
)
parser.add_argument(
"-c",
"--config",
help="Load the configuration from a json file",
default=None,
type=str,
)
parser.add_argument(
"--parallelism",
help="Enables parallelism (defaults to off if no argument given)",
nargs="?",
choices=["processes", "threads", "off"],
const="processes",
default=None,
type=str,
)
parser.add_argument(
"-e",
"--extend",
help="Extend the configuration with a new (or changed) setting",
default=None,
action="append",
)
parser.add_argument(
"-p",
"--plugin-dirs",
help="Semi-colon separated list of paths to find plugins",
default="",
type=str,
)
parser.add_argument(
"-s",
"--symbol-dirs",
help="Semi-colon separated list of paths to find symbols",
default="",
type=str,
)
parser.add_argument(
"-v",
"--verbosity",
help="Increase output verbosity",
default=0,
action="count",
)
parser.add_argument(
"-l",
"--log",
help="Log output to a file as well as the console",
default=None,
type=str,
)
parser.add_argument(
"-o",
"--output-dir",
help="Directory in which to output any generated files",
default=os.getcwd(),
type=str,
)
parser.add_argument(
"-q",
"--quiet",
help="Remove progress feedback",
default=False,
action="store_true",
)
parser.add_argument(
"-r",
"--renderer",
metavar="RENDERER",
help=f"Determines how to render the output ({', '.join(list(renderers))})",
default="quick",
choices=list(renderers),
)
parser.add_argument(
"-f",
"--file",
metavar="FILE",
default=None,
type=str,
help="Shorthand for --single-location=file:// if single-location is not defined",
)
parser.add_argument(
"--write-config",
help="Write configuration JSON file out to config.json",
default=False,
action="store_true",
)
parser.add_argument(
"--save-config",
help="Save configuration JSON file to a file",
default=None,
type=str,
)
parser.add_argument(
"--clear-cache",
help="Clears out all short-term cached items",
default=False,
action="store_true",
)
parser.add_argument(
"--cache-path",
help=f"Change the default path ({constants.CACHE_PATH}) used to store the cache",
default=constants.CACHE_PATH,
type=str,
)
parser.add_argument(
"--offline",
help="Do not search online for additional JSON files",
default=False,
action="store_true",
)
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
# processed the plugin choice or had the plugin subparser added.
known_args = [arg for arg in sys.argv if arg != '--help' and arg != '-h']
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
partial_args, _ = parser.parse_known_args(known_args)
banner_output = sys.stdout
@@ -185,12 +242,14 @@ class CommandLine:
banner_output.write(f"Volatility 3 Framework {constants.PACKAGE_VERSION}\n")
if partial_args.plugin_dirs:
volatility3.plugins.__path__ = [os.path.abspath(p)
for p in partial_args.plugin_dirs.split(";")] + constants.PLUGINS_PATH
volatility3.plugins.__path__ = [
os.path.abspath(p) for p in partial_args.plugin_dirs.split(";")
] + constants.PLUGINS_PATH
if partial_args.symbol_dirs:
volatility3.symbols.__path__ = [os.path.abspath(p)
for p in partial_args.symbol_dirs.split(";")] + constants.SYMBOL_BASEPATHS
volatility3.symbols.__path__ = [
os.path.abspath(p) for p in partial_args.symbol_dirs.split(";")
] + constants.SYMBOL_BASEPATHS
if partial_args.cache_path:
constants.CACHE_PATH = partial_args.cache_path
@@ -198,8 +257,10 @@ class CommandLine:
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
file_logger.setLevel(1)
file_formatter = logging.Formatter(datefmt = '%y-%m-%d %H:%M:%S',
fmt = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
rootlog.addHandler(file_logger)
vollog.info("Logging started")
@@ -214,9 +275,9 @@ class CommandLine:
vollog.info(f"Volatility symbols path: {volatility3.symbols.__path__}")
# Set the PARALLELISM
if partial_args.parallelism == 'processes':
if partial_args.parallelism == "processes":
constants.PARALLELISM = constants.Parallelism.Multiprocessing
elif partial_args.parallelism == 'threads':
elif partial_args.parallelism == "threads":
constants.PARALLELISM = constants.Parallelism.Threading
else:
constants.PARALLELISM = constants.Parallelism.Off
@@ -229,11 +290,14 @@ class CommandLine:
# Do the initialization
ctx = contexts.Context() # Construct a blank context
failures = framework.import_files(volatility3.plugins,
True) # Will not log as console's default level is WARNING
failures = framework.import_files(
volatility3.plugins, True
) # Will not log as console's default level is WARNING
if failures:
parser.epilog = "The following plugins could not be loaded (use -vv to see why): " + \
", ".join(sorted(failures))
parser.epilog = (
"The following plugins could not be loaded (use -vv to see why): "
+ ", ".join(sorted(failures))
)
vollog.info(parser.epilog)
automagics = automagic.available(ctx)
@@ -248,13 +312,18 @@ class CommandLine:
if isinstance(amagic, interfaces.configuration.ConfigurableInterface):
self.populate_requirements_argparse(parser, amagic.__class__)
subparser = parser.add_subparsers(title = "Plugins",
dest = "plugin",
description = "For plugin specific options, run '{} <plugin> --help'".format(
self.CLI_NAME),
action = volargparse.HelpfulSubparserAction)
subparser = parser.add_subparsers(
title="Plugins",
dest="plugin",
description="For plugin specific options, run '{} <plugin> --help'".format(
self.CLI_NAME
),
action=volargparse.HelpfulSubparserAction,
)
for plugin in sorted(plugin_list):
plugin_parser = subparser.add_parser(plugin, help = plugin_list[plugin].__doc__)
plugin_parser = subparser.add_parser(
plugin, help=plugin_list[plugin].__doc__
)
self.populate_requirements_argparse(plugin_parser, plugin_list[plugin])
###
@@ -267,12 +336,16 @@ class CommandLine:
if args.plugin is None:
parser.error("Please select a plugin to run")
vollog.log(constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}")
vollog.log(
constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}"
)
plugin = plugin_list[args.plugin]
chosen_configurables_list[args.plugin] = plugin
base_config_path = "plugins"
plugin_config_path = interfaces.configuration.path_join(base_config_path, plugin.__name__)
plugin_config_path = interfaces.configuration.path_join(
base_config_path, plugin.__name__
)
# Special case the -f argument because people use is so frequently
# It has to go here so it can be overridden by single-location if it's defined
@@ -281,7 +354,7 @@ class CommandLine:
if args.file:
try:
single_location = self.location_from_file(args.file)
ctx.config['automagic.LayerStacker.single_location'] = single_location
ctx.config["automagic.LayerStacker.single_location"] = single_location
except ValueError as excp:
parser.error(str(excp))
@@ -289,26 +362,37 @@ class CommandLine:
if args.config:
with open(args.config, "r") as f:
json_val = json.load(f)
ctx.config.splice(plugin_config_path, interfaces.configuration.HierarchicalDict(json_val))
ctx.config.splice(
plugin_config_path,
interfaces.configuration.HierarchicalDict(json_val),
)
# It should be up to the UI to determine which automagics to run, so this is before BACK TO THE FRAMEWORK
automagics = automagic.choose_automagic(automagics, plugin)
for amagic in automagics:
chosen_configurables_list[amagic.__class__.__name__] = amagic
if ctx.config.get('automagic.LayerStacker.stackers', None) is None:
ctx.config['automagic.LayerStacker.stackers'] = stacker.choose_os_stackers(plugin)
if ctx.config.get("automagic.LayerStacker.stackers", None) is None:
ctx.config["automagic.LayerStacker.stackers"] = stacker.choose_os_stackers(
plugin
)
self.output_dir = args.output_dir
if not os.path.exists(self.output_dir):
parser.error(f"The output directory specified does not exist: {self.output_dir}")
parser.error(
f"The output directory specified does not exist: {self.output_dir}"
)
self.populate_config(ctx, chosen_configurables_list, args, plugin_config_path)
if args.extend:
for extension in args.extend:
if '=' not in extension:
raise ValueError("Invalid extension (extensions must be of the format \"conf.path.value='value'\")")
address, value = extension[:extension.find('=')], json.loads(extension[extension.find('=') + 1:])
if "=" not in extension:
raise ValueError(
"Invalid extension (extensions must be of the format \"conf.path.value='value'\")"
)
address, value = extension[: extension.find("=")], json.loads(
extension[extension.find("=") + 1 :]
)
ctx.config[address] = value
###
@@ -320,22 +404,40 @@ class CommandLine:
if args.quiet:
progress_callback = MuteProgress()
constructed = plugins.construct_plugin(ctx, automagics, plugin, base_config_path, progress_callback,
self.file_handler_class_factory())
constructed = plugins.construct_plugin(
ctx,
automagics,
plugin,
base_config_path,
progress_callback,
self.file_handler_class_factory(),
)
if args.write_config:
vollog.warning('Use of --write-config has been deprecated, replaced by --save-config <filename>')
args.save_config = 'config.json'
vollog.warning(
"Use of --write-config has been deprecated, replaced by --save-config <filename>"
)
args.save_config = "config.json"
if args.save_config:
vollog.debug("Writing out configuration data to {args.save_config}")
if os.path.exists(os.path.abspath(args.save_config)):
parser.error(f"Cannot write configuration: file {args.save_config} already exists")
parser.error(
f"Cannot write configuration: file {args.save_config} already exists"
)
with open(args.save_config, "w") as f:
json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2)
json.dump(
dict(constructed.build_configuration()),
f,
sort_keys=True,
indent=2,
)
f.write("\n")
except exceptions.UnsatisfiedException as excp:
self.process_unsatisfied_exceptions(excp)
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
parser.exit(
1,
f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n",
)
try:
# Construct and run the plugin
@@ -355,10 +457,12 @@ class CommandLine:
The URL for the location of the file
"""
# We want to work in URLs, but we need to accept absolute and relative files (including on windows)
single_location = parse.urlparse(filename, '')
if single_location.scheme == '' or len(single_location.scheme) == 1:
single_location = parse.urlparse(parse.urljoin('file:', request.pathname2url(os.path.abspath(filename))))
if single_location.scheme == 'file':
single_location = parse.urlparse(filename, "")
if single_location.scheme == "" or len(single_location.scheme) == 1:
single_location = parse.urlparse(
parse.urljoin("file:", request.pathname2url(os.path.abspath(filename)))
)
if single_location.scheme == "file":
if not os.path.exists(request.url2pathname(single_location.path)):
filename = request.url2pathname(single_location.path)
if not filename:
@@ -374,7 +478,7 @@ class CommandLine:
sys.stderr.flush()
# Log the full exception at a high level for easy access
fulltrace = traceback.TracebackException.from_exception(excp).format(chain = True)
fulltrace = traceback.TracebackException.from_exception(excp).format(chain=True)
vollog.debug("".join(fulltrace))
if isinstance(excp, exceptions.InvalidAddressException):
@@ -383,22 +487,24 @@ class CommandLine:
detail = f"Swap error {hex(excp.invalid_address)} in layer {excp.layer_name} ({excp})"
caused_by = [
"No suitable swap file having been provided (locate and provide the correct swap file)",
"An intentionally invalid page (operating system protection)"
"An intentionally invalid page (operating system protection)",
]
elif isinstance(excp, exceptions.PagedInvalidAddressException):
detail = f"Page error {hex(excp.invalid_address)} in layer {excp.layer_name} ({excp})"
caused_by = [
"Memory smear during acquisition (try re-acquiring if possible)",
"An intentionally invalid page lookup (operating system protection)",
"A bug in the plugin/volatility3 (re-run with -vvv and file a bug)"
"A bug in the plugin/volatility3 (re-run with -vvv and file a bug)",
]
else:
detail = f"{hex(excp.invalid_address)} in layer {excp.layer_name} ({excp})"
detail = (
f"{hex(excp.invalid_address)} in layer {excp.layer_name} ({excp})"
)
caused_by = [
"The base memory file being incomplete (try re-acquiring if possible)",
"Memory smear during acquisition (try re-acquiring if possible)",
"An intentionally invalid page lookup (operating system protection)",
"A bug in the plugin/volatility3 (re-run with -vvv and file a bug)"
"A bug in the plugin/volatility3 (re-run with -vvv and file a bug)",
]
elif isinstance(excp, exceptions.SymbolError):
general = "Volatility experienced a symbol-related issue:"
@@ -412,22 +518,28 @@ class CommandLine:
general = "Volatility experienced an issue related to a symbol table:"
detail = f"{excp}"
caused_by = [
"An invalid symbol table", "A plugin requesting a bad symbol",
"A plugin requesting a symbol from the wrong table"
"An invalid symbol table",
"A plugin requesting a bad symbol",
"A plugin requesting a symbol from the wrong table",
]
elif isinstance(excp, exceptions.LayerException):
general = f"Volatility experienced a layer-related issue: {excp.layer_name}"
detail = f"{excp}"
caused_by = ["A faulty layer implementation (re-run with -vvv and file a bug)"]
caused_by = [
"A faulty layer implementation (re-run with -vvv and file a bug)"
]
elif isinstance(excp, exceptions.MissingModuleException):
general = f"Volatility could not import a necessary module: {excp.module}"
detail = f"{excp}"
caused_by = ["A required python module is not installed (install the module and re-run)"]
caused_by = [
"A required python module is not installed (install the module and re-run)"
]
else:
general = "Volatility encountered an unexpected situation."
detail = ""
caused_by = [
"Please re-run using with -vvv and file a bug with the output", f"at {constants.BUG_URL}"
"Please re-run using with -vvv and file a bug with the output",
f"at {constants.BUG_URL}",
]
# Code that actually renders the exception
@@ -447,27 +559,43 @@ class CommandLine:
symbols_failed = False
for config_path in excp.unsatisfied:
translation_failed = translation_failed or isinstance(
excp.unsatisfied[config_path], configuration.requirements.TranslationLayerRequirement)
symbols_failed = symbols_failed or isinstance(excp.unsatisfied[config_path],
configuration.requirements.SymbolTableRequirement)
excp.unsatisfied[config_path],
configuration.requirements.TranslationLayerRequirement,
)
symbols_failed = symbols_failed or isinstance(
excp.unsatisfied[config_path],
configuration.requirements.SymbolTableRequirement,
)
print(f"Unsatisfied requirement {config_path}: {excp.unsatisfied[config_path].description}")
print(
f"Unsatisfied requirement {config_path}: {excp.unsatisfied[config_path].description}"
)
if translation_failed:
print("\nA translation layer requirement was not fulfilled. Please verify that:\n"
"\tA file was provided to create this layer (by -f, --single-location or by config)\n"
"\tThe file exists and is readable\n"
"\tThe file is a valid memory image and was acquired cleanly")
print(
"\nA translation layer requirement was not fulfilled. Please verify that:\n"
"\tA file was provided to create this layer (by -f, --single-location or by config)\n"
"\tThe file exists and is readable\n"
"\tThe file is a valid memory image and was acquired cleanly"
)
if symbols_failed:
print("\nA symbol table requirement was not fulfilled. Please verify that:\n"
"\tThe associated translation layer requirement was fulfilled\n"
"\tYou have the correct symbol file for the requirement\n"
"\tThe symbol file is under the correct directory or zip file\n"
"\tThe symbol file is named appropriately or contains the correct banner\n")
print(
"\nA symbol table requirement was not fulfilled. Please verify that:\n"
"\tThe associated translation layer requirement was fulfilled\n"
"\tYou have the correct symbol file for the requirement\n"
"\tThe symbol file is under the correct directory or zip file\n"
"\tThe symbol file is named appropriately or contains the correct banner\n"
)
def populate_config(self, context: interfaces.context.ContextInterface,
configurables_list: Dict[str, Type[interfaces.configuration.ConfigurableInterface]],
args: argparse.Namespace, plugin_config_path: str) -> None:
def populate_config(
self,
context: interfaces.context.ContextInterface,
configurables_list: Dict[
str, Type[interfaces.configuration.ConfigurableInterface]
],
args: argparse.Namespace,
plugin_config_path: str,
) -> None:
"""Populate the context config based on the returned args.
We have already determined these elements must be descended from ConfigurableInterface
@@ -489,34 +617,42 @@ class CommandLine:
if not scheme or len(scheme) <= 1:
if not os.path.exists(value):
raise FileNotFoundError(
f"Non-existent file {value} passed to URIRequirement")
f"Non-existent file {value} passed to URIRequirement"
)
value = f"file://{request.pathname2url(os.path.abspath(value))}"
if isinstance(requirement, requirements.ListRequirement):
if not isinstance(value, list):
raise TypeError("Configuration for ListRequirement was not a list: {}".format(
requirement.name))
raise TypeError(
"Configuration for ListRequirement was not a list: {}".format(
requirement.name
)
)
value = [requirement.element_type(x) for x in value]
if not inspect.isclass(configurables_list[configurable]):
config_path = configurables_list[configurable].config_path
else:
# We must be the plugin, so name it appropriately:
config_path = plugin_config_path
extended_path = interfaces.configuration.path_join(config_path, requirement.name)
extended_path = interfaces.configuration.path_join(
config_path, requirement.name
)
context.config[extended_path] = value
def file_handler_class_factory(self, direct = True):
def file_handler_class_factory(self, direct=True):
output_dir = self.output_dir
class CLIFileHandler(interfaces.plugins.FileHandlerInterface):
def _get_final_filename(self):
"""Gets the final filename"""
if output_dir is None:
raise TypeError("Output directory is not a string")
os.makedirs(output_dir, exist_ok = True)
os.makedirs(output_dir, exist_ok=True)
pref_name_array = self.preferred_filename.split('.')
filename, extension = os.path.join(output_dir, '.'.join(pref_name_array[:-1])), pref_name_array[-1]
pref_name_array = self.preferred_filename.split(".")
filename, extension = (
os.path.join(output_dir, ".".join(pref_name_array[:-1])),
pref_name_array[-1],
)
output_filename = f"{filename}.{extension}"
counter = 1
@@ -526,7 +662,6 @@ class CommandLine:
return output_filename
class CLIMemFileHandler(io.BytesIO, CLIFileHandler):
def __init__(self, filename: str):
io.BytesIO.__init__(self)
CLIFileHandler.__init__(self, filename)
@@ -543,18 +678,26 @@ class CommandLine:
with open(output_filename, "wb") as current_file:
current_file.write(self.read())
self._committed = True
vollog.log(logging.INFO, f"Saved stored plugin file: {output_filename}")
vollog.log(
logging.INFO, f"Saved stored plugin file: {output_filename}"
)
super().close()
class CLIDirectFileHandler(CLIFileHandler):
def __init__(self, filename: str):
fd, self._name = tempfile.mkstemp(suffix = '.vol3', prefix = 'tmp_', dir = output_dir)
self._file = io.open(fd, mode = 'w+b')
fd, self._name = tempfile.mkstemp(
suffix=".vol3", prefix="tmp_", dir=output_dir
)
self._file = io.open(fd, mode="w+b")
CLIFileHandler.__init__(self, filename)
for item in dir(self._file):
if not item.startswith('_') and item not in ('closed', 'close', 'mode', 'name'):
if not item.startswith("_") and item not in (
"closed",
"close",
"mode",
"name",
):
setattr(self, item, getattr(self._file, item))
def __getattr__(self, item):
@@ -587,8 +730,11 @@ class CommandLine:
else:
return CLIMemFileHandler
def populate_requirements_argparse(self, parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup],
configurable: Type[interfaces.configuration.ConfigurableInterface]):
def populate_requirements_argparse(
self,
parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup],
configurable: Type[interfaces.configuration.ConfigurableInterface],
):
"""Adds the plugin's simple requirements to the provided parser.
Args:
@@ -596,15 +742,22 @@ class CommandLine:
configurable: The plugin object to pull the requirements from
"""
if not issubclass(configurable, interfaces.configuration.ConfigurableInterface):
raise TypeError(f"Expected ConfigurableInterface type, not: {type(configurable)}")
raise TypeError(
f"Expected ConfigurableInterface type, not: {type(configurable)}"
)
# Construct an argparse group
for requirement in configurable.get_requirements():
additional: Dict[str, Any] = {}
if not isinstance(requirement, interfaces.configuration.RequirementInterface):
raise TypeError("Plugin contains requirements that are not RequirementInterfaces: {}".format(
configurable.__name__))
if not isinstance(
requirement, interfaces.configuration.RequirementInterface
):
raise TypeError(
"Plugin contains requirements that are not RequirementInterfaces: {}".format(
configurable.__name__
)
)
if isinstance(requirement, interfaces.configuration.SimpleTypeRequirement):
additional["type"] = requirement.instance_type
if isinstance(requirement, requirements.IntRequirement):
@@ -613,21 +766,29 @@ class CommandLine:
additional["action"] = "store_true"
if "type" in additional:
del additional["type"]
elif isinstance(requirement, volatility3.framework.configuration.requirements.ListRequirement):
elif isinstance(
requirement,
volatility3.framework.configuration.requirements.ListRequirement,
):
additional["type"] = requirement.element_type
nargs = '*' if requirement.optional else '+'
nargs = "*" if requirement.optional else "+"
additional["nargs"] = nargs
elif isinstance(requirement, volatility3.framework.configuration.requirements.ChoiceRequirement):
elif isinstance(
requirement,
volatility3.framework.configuration.requirements.ChoiceRequirement,
):
additional["type"] = str
additional["choices"] = requirement.choices
else:
continue
parser.add_argument("--" + requirement.name.replace('_', '-'),
help = requirement.description,
default = requirement.default,
dest = requirement.name,
required = not requirement.optional,
**additional)
parser.add_argument(
"--" + requirement.name.replace("_", "-"),
help=requirement.description,
default=requirement.default,
dest=requirement.name,
required=not requirement.optional,
**additional,
)
def main():
+114 -53
View File
@@ -44,9 +44,9 @@ def hex_bytes_as_text(value: bytes) -> str:
ascii.append(chr(byte) if 0x20 < byte <= 0x7E else ".")
if (count % 8) == 7:
output += "\n"
output += " ".join(hex[count - 7:count + 1])
output += " ".join(hex[count - 7 : count + 1])
output += "\t"
output += "".join(ascii[count - 7:count + 1])
output += "".join(ascii[count - 7 : count + 1])
count += 1
return output
@@ -58,10 +58,16 @@ def multitypedata_as_text(value: format_hints.MultiTypeData) -> str:
"""
if value.show_hex:
return hex_bytes_as_text(value)
string_representation = str(value, encoding = value.encoding, errors = 'replace')
if value.split_nulls and ((len(value) / 2 - 1) <= len(string_representation) <= (len(value) / 2)):
string_representation = str(value, encoding=value.encoding, errors="replace")
if value.split_nulls and (
(len(value) / 2 - 1) <= len(string_representation) <= (len(value) / 2)
):
return "\n".join(string_representation.split("\x00"))
if len(string_representation) - 1 <= len(string_representation.split("\x00")[0]) <= len(string_representation):
if (
len(string_representation) - 1
<= len(string_representation.split("\x00")[0])
<= len(string_representation)
):
return string_representation.split("\x00")[0]
return hex_bytes_as_text(value)
@@ -87,9 +93,11 @@ def quoted_optional(func: Callable) -> Callable:
return ""
if isinstance(x, format_hints.MultiTypeData) and x.converted_int:
return f"{result}"
if isinstance(x, int) and not isinstance(x, (format_hints.Hex, format_hints.Bin)):
if isinstance(x, int) and not isinstance(
x, (format_hints.Hex, format_hints.Bin)
):
return f"{result}"
return f"\"{result}\""
return f'"{result}"'
return wrapped
@@ -106,14 +114,16 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str:
if CAPSTONE_PRESENT:
disasm_types = {
'intel': capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32),
'intel64': capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64),
'arm': capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
'arm64': capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
"intel": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32),
"intel64": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64),
"arm": capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
"arm64": capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM),
}
output = ""
if disasm.architecture is not None:
for i in disasm_types[disasm.architecture].disasm(disasm.data, disasm.offset):
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}"
return output
return QuickTextRenderer._type_renderers[bytes](disasm.data)
@@ -121,6 +131,7 @@ def display_disassembly(disasm: interfaces.renderers.Disassembly) -> str:
class CLIRenderer(interfaces.renderers.Renderer):
"""Class to add specific requirements for CLI renderers."""
name = "unnamed"
structured_output = False
@@ -134,7 +145,7 @@ class QuickTextRenderer(CLIRenderer):
interfaces.renderers.Disassembly: optional(display_disassembly),
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}")
"default": optional(lambda x: f"{x}"),
}
name = "quick"
@@ -163,11 +174,16 @@ class QuickTextRenderer(CLIRenderer):
def visitor(node: interfaces.renderers.TreeNode, accumulator):
accumulator.write("\n")
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
accumulator.write("*" * max(0, node.path_depth - 1) + ("" if (node.path_depth <= 1) else " "))
accumulator.write(
"*" * max(0, node.path_depth - 1)
+ ("" if (node.path_depth <= 1) else " ")
)
line = []
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(column.type, self._type_renderers['default'])
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
line.append(renderer(node.values[column_index]))
accumulator.write("{}".format("\t".join(line)))
accumulator.flush()
@@ -176,13 +192,14 @@ class QuickTextRenderer(CLIRenderer):
if not grid.populated:
grid.populate(visitor, outfd)
else:
grid.visit(node = None, function = visitor, initial_accumulator = outfd)
grid.visit(node=None, function=visitor, initial_accumulator=outfd)
outfd.write("\n")
class NoneRenderer(CLIRenderer):
"""Outputs no results"""
name = "none"
def get_render_options(self):
@@ -202,7 +219,7 @@ class CSVRenderer(CLIRenderer):
interfaces.renderers.Disassembly: optional(display_disassembly),
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}")
"default": optional(lambda x: f"{x}"),
}
name = "csv"
@@ -219,28 +236,30 @@ class CSVRenderer(CLIRenderer):
"""
outfd = sys.stdout
header_list = ['TreeDepth']
header_list = ["TreeDepth"]
for column in grid.columns:
# Ignore the type because namedtuples don't realize they have accessible attributes
header_list.append(f"{column.name}")
writer = csv.DictWriter(outfd, header_list, lineterminator='\n')
writer = csv.DictWriter(outfd, header_list, lineterminator="\n")
writer.writeheader()
def visitor(node: interfaces.renderers.TreeNode, accumulator):
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
row = {'TreeDepth': str(max(0, node.path_depth - 1))}
row = {"TreeDepth": str(max(0, node.path_depth - 1))}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(column.type, self._type_renderers['default'])
row[f'{column.name}'] = renderer(node.values[column_index])
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
row[f"{column.name}"] = renderer(node.values[column_index])
accumulator.writerow(row)
return accumulator
if not grid.populated:
grid.populate(visitor, writer)
else:
grid.visit(node = None, function = visitor, initial_accumulator = writer)
grid.visit(node=None, function=visitor, initial_accumulator=writer)
outfd.write("\n")
@@ -270,23 +289,34 @@ class PrettyTextRenderer(CLIRenderer):
display_alignment = ">"
column_separator = " | "
tree_indent_column = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20))
max_column_widths = dict([(column.name, len(column.name)) for column in grid.columns])
tree_indent_column = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(20)
)
max_column_widths = dict(
[(column.name, len(column.name)) for column in grid.columns]
)
def visitor(
node: interfaces.renderers.TreeNode,
accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]
node: interfaces.renderers.TreeNode,
accumulator: List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]],
) -> List[Tuple[int, Dict[interfaces.renderers.Column, bytes]]]:
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
max_column_widths[tree_indent_column] = max(max_column_widths.get(tree_indent_column, 0), node.path_depth)
max_column_widths[tree_indent_column] = max(
max_column_widths.get(tree_indent_column, 0), node.path_depth
)
line = {}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(column.type, self._type_renderers['default'])
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
data = renderer(node.values[column_index])
field_width = max([len(self.tab_stop(x)) for x in f"{data}".split("\n")])
max_column_widths[column.name] = max(max_column_widths.get(column.name, len(column.name)),
field_width)
field_width = max(
[len(self.tab_stop(x)) for x in f"{data}".split("\n")]
)
max_column_widths[column.name] = max(
max_column_widths.get(column.name, len(column.name)), field_width
)
line[column] = data.split("\n")
accumulator.append((node.path_depth, line))
return accumulator
@@ -295,14 +325,22 @@ class PrettyTextRenderer(CLIRenderer):
if not grid.populated:
grid.populate(visitor, final_output)
else:
grid.visit(node = None, function = visitor, initial_accumulator = final_output)
grid.visit(node=None, function=visitor, initial_accumulator=final_output)
# Always align the tree to the left
format_string_list = ["{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"]
format_string_list = [
"{0:<" + str(max_column_widths.get(tree_indent_column, 0)) + "s}"
]
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
format_string_list.append("{" + str(column_index + 1) + ":" + display_alignment +
str(max_column_widths[column.name]) + "s}")
format_string_list.append(
"{"
+ str(column_index + 1)
+ ":"
+ display_alignment
+ str(max_column_widths[column.name])
+ "s}"
)
format_string = column_separator.join(format_string_list) + "\n"
@@ -314,14 +352,30 @@ class PrettyTextRenderer(CLIRenderer):
line[column] = line[column] + ([""] * (nums_line - len(line[column])))
for index in range(nums_line):
if index == 0:
outfd.write(format_string.format("*" * depth, *[self.tab_stop(line[column][index]) for column in grid.columns]))
outfd.write(
format_string.format(
"*" * depth,
*[
self.tab_stop(line[column][index])
for column in grid.columns
],
)
)
else:
outfd.write(format_string.format(" " * depth, *[self.tab_stop(line[column][index]) for column in grid.columns]))
outfd.write(
format_string.format(
" " * depth,
*[
self.tab_stop(line[column][index])
for column in grid.columns
],
)
)
def tab_stop(self, line: str) -> str:
tab_width = 8
while line.find('\t') >= 0:
i = line.find('\t')
while line.find("\t") >= 0:
i = line.find("\t")
pad = " " * (tab_width - (i % tab_width))
line = line.replace("\t", pad, 1)
return line
@@ -333,11 +387,13 @@ class JsonRenderer(CLIRenderer):
interfaces.renderers.Disassembly: quoted_optional(display_disassembly),
format_hints.MultiTypeData: quoted_optional(multitypedata_as_text),
bytes: optional(lambda x: " ".join([f"{b:02x}" for b in x])),
datetime.datetime: lambda x: x.isoformat() if not isinstance(x, interfaces.renderers.BaseAbsentValue) else None,
'default': lambda x: x
datetime.datetime: lambda x: x.isoformat()
if not isinstance(x, interfaces.renderers.BaseAbsentValue)
else None,
"default": lambda x: x,
}
name = 'JSON'
name = "JSON"
structured_output = True
def get_render_options(self) -> List[interfaces.renderers.RenderOption]:
@@ -345,30 +401,35 @@ 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("{}\n".format(json.dumps(result, indent=2, sort_keys=True)))
def render(self, grid: interfaces.renderers.TreeGrid):
outfd = sys.stdout
outfd.write("\n")
final_output: Tuple[Dict[str, List[interfaces.renderers.TreeNode]], List[interfaces.renderers.TreeNode]] = (
{}, [])
final_output: Tuple[
Dict[str, List[interfaces.renderers.TreeNode]],
List[interfaces.renderers.TreeNode],
] = ({}, [])
def visitor(
node: interfaces.renderers.TreeNode, accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]
node: interfaces.renderers.TreeNode,
accumulator: Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]],
) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]:
# Nodes always have a path value, giving them a path_depth of at least 1, we use max just in case
acc_map, final_tree = accumulator
node_dict: Dict[str, Any] = {'__children': []}
node_dict: Dict[str, Any] = {"__children": []}
for column_index in range(len(grid.columns)):
column = grid.columns[column_index]
renderer = self._type_renderers.get(column.type, self._type_renderers['default'])
renderer = self._type_renderers.get(
column.type, self._type_renderers["default"]
)
data = renderer(list(node.values)[column_index])
if isinstance(data, interfaces.renderers.BaseAbsentValue):
data = None
node_dict[column.name] = data
if node.parent:
acc_map[node.parent.path]['__children'].append(node_dict)
acc_map[node.parent.path]["__children"].append(node_dict)
else:
final_tree.append(node_dict)
acc_map[node.path] = node_dict
@@ -378,16 +439,16 @@ class JsonRenderer(CLIRenderer):
if not grid.populated:
grid.populate(visitor, final_output)
else:
grid.visit(node = None, function = visitor, initial_accumulator = final_output)
grid.visit(node=None, function=visitor, initial_accumulator=final_output)
self.output_result(outfd, final_output[1])
class JsonLinesRenderer(JsonRenderer):
name = 'JSONL'
name = "JSONL"
def output_result(self, outfd, result):
"""Outputs the JSON results as JSON lines"""
for line in result:
outfd.write(json.dumps(line, sort_keys = True))
outfd.write(json.dumps(line, sort_keys=True))
outfd.write("\n")
+21 -13
View File
@@ -24,13 +24,15 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
# We don't want the action self-check to kick in, so we remove the choices list, the check happens in __call__
self.choices = None
def __call__(self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[str, Sequence[Any], None],
option_string: Optional[str] = None) -> None:
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[str, Sequence[Any], None],
option_string: Optional[str] = None,
) -> None:
parser_name = ''
parser_name = ""
arg_strings = [] # type: List[str]
if values is not None:
for value in values:
@@ -43,7 +45,9 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
if self.dest != argparse.SUPPRESS:
setattr(namespace, self.dest, parser_name)
matched_parsers = [name for name in self._name_parser_map if parser_name in name]
matched_parsers = [
name for name in self._name_parser_map if parser_name in name
]
if len(matched_parsers) < 1:
msg = f"invalid choice {parser_name} (choose from {', '.join(self._name_parser_map)})"
@@ -52,7 +56,7 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
msg = f"plugin {parser_name} matches multiple plugins ({', '.join(matched_parsers)})"
raise argparse.ArgumentError(self, msg)
parser = self._name_parser_map[matched_parsers[0]]
setattr(namespace, 'plugin', matched_parsers[0])
setattr(namespace, "plugin", matched_parsers[0])
# parse all the remaining options into the namespace
# store any unrecognized options on the object, so that the top
@@ -71,7 +75,6 @@ class HelpfulSubparserAction(argparse._SubParsersAction):
class HelpfulArgParser(argparse.ArgumentParser):
def _match_argument(self, action, arg_strings_pattern) -> int:
# match the pattern for this action to the arg strings
nargs_pattern = self._get_nargs_pattern(action)
@@ -80,13 +83,18 @@ class HelpfulArgParser(argparse.ArgumentParser):
# raise an exception if we weren't able to find a match
if match is None:
nargs_errors = {
None: gettext.gettext('expected one argument'),
argparse.OPTIONAL: gettext.gettext('expected at most one argument'),
argparse.ONE_OR_MORE: gettext.gettext('expected at least one argument'),
None: gettext.gettext("expected one argument"),
argparse.OPTIONAL: gettext.gettext("expected at most one argument"),
argparse.ONE_OR_MORE: gettext.gettext("expected at least one argument"),
}
msg = nargs_errors.get(action.nargs)
if msg is None:
msg = gettext.ngettext('expected %s argument', 'expected %s arguments', action.nargs) % action.nargs
msg = (
gettext.ngettext(
"expected %s argument", "expected %s arguments", action.nargs
)
% action.nargs
)
if action.choices:
msg = f"{msg} (from: {', '.join(action.choices)})"
raise argparse.ArgumentError(action, msg)
+194 -93
View File
@@ -12,7 +12,14 @@ import volatility3.plugins
import volatility3.symbols
from volatility3 import cli, framework
from volatility3.cli.volshell import generic, linux, mac, windows
from volatility3.framework import automagic, constants, contexts, exceptions, interfaces, plugins
from volatility3.framework import (
automagic,
constants,
contexts,
exceptions,
interfaces,
plugins,
)
# Make sure we log everything
vollog = logging.getLogger()
@@ -20,7 +27,7 @@ vollog.setLevel(0)
# Trim the console down by default
console = logging.StreamHandler()
console.setLevel(logging.WARNING)
formatter = logging.Formatter('%(levelname)-8s %(name)-12s: %(message)s')
formatter = logging.Formatter("%(levelname)-8s %(name)-12s: %(message)s")
console.setFormatter(formatter)
vollog.addHandler(console)
@@ -36,89 +43,143 @@ class VolShell(cli.CommandLine):
def __init__(self):
super().__init__()
self.output_dir = None
def run(self):
"""Executes the command line module, taking the system arguments,
determining the plugin to run and then running it."""
sys.stdout.write(f"Volshell (Volatility 3 Framework) {constants.PACKAGE_VERSION}\n")
sys.stdout.write(
f"Volshell (Volatility 3 Framework) {constants.PACKAGE_VERSION}\n"
)
framework.require_interface_version(2, 0, 0)
parser = argparse.ArgumentParser(prog = self.CLI_NAME,
description = "A tool for interactivate forensic analysis of memory images")
parser.add_argument("-c",
"--config",
help = "Load the configuration from a json file",
default = None,
type = str)
parser.add_argument("-e",
"--extend",
help = "Extend the configuration with a new (or changed) setting",
default = None,
action = 'append')
parser.add_argument("-p",
"--plugin-dirs",
help = "Semi-colon separated list of paths to find plugins",
default = "",
type = str)
parser.add_argument("-s",
"--symbol-dirs",
help = "Semi-colon separated list of paths to find symbols",
default = "",
type = str)
parser.add_argument("-v", "--verbosity", help = "Increase output verbosity", default = 0, action = "count")
parser.add_argument("-o",
"--output-dir",
help = "Directory in which to output any generated files",
default = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')),
type = str)
parser.add_argument("-q", "--quiet", help = "Remove progress feedback", default = False, action = 'store_true')
parser.add_argument("--log", help = "Log output to a file as well as the console", default = None, type = str)
parser.add_argument("-f",
"--file",
metavar = 'FILE',
default = None,
type = str,
help = "Shorthand for --single-location=file:// if single-location is not defined")
parser.add_argument("--write-config",
help = "Write configuration JSON file out to config.json",
default = False,
action = 'store_true')
parser.add_argument("--save-config",
help = "Save configuration JSON file to a file",
default = None,
type = str)
parser.add_argument("--clear-cache",
help = "Clears out all short-term cached items",
default = False,
action = 'store_true')
parser.add_argument("--cache-path",
help = f"Change the default path ({constants.CACHE_PATH}) used to store the cache",
default = constants.CACHE_PATH,
type = str)
parser = argparse.ArgumentParser(
prog=self.CLI_NAME,
description="A tool for interactivate forensic analysis of memory images",
)
parser.add_argument(
"-c",
"--config",
help="Load the configuration from a json file",
default=None,
type=str,
)
parser.add_argument(
"-e",
"--extend",
help="Extend the configuration with a new (or changed) setting",
default=None,
action="append",
)
parser.add_argument(
"-p",
"--plugin-dirs",
help="Semi-colon separated list of paths to find plugins",
default="",
type=str,
)
parser.add_argument(
"-s",
"--symbol-dirs",
help="Semi-colon separated list of paths to find symbols",
default="",
type=str,
)
parser.add_argument(
"-v",
"--verbosity",
help="Increase output verbosity",
default=0,
action="count",
)
parser.add_argument(
"-o",
"--output-dir",
help="Directory in which to output any generated files",
default=os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..")
),
type=str,
)
parser.add_argument(
"-q",
"--quiet",
help="Remove progress feedback",
default=False,
action="store_true",
)
parser.add_argument(
"--log",
help="Log output to a file as well as the console",
default=None,
type=str,
)
parser.add_argument(
"-f",
"--file",
metavar="FILE",
default=None,
type=str,
help="Shorthand for --single-location=file:// if single-location is not defined",
)
parser.add_argument(
"--write-config",
help="Write configuration JSON file out to config.json",
default=False,
action="store_true",
)
parser.add_argument(
"--save-config",
help="Save configuration JSON file to a file",
default=None,
type=str,
)
parser.add_argument(
"--clear-cache",
help="Clears out all short-term cached items",
default=False,
action="store_true",
)
parser.add_argument(
"--cache-path",
help=f"Change the default path ({constants.CACHE_PATH}) used to store the cache",
default=constants.CACHE_PATH,
type=str,
)
# Volshell specific flags
os_specific = parser.add_mutually_exclusive_group(required = False)
os_specific.add_argument("-w",
"--windows",
default = False,
action = "store_true",
help = "Run a Windows volshell")
os_specific.add_argument("-l", "--linux", default = False, action = "store_true", help = "Run a Linux volshell")
os_specific.add_argument("-m", "--mac", default = False, action = "store_true", help = "Run a Mac volshell")
os_specific = parser.add_mutually_exclusive_group(required=False)
os_specific.add_argument(
"-w",
"--windows",
default=False,
action="store_true",
help="Run a Windows volshell",
)
os_specific.add_argument(
"-l",
"--linux",
default=False,
action="store_true",
help="Run a Linux volshell",
)
os_specific.add_argument(
"-m", "--mac", default=False, action="store_true", help="Run a Mac volshell"
)
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
# processed the plugin choice or had the plugin subparser added.
known_args = [arg for arg in sys.argv if arg != '--help' and arg != '-h']
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
partial_args, _ = parser.parse_known_args(known_args)
if partial_args.plugin_dirs:
volatility3.plugins.__path__ = [os.path.abspath(p)
for p in partial_args.plugin_dirs.split(";")] + constants.PLUGINS_PATH
volatility3.plugins.__path__ = [
os.path.abspath(p) for p in partial_args.plugin_dirs.split(";")
] + constants.PLUGINS_PATH
if partial_args.symbol_dirs:
volatility3.symbols.__path__ = [os.path.abspath(p)
for p in partial_args.symbol_dirs.split(";")] + constants.SYMBOL_BASEPATHS
volatility3.symbols.__path__ = [
os.path.abspath(p) for p in partial_args.symbol_dirs.split(";")
] + constants.SYMBOL_BASEPATHS
if partial_args.cache_path:
constants.CACHE_PATH = partial_args.cache_path
@@ -129,8 +190,10 @@ class VolShell(cli.CommandLine):
if partial_args.log:
file_logger = logging.FileHandler(partial_args.log)
file_logger.setLevel(0)
file_formatter = logging.Formatter(datefmt = '%y-%m-%d %H:%M:%S',
fmt = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
file_formatter = logging.Formatter(
datefmt="%y-%m-%d %H:%M:%S",
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
)
file_logger.setFormatter(file_formatter)
vollog.addHandler(file_logger)
vollog.info("Logging started")
@@ -145,11 +208,14 @@ class VolShell(cli.CommandLine):
# Do the initialization
ctx = contexts.Context() # Construct a blank context
failures = framework.import_files(volatility3.plugins,
True) # Will not log as console's default level is WARNING
failures = framework.import_files(
volatility3.plugins, True
) # Will not log as console's default level is WARNING
if failures:
parser.epilog = "The following plugins could not be loaded (use -vv to see why): " + \
", ".join(sorted(failures))
parser.epilog = (
"The following plugins could not be loaded (use -vv to see why): "
+ ", ".join(sorted(failures))
)
vollog.info(parser.epilog)
automagics = automagic.available(ctx)
@@ -167,11 +233,17 @@ class VolShell(cli.CommandLine):
configurables_list[amagic.__class__.__name__] = amagic
# We don't list plugin arguments, because they can be provided within python
volshell_plugin_list = {'generic': generic.Volshell, 'windows': windows.Volshell}
volshell_plugin_list = {
"generic": generic.Volshell,
"windows": windows.Volshell,
}
for plugin in volshell_plugin_list:
subparser = parser.add_argument_group(title = plugin.capitalize(),
description = "Configuration options based on {} options".format(
plugin.capitalize()))
subparser = parser.add_argument_group(
title=plugin.capitalize(),
description="Configuration options based on {} options".format(
plugin.capitalize()
),
)
self.populate_requirements_argparse(subparser, volshell_plugin_list[plugin])
configurables_list[plugin] = volshell_plugin_list[plugin]
@@ -183,7 +255,9 @@ class VolShell(cli.CommandLine):
# Run the argparser
args = parser.parse_args()
vollog.log(constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}")
vollog.log(
constants.LOGLEVEL_VVV, f"Cache directory used: {constants.CACHE_PATH}"
)
plugin = generic.Volshell
if args.windows:
@@ -194,7 +268,9 @@ class VolShell(cli.CommandLine):
plugin = mac.Volshell
base_config_path = "plugins"
plugin_config_path = interfaces.configuration.path_join(base_config_path, plugin.__name__)
plugin_config_path = interfaces.configuration.path_join(
base_config_path, plugin.__name__
)
# Special case the -f argument because people use is so frequently
# It has to go here so it can be overridden by single-location if it's defined
@@ -203,7 +279,7 @@ class VolShell(cli.CommandLine):
if args.file:
try:
single_location = self.location_from_file(args.file)
ctx.config['automagic.LayerStacker.single_location'] = single_location
ctx.config["automagic.LayerStacker.single_location"] = single_location
except ValueError as excp:
parser.error(str(excp))
@@ -211,15 +287,22 @@ class VolShell(cli.CommandLine):
if args.config:
with open(args.config, "r") as f:
json_val = json.load(f)
ctx.config.splice(plugin_config_path, interfaces.configuration.HierarchicalDict(json_val))
ctx.config.splice(
plugin_config_path,
interfaces.configuration.HierarchicalDict(json_val),
)
self.populate_config(ctx, configurables_list, args, plugin_config_path)
if args.extend:
for extension in args.extend:
if '=' not in extension:
raise ValueError("Invalid extension (extensions must be of the format \"conf.path.value='value'\")")
address, value = extension[:extension.find('=')], json.loads(extension[extension.find('=') + 1:])
if "=" not in extension:
raise ValueError(
"Invalid extension (extensions must be of the format \"conf.path.value='value'\")"
)
address, value = extension[: extension.find("=")], json.loads(
extension[extension.find("=") + 1 :]
)
ctx.config[address] = value
# It should be up to the UI to determine which automagics to run, so this is before BACK TO THE FRAMEWORK
@@ -234,22 +317,40 @@ class VolShell(cli.CommandLine):
if args.quiet:
progress_callback = cli.MuteProgress()
constructed = plugins.construct_plugin(ctx, automagics, plugin, base_config_path, progress_callback,
self.file_handler_class_factory())
constructed = plugins.construct_plugin(
ctx,
automagics,
plugin,
base_config_path,
progress_callback,
self.file_handler_class_factory(),
)
if args.write_config:
vollog.warning('Use of --write-config has been deprecated, replaced by --save-config <filename>')
args.save_config = 'config.json'
vollog.warning(
"Use of --write-config has been deprecated, replaced by --save-config <filename>"
)
args.save_config = "config.json"
if args.save_config:
vollog.debug("Writing out configuration data to {args.save_config}")
if os.path.exists(os.path.abspath(args.save_config)):
parser.error(f"Cannot write configuration: file {args.save_config} already exists")
parser.error(
f"Cannot write configuration: file {args.save_config} already exists"
)
with open(args.save_config, "w") as f:
json.dump(dict(constructed.build_configuration()), f, sort_keys = True, indent = 2)
json.dump(
dict(constructed.build_configuration()),
f,
sort_keys=True,
indent=2,
)
f.write("\n")
except exceptions.UnsatisfiedException as excp:
self.process_unsatisfied_exceptions(excp)
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
parser.exit(
1,
f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n",
)
try:
# Construct and run the plugin
+212 -105
View File
@@ -26,6 +26,7 @@ except ImportError:
class Volshell(interfaces.plugins.PluginInterface):
"""Shell environment to directly interact with a memory image."""
_required_framework_version = (2, 0, 0)
def __init__(self, *args, **kwargs):
@@ -36,23 +37,29 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__console = None
def random_string(self, length: int = 32) -> str:
return ''.join(random.sample(string.ascii_uppercase + string.digits, length))
return "".join(random.sample(string.ascii_uppercase + string.digits, length))
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
reqs: List[interfaces.configuration.RequirementInterface] = []
if cls == Volshell:
reqs = [
requirements.URIRequirement(name = 'script',
description = 'File to load and execute at start',
default = None,
optional = True)
requirements.URIRequirement(
name="script",
description="File to load and execute at start",
default=None,
optional=True,
)
]
return reqs + [
requirements.TranslationLayerRequirement(name = 'primary', description = 'Memory layer for the kernel'),
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer for the kernel"
),
]
def run(self, additional_locals: Dict[str, Any] = None) -> interfaces.renderers.TreeGrid:
def run(
self, additional_locals: Dict[str, Any] = None
) -> interfaces.renderers.TreeGrid:
"""Runs the interactive volshell plugin.
Returns:
@@ -66,14 +73,15 @@ class Volshell(interfaces.plugins.PluginInterface):
pass
else:
import rlcompleter
completer = rlcompleter.Completer(namespace = self._construct_locals_dict())
completer = rlcompleter.Completer(namespace=self._construct_locals_dict())
readline.set_completer(completer.complete)
readline.parse_and_bind("tab: complete")
print("Readline imported successfully")
# TODO: provide help, consider generic functions (pslist?) and/or providing windows/linux functions
mode = self.__module__.split('.')[-1]
mode = self.__module__.split(".")[-1]
mode = mode[0].upper() + mode[1:]
banner = f"""
@@ -86,13 +94,13 @@ class Volshell(interfaces.plugins.PluginInterface):
"""
sys.ps1 = f"({self.current_layer}) >>> "
self.__console = code.InteractiveConsole(locals = self._construct_locals_dict())
self.__console = code.InteractiveConsole(locals=self._construct_locals_dict())
# Since we have to do work to add the option only once for all different modes of volshell, we can't
# rely on the default having been set
if self.config.get('script', None) is not None:
self.run_script(location = self.config['script'])
if self.config.get("script", None) is not None:
self.run_script(location=self.config["script"])
self.__console.interact(banner = banner)
self.__console.interact(banner=banner)
return renderers.TreeGrid([("Terminating", str)], None)
@@ -119,47 +127,70 @@ class Volshell(interfaces.plugins.PluginInterface):
def construct_locals(self) -> List[Tuple[List[str], Any]]:
"""Returns a dictionary listing the functions to be added to the
environment."""
return [(['dt', 'display_type'], self.display_type), (['db', 'display_bytes'], self.display_bytes),
(['dw', 'display_words'], self.display_words), (['dd',
'display_doublewords'], self.display_doublewords),
(['dq', 'display_quadwords'], self.display_quadwords), (['dis', 'disassemble'], self.disassemble),
(['cl', 'change_layer'], self.change_layer),
(['cs', 'change_symboltable'], self.change_symbol_table),
(['ck', 'change_kernel'], self.change_kernel),
(['context'], self.context), (['self'], self),
(['dpo', 'display_plugin_output'], self.display_plugin_output),
(['gt', 'generate_treegrid'], self.generate_treegrid), (['rt',
'render_treegrid'], self.render_treegrid),
(['ds', 'display_symbols'], self.display_symbols), (['hh', 'help'], self.help),
(['cc', 'create_configurable'], self.create_configurable), (['lf', 'load_file'], self.load_file),
(['rs', 'run_script'], self.run_script)]
return [
(["dt", "display_type"], self.display_type),
(["db", "display_bytes"], self.display_bytes),
(["dw", "display_words"], self.display_words),
(["dd", "display_doublewords"], self.display_doublewords),
(["dq", "display_quadwords"], self.display_quadwords),
(["dis", "disassemble"], self.disassemble),
(["cl", "change_layer"], self.change_layer),
(["cs", "change_symboltable"], self.change_symbol_table),
(["ck", "change_kernel"], self.change_kernel),
(["context"], self.context),
(["self"], self),
(["dpo", "display_plugin_output"], self.display_plugin_output),
(["gt", "generate_treegrid"], self.generate_treegrid),
(["rt", "render_treegrid"], self.render_treegrid),
(["ds", "display_symbols"], self.display_symbols),
(["hh", "help"], self.help),
(["cc", "create_configurable"], self.create_configurable),
(["lf", "load_file"], self.load_file),
(["rs", "run_script"], self.run_script),
]
def _construct_locals_dict(self) -> Dict[str, Any]:
"""Returns a dictionary of the locals """
"""Returns a dictionary of the locals"""
result = {}
for aliases, value in self.construct_locals():
for alias in aliases:
result[alias] = value
return result
def _read_data(self, offset, count = 128, layer_name = None):
def _read_data(self, offset, count=128, layer_name=None):
"""Reads the bytes necessary for the display_* methods"""
return self.context.layers[layer_name or self.current_layer].read(offset, count)
def _display_data(self, offset: int, remaining_data: bytes, format_string: str = "B", ascii: bool = True):
def _display_data(
self,
offset: int,
remaining_data: bytes,
format_string: str = "B",
ascii: bool = True,
):
"""Display a series of bytes"""
chunk_size = struct.calcsize(format_string)
data_length = len(remaining_data)
remaining_data = remaining_data[:data_length - (data_length % chunk_size)]
remaining_data = remaining_data[: data_length - (data_length % chunk_size)]
while remaining_data:
current_line, remaining_data = remaining_data[:16], remaining_data[16:]
data_blocks = [current_line[chunk_size * i:chunk_size * (i + 1)] for i in range(16 // chunk_size)]
data_blocks = [x for x in data_blocks if x != b'']
valid_data = [("{:0" + str(2 * chunk_size) + "x}").format(struct.unpack(format_string, x)[0])
for x in data_blocks]
padding_data = [" " * 2 * chunk_size for _ in range((16 - len(current_line)) // chunk_size)]
data_blocks = [
current_line[chunk_size * i : chunk_size * (i + 1)]
for i in range(16 // chunk_size)
]
data_blocks = [x for x in data_blocks if x != b""]
valid_data = [
("{:0" + str(2 * chunk_size) + "x}").format(
struct.unpack(format_string, x)[0]
)
for x in data_blocks
]
padding_data = [
" " * 2 * chunk_size
for _ in range((16 - len(current_line)) // chunk_size)
]
hex_data = " ".join(valid_data + padding_data)
ascii_data = ""
@@ -175,12 +206,14 @@ class Volshell(interfaces.plugins.PluginInterface):
@staticmethod
def _ascii_bytes(bytes):
"""Converts bytes into an ascii string"""
return "".join([chr(x) if 32 < x < 127 else '.' for x in binascii.unhexlify(bytes)])
return "".join(
[chr(x) if 32 < x < 127 else "." for x in binascii.unhexlify(bytes)]
)
@property
def current_layer(self):
if self.__current_layer is None:
self.__current_layer = self.config['primary']
self.__current_layer = self.config["primary"]
return self.__current_layer
@property
@@ -192,7 +225,7 @@ class Volshell(interfaces.plugins.PluginInterface):
@property
def current_kernel_name(self):
if self.__current_kernel_name is None:
self.__current_kernel_name = self.config.get('kernel', None)
self.__current_kernel_name = self.config.get("kernel", None)
return self.__current_kernel_name
@property
@@ -217,7 +250,9 @@ class Volshell(interfaces.plugins.PluginInterface):
if not symbol_table_name:
print("No symbol table provided, not changing current symbol table")
if symbol_table_name not in self.context.symbol_space:
print(f"Symbol table {symbol_table_name} not present in context symbol_space")
print(
f"Symbol table {symbol_table_name} not present in context symbol_space"
)
else:
self.__current_symbol_table = symbol_table_name
print(f"Current Symbol Table: {self.current_symbol_table}")
@@ -231,51 +266,64 @@ class Volshell(interfaces.plugins.PluginInterface):
self.__current_kernel_name = kernel_name
print(f"Current kernel : {self.current_kernel_name}")
def display_bytes(self, offset, count = 128, layer_name = None):
def display_bytes(self, offset, count=128, layer_name=None):
"""Displays byte values and ASCII characters"""
remaining_data = self._read_data(offset, count = count, layer_name = layer_name)
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data)
def display_quadwords(self, offset, count = 128, layer_name = None):
def display_quadwords(self, offset, count=128, layer_name=None):
"""Displays quad-word values (8 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count = count, layer_name = layer_name)
self._display_data(offset, remaining_data, format_string = "Q")
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="Q")
def display_doublewords(self, offset, count = 128, layer_name = None):
def display_doublewords(self, offset, count=128, layer_name=None):
"""Displays double-word values (4 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count = count, layer_name = layer_name)
self._display_data(offset, remaining_data, format_string = "I")
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="I")
def display_words(self, offset, count = 128, layer_name = None):
def display_words(self, offset, count=128, layer_name=None):
"""Displays word values (2 bytes) and corresponding ASCII characters"""
remaining_data = self._read_data(offset, count = count, layer_name = layer_name)
self._display_data(offset, remaining_data, format_string = "H")
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
self._display_data(offset, remaining_data, format_string="H")
def disassemble(self, offset, count = 128, layer_name = None, architecture = None):
def disassemble(self, offset, count=128, layer_name=None, architecture=None):
"""Disassembles a number of instructions from the code at offset"""
remaining_data = self._read_data(offset, count = count, layer_name = layer_name)
remaining_data = self._read_data(offset, count=count, layer_name=layer_name)
if not has_capstone:
print("Capstone not available - please install it to use the disassemble command")
print(
"Capstone not available - please install it to use the disassemble command"
)
else:
if isinstance(self.context.layers[layer_name or self.current_layer], intel.Intel32e):
architecture = 'intel64'
elif isinstance(self.context.layers[layer_name or self.current_layer], intel.Intel):
architecture = 'intel'
if isinstance(
self.context.layers[layer_name or self.current_layer], intel.Intel32e
):
architecture = "intel64"
elif isinstance(
self.context.layers[layer_name or self.current_layer], intel.Intel
):
architecture = "intel"
disasm_types = {
'intel': capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32),
'intel64': capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64),
'arm': capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
'arm64': capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
"intel": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32),
"intel64": capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64),
"arm": capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM),
"arm64": capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM),
}
if architecture is not None:
for i in disasm_types[architecture].disasm(remaining_data, offset):
print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")
def display_type(self,
object: Union[str, interfaces.objects.ObjectInterface, interfaces.objects.Template],
offset: int = None):
def display_type(
self,
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if not isinstance(object, (str, interfaces.objects.ObjectInterface, interfaces.objects.Template)):
if not isinstance(
object,
(str, interfaces.objects.ObjectInterface, interfaces.objects.Template),
):
print("Cannot display information about non-type object")
return
@@ -287,20 +335,29 @@ class Volshell(interfaces.plugins.PluginInterface):
volobject = self.context.symbol_space.get_type(object)
else:
# Str and offset
volobject = self.context.object(object, layer_name = self.current_layer, offset = offset)
volobject = self.context.object(
object, layer_name=self.current_layer, offset=offset
)
if offset is not None:
volobject = self.context.object(volobject.vol.type_name, layer_name = self.current_layer, offset = offset)
volobject = self.context.object(
volobject.vol.type_name, layer_name=self.current_layer, offset=offset
)
if hasattr(volobject.vol, 'size'):
if hasattr(volobject.vol, "size"):
print(f"{volobject.vol.type_name} ({volobject.vol.size} bytes)")
elif hasattr(volobject.vol, 'data_format'):
elif hasattr(volobject.vol, "data_format"):
data_format = volobject.vol.data_format
print("{} ({} bytes, {} endian, {})".format(volobject.vol.type_name, data_format.length,
data_format.byteorder,
'signed' if data_format.signed else 'unsigned'))
print(
"{} ({} bytes, {} endian, {})".format(
volobject.vol.type_name,
data_format.length,
data_format.byteorder,
"signed" if data_format.signed else "unsigned",
)
)
if hasattr(volobject.vol, 'members'):
if hasattr(volobject.vol, "members"):
longest_member = longest_offset = longest_typename = 0
for member in volobject.vol.members:
relative_offset, member_type = volobject.vol.members[member]
@@ -308,32 +365,50 @@ class Volshell(interfaces.plugins.PluginInterface):
longest_offset = max(len(hex(relative_offset)), longest_offset)
longest_typename = max(len(member_type.vol.type_name), longest_typename)
for member in sorted(volobject.vol.members, key = lambda x: (volobject.vol.members[x][0], x)):
for member in sorted(
volobject.vol.members, key=lambda x: (volobject.vol.members[x][0], x)
):
relative_offset, member_type = volobject.vol.members[member]
len_offset = len(hex(relative_offset))
len_member = len(member)
len_typename = len(member_type.vol.type_name)
if isinstance(volobject, interfaces.objects.ObjectInterface):
# We're an instance, so also display the data
print(" " * (longest_offset - len_offset), hex(relative_offset), ": ", member,
" " * (longest_member - len_member), " ",
member_type.vol.type_name, " " * (longest_typename - len_typename), " ",
self._display_value(getattr(volobject, member)))
print(
" " * (longest_offset - len_offset),
hex(relative_offset),
": ",
member,
" " * (longest_member - len_member),
" ",
member_type.vol.type_name,
" " * (longest_typename - len_typename),
" ",
self._display_value(getattr(volobject, member)),
)
else:
print(" " * (longest_offset - len_offset), hex(relative_offset), ": ", member,
" " * (longest_member - len_member), " ", member_type.vol.type_name)
print(
" " * (longest_offset - len_offset),
hex(relative_offset),
": ",
member,
" " * (longest_member - len_member),
" ",
member_type.vol.type_name,
)
@classmethod
def _display_value(self, value: Any) -> str:
def _display_value(cls, value: Any) -> str:
if isinstance(value, objects.PrimitiveObject):
return repr(value)
elif isinstance(value, objects.Array):
return repr([self._display_value(val) for val in value])
return repr([cls._display_value(val) for val in value])
else:
return hex(value.vol.offset)
def generate_treegrid(self, plugin: Type[interfaces.plugins.PluginInterface],
**kwargs) -> Optional[interfaces.renderers.TreeGrid]:
def generate_treegrid(
self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs
) -> Optional[interfaces.renderers.TreeGrid]:
"""Generates a TreeGrid based on a specific plugin passing in kwarg configuration values"""
path_join = interfaces.configuration.path_join
@@ -346,21 +421,29 @@ class Volshell(interfaces.plugins.PluginInterface):
self.config[path_join(plugin_config_suffix, plugin.__name__, name)] = value
try:
constructed = plugins.construct_plugin(self.context, [], plugin, plugin_path, None, NullFileHandler)
constructed = plugins.construct_plugin(
self.context, [], plugin, plugin_path, None, NullFileHandler
)
return constructed.run()
except exceptions.UnsatisfiedException as excp:
print(f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
print(
f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n"
)
return None
def render_treegrid(self,
treegrid: interfaces.renderers.TreeGrid,
renderer: Optional[interfaces.renderers.Renderer] = None) -> None:
def render_treegrid(
self,
treegrid: interfaces.renderers.TreeGrid,
renderer: Optional[interfaces.renderers.Renderer] = None,
) -> None:
"""Renders a treegrid as produced by generate_treegrid"""
if renderer is None:
renderer = text_renderer.QuickTextRenderer()
renderer.render(treegrid)
def display_plugin_output(self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs) -> None:
def display_plugin_output(
self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs
) -> None:
"""Displays the output for a particular plugin (with keyword arguments)"""
treegrid = self.generate_treegrid(plugin, **kwargs)
if treegrid is not None:
@@ -382,7 +465,12 @@ class Volshell(interfaces.plugins.PluginInterface):
for symbol_name in sorted(table.symbols):
symbol = table.get_symbol(symbol_name)
len_offset = len(hex(symbol.address))
print(" " * (longest_offset - len_offset), hex(symbol.address), " ", symbol.name)
print(
" " * (longest_offset - len_offset),
hex(symbol.address),
" ",
symbol.name,
)
def run_script(self, location: str):
"""Runs a python script within the context of volshell"""
@@ -390,32 +478,45 @@ class Volshell(interfaces.plugins.PluginInterface):
location = "file:" + request.pathname2url(location)
print(f"Running code from {location}\n")
accessor = resources.ResourceAccessor()
with io.TextIOWrapper(accessor.open(url = location), encoding = 'utf-8') as fp:
self.__console.runsource(fp.read(), symbol = 'exec')
with accessor.open(url=location) as fp:
self.__console.runsource(
io.TextIOWrapper(fp.read(), encoding="utf-8"), symbol="exec"
)
print("\nCode complete")
def load_file(self, location: str):
"""Loads a file into a Filelayer and returns the name of the layer"""
layer_name = self.context.layers.free_layer_name()
location = volshell.VolShell.location_from_file(location)
current_config_path = 'volshell.layers.' + layer_name
self.context.config[interfaces.configuration.path_join(current_config_path, "location")] = location
current_config_path = "volshell.layers." + layer_name
self.context.config[
interfaces.configuration.path_join(current_config_path, "location")
] = location
layer = physical.FileLayer(self.context, current_config_path, layer_name)
self.context.add_layer(layer)
return layer_name
def create_configurable(self, clazz: Type[interfaces.configuration.ConfigurableInterface], **kwargs):
def create_configurable(
self, clazz: Type[interfaces.configuration.ConfigurableInterface], **kwargs
):
"""Creates a configurable object, converting arguments to configuration"""
config_name = self.random_string()
config_path = 'volshell.configurable.' + config_name
config_path = "volshell.configurable." + config_name
constructor_args = {}
constructor_keywords = []
if issubclass(clazz, interfaces.layers.DataLayerInterface):
constructor_keywords = [('name', self.context.layers.free_layer_name(config_name)), ('metadata', None)]
constructor_keywords = [
("name", self.context.layers.free_layer_name(config_name)),
("metadata", None),
]
if issubclass(clazz, interfaces.symbols.SymbolTableInterface):
constructor_keywords = [('name', self.context.symbol_space.free_table_name(config_name)),
('native_types', None), ('table_mapping', None), ('class_types', None)]
constructor_keywords = [
("name", self.context.symbol_space.free_table_name(config_name)),
("native_types", None),
("table_mapping", None),
("class_types", None),
]
for argname, default in constructor_keywords:
constructor_args[argname] = kwargs.get(argname, default)
@@ -424,10 +525,16 @@ class Volshell(interfaces.plugins.PluginInterface):
for keyword in kwargs:
val = kwargs[keyword]
if not isinstance(val, interfaces.configuration.BasicTypes) and not isinstance(val, list):
if not isinstance(val, list) or all([isinstance(x, interfaces.configuration.BasicTypes) for x in val]):
raise TypeError("Configurable values must be simple types (int, bool, str, bytes)")
self.context.config[config_path + '.' + keyword] = val
if not isinstance(
val, interfaces.configuration.BasicTypes
) and not isinstance(val, list):
if not isinstance(val, list) or all(
[isinstance(x, interfaces.configuration.BasicTypes) for x in val]
):
raise TypeError(
"Configurable values must be simple types (int, bool, str, bytes)"
)
self.context.config[config_path + "." + keyword] = val
constructed = clazz(self.context, config_path, **constructor_args)
+24 -14
View File
@@ -15,13 +15,19 @@ class Volshell(generic.Volshell):
@classmethod
def get_requirements(cls):
return ([
requirements.ModuleRequirement(name = "kernel", description = "Linux kernel module"),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.IntRequirement(name = 'pid', description = "Process ID", optional = True)
])
return [
requirements.ModuleRequirement(
name="kernel", description="Linux kernel module"
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
def change_task(self, pid = None):
def change_task(self, pid=None):
"""Change the current process and layer, based on a process ID"""
tasks = self.list_tasks()
for task in tasks:
@@ -42,17 +48,21 @@ class Volshell(generic.Volshell):
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(['ct', 'change_task', 'cp'], self.change_task),
(['lt', 'list_tasks', 'ps'], self.list_tasks),
(['symbols'], self.context.symbol_space[self.current_symbol_table]),
(["ct", "change_task", "cp"], self.change_task),
(["lt", "list_tasks", "ps"], self.list_tasks),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get('pid', None) is not None:
self.change_task(self.config['pid'])
if self.config.get("pid", None) is not None:
self.change_task(self.config["pid"])
return result
def display_type(self,
object: Union[str, interfaces.objects.ObjectInterface, interfaces.objects.Template],
offset: int = None):
def display_type(
self,
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
if constants.BANG not in object:
+28 -16
View File
@@ -15,13 +15,19 @@ class Volshell(generic.Volshell):
@classmethod
def get_requirements(cls):
return ([
requirements.ModuleRequirement(name = "kernel", description = "Darwin kernel module"),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)),
requirements.IntRequirement(name = 'pid', description = "Process ID", optional = True)
])
return [
requirements.ModuleRequirement(
name="kernel", description="Darwin kernel module"
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
def change_task(self, pid = None):
def change_task(self, pid=None):
"""Change the current process and layer, based on a process ID"""
tasks = self.list_tasks()
for task in tasks:
@@ -34,25 +40,31 @@ class Volshell(generic.Volshell):
return
print(f"No task with task ID {pid} found")
def list_tasks(self, method = None):
def list_tasks(self, method=None):
"""Returns a list of task objects from the primary layer"""
# We always use the main kernel memory and associated symbols
return list(pslist.PsList.get_list_tasks(method)(self.context, self.current_kernel_name))
return list(
pslist.PsList.get_list_tasks(method)(self.context, self.current_kernel_name)
)
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(['ct', 'change_task', 'cp'], self.change_task),
(['lt', 'list_tasks', 'ps'], self.list_tasks),
(['symbols'], self.context.symbol_space[self.current_symbol_table]),
(["ct", "change_task", "cp"], self.change_task),
(["lt", "list_tasks", "ps"], self.list_tasks),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get('pid', None) is not None:
self.change_task(self.config['pid'])
if self.config.get("pid", None) is not None:
self.change_task(self.config["pid"])
return result
def display_type(self,
object: Union[str, interfaces.objects.ObjectInterface, interfaces.objects.Template],
offset: int = None):
def display_type(
self,
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
if constants.BANG not in object:
+27 -15
View File
@@ -15,13 +15,17 @@ class Volshell(generic.Volshell):
@classmethod
def get_requirements(cls):
return ([
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel'),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.IntRequirement(name = 'pid', description = "Process ID", optional = True)
])
return [
requirements.ModuleRequirement(name="kernel", description="Windows kernel"),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.IntRequirement(
name="pid", description="Process ID", optional=True
),
]
def change_process(self, pid = None):
def change_process(self, pid=None):
"""Change the current process and layer, based on a process ID"""
processes = self.list_processes()
for process in processes:
@@ -34,22 +38,30 @@ class Volshell(generic.Volshell):
def list_processes(self):
"""Returns a list of EPROCESS objects from the primary layer"""
# We always use the main kernel memory and associated symbols
return list(pslist.PsList.list_processes(self.context, self.current_layer, self.current_symbol_table))
return list(
pslist.PsList.list_processes(
self.context, self.current_layer, self.current_symbol_table
)
)
def construct_locals(self) -> List[Tuple[List[str], Any]]:
result = super().construct_locals()
result += [
(['cp', 'change_process'], self.change_process),
(['lp', 'list_processes', 'ps'], self.list_processes),
(['symbols'], self.context.symbol_space[self.current_symbol_table]),
(["cp", "change_process"], self.change_process),
(["lp", "list_processes", "ps"], self.list_processes),
(["symbols"], self.context.symbol_space[self.current_symbol_table]),
]
if self.config.get('pid', None) is not None:
self.change_process(self.config['pid'])
if self.config.get("pid", None) is not None:
self.change_process(self.config["pid"])
return result
def display_type(self,
object: Union[str, interfaces.objects.ObjectInterface, interfaces.objects.Template],
offset: int = None):
def display_type(
self,
object: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
offset: int = None,
):
"""Display Type describes the members of a particular object in alphabetical order"""
if isinstance(object, str):
if constants.BANG not in object:
+69 -33
View File
@@ -7,11 +7,20 @@ import glob
import sys
import zipfile
required_python_version = (3, 6, 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])):
required_python_version = (3, 7, 0)
if (
sys.version_info.major != required_python_version[0]
or sys.version_info.minor < required_python_version[1]
or (
sys.version_info.minor == required_python_version[1]
and sys.version_info.micro < required_python_version[2]
)
):
raise RuntimeError(
"Volatility framework requires python version {}.{}.{} or greater".format(*required_python_version))
"Volatility framework requires python version {}.{}.{} or greater".format(
*required_python_version
)
)
import importlib
import inspect
@@ -45,24 +54,29 @@ def require_interface_version(*args) -> None:
"""Checks the required version of a plugin."""
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]))
raise RuntimeError(
"Framework interface version {} is incompatible with required version {}".format(
interface_version()[0], 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):
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:
if hasattr(self.default_value, '__get__'):
if hasattr(self.default_value, "__get__"):
return self.default_value.__get__(obj, get_type)
return self.default_value
raise AttributeError
@@ -73,7 +87,7 @@ def hide_from_subclasses(cls: Type) -> Type:
return cls
T = TypeVar('T')
T = TypeVar("T")
def class_subclasses(cls: Type[T]) -> Generator[Type[T], None, None]:
@@ -82,7 +96,7 @@ def class_subclasses(cls: Type[T]) -> Generator[Type[T], None, None]:
raise TypeError(f"class_subclasses parameter not a valid class: {cls}")
for clazz in cls.__subclasses__():
# 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
if not hasattr(clazz, "hidden") or not clazz.hidden: # type: ignore
yield clazz
for return_value in class_subclasses(clazz):
yield return_value
@@ -93,10 +107,12 @@ def import_files(base_module, ignore_errors: bool = False) -> List[str]:
failures = []
if not isinstance(base_module.__path__, list):
raise TypeError("[base_module].__path__ must be a list of paths")
vollog.log(constants.LOGLEVEL_VVVV,
f"Importing from the following paths: {', '.join(base_module.__path__)}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"Importing from the following paths: {', '.join(base_module.__path__)}",
)
for path in base_module.__path__:
for root, _, files in os.walk(path, followlinks = True):
for root, _, files in os.walk(path, followlinks=True):
# TODO: Figure out how to import pycache files
if root.endswith("__pycache__"):
continue
@@ -104,35 +120,51 @@ def import_files(base_module, ignore_errors: bool = False) -> List[str]:
if zipfile.is_zipfile(os.path.join(root, filename)):
# Use the root to add this to the module path, and sub-traverse the files
new_module = base_module
premodules = root[len(path) + len(os.path.sep):].replace(os.path.sep, '.')
for component in premodules.split('.'):
premodules = root[len(path) + len(os.path.sep) :].replace(
os.path.sep, "."
)
for component in premodules.split("."):
if component:
try:
new_module = getattr(new_module, component)
except AttributeError:
failures += [new_module + '.' + component]
new_module.__path__ = [os.path.join(root, filename)] + new_module.__path__
failures += [new_module + "." + component]
new_module.__path__ = [
os.path.join(root, filename)
] + new_module.__path__
for ziproot, zipfiles in _zipwalk(os.path.join(root, filename)):
for zfile in zipfiles:
if _filter_files(zfile):
submodule = zfile[:zfile.rfind('.')].replace(os.path.sep, '.')
failures += import_file(new_module.__name__ + '.' + submodule,
os.path.join(path, ziproot, zfile))
submodule = zfile[: zfile.rfind(".")].replace(
os.path.sep, "."
)
failures += import_file(
new_module.__name__ + "." + submodule,
os.path.join(path, ziproot, zfile),
)
else:
if _filter_files(filename):
modpath = os.path.join(root[len(path) + len(os.path.sep):], filename[:filename.rfind(".")])
modpath = os.path.join(
root[len(path) + len(os.path.sep) :],
filename[: filename.rfind(".")],
)
submodule = modpath.replace(os.path.sep, ".")
failures += import_file(base_module.__name__ + '.' + submodule,
os.path.join(root, filename),
ignore_errors)
failures += import_file(
base_module.__name__ + "." + submodule,
os.path.join(root, filename),
ignore_errors,
)
return failures
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")
or filename.endswith(".pyc")
or filename.endswith(".pyo")
) and not filename.startswith("__")
def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str]:
@@ -152,7 +184,9 @@ def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str
importlib.import_module(module)
except ImportError as e:
vollog.debug(str(e))
vollog.debug("Failed to import module {} based on file: {}".format(module, path))
vollog.debug(
"Failed to import module {} based on file: {}".format(module, path)
)
failures.append(module)
if not ignore_errors:
raise
@@ -167,7 +201,9 @@ def _zipwalk(path: str):
if not file.is_dir():
dirlist = zip_results.get(os.path.dirname(file.filename), [])
dirlist.append(os.path.basename(file.filename))
zip_results[os.path.join(path, os.path.dirname(file.filename))] = dirlist
zip_results[
os.path.join(path, os.path.dirname(file.filename))
] = dirlist
for value in zip_results:
yield value, zip_results[value]
@@ -177,14 +213,14 @@ def list_plugins() -> Dict[str, Type[interfaces.plugins.PluginInterface]]:
for plugin in class_subclasses(interfaces.plugins.PluginInterface):
plugin_name = plugin.__module__ + "." + plugin.__name__
if plugin_name.startswith("volatility3.plugins."):
plugin_name = plugin_name[len("volatility3.plugins."):]
plugin_name = plugin_name[len("volatility3.plugins.") :]
plugin_list[plugin_name] = plugin
return plugin_list
def clear_cache(complete = False):
glob_pattern = '*.cache'
def clear_cache(complete=False):
glob_pattern = "*.cache"
if not complete:
glob_pattern = 'data_' + glob_pattern
glob_pattern = "data_" + glob_pattern
for cache_filename in glob.glob(os.path.join(constants.CACHE_PATH, glob_pattern)):
os.unlink(cache_filename)
+33 -19
View File
@@ -22,7 +22,9 @@ from volatility3.framework.configuration import requirements
vollog = logging.getLogger(__name__)
def available(context: interfaces.context.ContextInterface) -> List[interfaces.automagic.AutomagicInterface]:
def available(
context: interfaces.context.ContextInterface,
) -> List[interfaces.automagic.AutomagicInterface]:
"""Returns an ordered list of all subclasses of
:class:`~volatility3.framework.interfaces.automagic.AutomagicInterface`.
@@ -34,21 +36,26 @@ def available(context: interfaces.context.ContextInterface) -> List[interfaces.a
"""
import_files(sys.modules[__name__])
config_path = constants.AUTOMAGIC_CONFIG_PATH
return sorted([
clazz(context, interfaces.configuration.path_join(config_path, clazz.__name__))
for clazz in class_subclasses(interfaces.automagic.AutomagicInterface)
],
key = lambda x: x.priority)
return sorted(
[
clazz(
context, interfaces.configuration.path_join(config_path, clazz.__name__)
)
for clazz in class_subclasses(interfaces.automagic.AutomagicInterface)
],
key=lambda x: x.priority,
)
def choose_automagic(
automagics: List[Type[interfaces.automagic.AutomagicInterface]],
plugin: Type[interfaces.plugins.PluginInterface]) -> List[Type[interfaces.automagic.AutomagicInterface]]:
automagics: List[Type[interfaces.automagic.AutomagicInterface]],
plugin: Type[interfaces.plugins.PluginInterface],
) -> List[Type[interfaces.automagic.AutomagicInterface]]:
"""Chooses which automagics to run, maintaining the order they were handed
in."""
plugin_category = "None"
plugin_categories = plugin.__module__.split('.')
plugin_categories = plugin.__module__.split(".")
lowest_index = len(plugin_categories)
for os in constants.OS_CATEGORIES:
try:
@@ -73,12 +80,16 @@ def choose_automagic(
return output
def run(automagics: List[interfaces.automagic.AutomagicInterface],
context: interfaces.context.ContextInterface,
configurable: Union[interfaces.configuration.ConfigurableInterface,
Type[interfaces.configuration.ConfigurableInterface]],
config_path: str,
progress_callback: constants.ProgressCallback = None) -> List[traceback.TracebackException]:
def run(
automagics: List[interfaces.automagic.AutomagicInterface],
context: interfaces.context.ContextInterface,
configurable: Union[
interfaces.configuration.ConfigurableInterface,
Type[interfaces.configuration.ConfigurableInterface],
],
config_path: str,
progress_callback: constants.ProgressCallback = None,
) -> List[traceback.TracebackException]:
"""Runs through the list of `automagics` in order, allowing them to make
changes to the context.
@@ -99,10 +110,13 @@ def run(automagics: List[interfaces.automagic.AutomagicInterface],
"""
for automagic in automagics:
if not isinstance(automagic, interfaces.automagic.AutomagicInterface):
raise TypeError("Automagics must only contain AutomagicInterface subclasses")
raise TypeError(
"Automagics must only contain AutomagicInterface subclasses"
)
if (not isinstance(configurable, interfaces.configuration.ConfigurableInterface)
and not issubclass(configurable, interfaces.configuration.ConfigurableInterface)):
if not isinstance(
configurable, interfaces.configuration.ConfigurableInterface
) and not issubclass(configurable, interfaces.configuration.ConfigurableInterface):
raise TypeError("Automagic operates on configurables only")
# TODO: Fix need for top level config element just because we're using a MultiRequirement to group the
@@ -112,7 +126,7 @@ def run(automagics: List[interfaces.automagic.AutomagicInterface],
configurable_class = configurable.__class__
else:
configurable_class = configurable
requirement = requirements.MultiRequirement(name = configurable_class.__name__)
requirement = requirements.MultiRequirement(name=configurable_class.__name__)
for req in configurable.get_requirements():
requirement.add_requirement(req)
@@ -25,39 +25,60 @@ class ConstructionMagic(interfaces.automagic.AutomagicInterface):
:warning: This `automagic` should run first to allow existing configurations to have been constructed for use by later automagic
"""
priority = 0
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback = None,
optional = False) -> List[str]:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback=None,
optional=False,
) -> List[str]:
# Make sure we import the layers, so they can reconstructed
framework.import_files(sys.modules['volatility3.framework.layers'])
framework.import_files(sys.modules["volatility3.framework.layers"])
result: List[str] = []
if requirement.unsatisfied(context, config_path):
# Having called validate at the top level tells us both that we need to dig deeper
# but also ensures that TranslationLayerRequirements have got the correct subrequirements if their class is populated
subreq_config_path = interfaces.configuration.path_join(config_path, requirement.name)
subreq_config_path = interfaces.configuration.path_join(
config_path, requirement.name
)
for subreq in requirement.requirements.values():
try:
self(context, subreq_config_path, subreq, optional = optional or subreq.optional)
self(
context,
subreq_config_path,
subreq,
optional=optional or subreq.optional,
)
except Exception as e:
# We don't really care if this fails, it tends to mean the configuration isn't complete for that item
vollog.log(constants.LOGLEVEL_VVVV, f"Construction Exception occurred: {e}")
vollog.log(
constants.LOGLEVEL_VVVV, f"Construction Exception occurred: {e}"
)
invalid = subreq.unsatisfied(context, subreq_config_path)
# We want to traverse optional paths, so don't check until we've tried to validate
# We also don't want to emit a debug message when a parent is optional, hence the optional parameter
if invalid and not (optional or subreq.optional):
vollog.log(constants.LOGLEVEL_V, f"Failed on requirement: {subreq_config_path}")
result.append(interfaces.configuration.path_join(subreq_config_path, subreq.name))
vollog.log(
constants.LOGLEVEL_V,
f"Failed on requirement: {subreq_config_path}",
)
result.append(
interfaces.configuration.path_join(
subreq_config_path, subreq.name
)
)
if result:
return result
elif isinstance(requirement, interfaces.configuration.ConstructableRequirementInterface):
elif isinstance(
requirement, interfaces.configuration.ConstructableRequirementInterface
):
# We know all the subrequirements are filled, so let's populate
requirement.construct(context, config_path)
+98 -59
View File
@@ -17,19 +17,24 @@ vollog = logging.getLogger(__name__)
class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 35
exclusion_list = ['mac', 'windows']
exclusion_list = ["mac", "windows"]
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempts to identify linux within this layer."""
# Version check the SQlite cache
required = (1, 0, 0)
if not requirements.VersionRequirement.matches_required(required, symbol_cache.SqliteCache.version):
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}")
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
)
return None
# Bail out by default unless we can stack properly
@@ -41,56 +46,70 @@ 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.SqliteCache(identifiers_path).get_identifier_dictionary(
operating_system = 'linux')
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
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("No Linux banners found - if this is a linux plugin, please check your symbol files location")
vollog.info(
"No Linux banners found - if this is a linux plugin, please check your symbol files location"
)
return None
mss = scanners.MultiStringScanner([x for x in linux_banners if x is not None])
for _, banner in layer.scan(context = context, scanner = mss, progress_callback = progress_callback):
for _, banner in layer.scan(
context=context, scanner=mss, progress_callback=progress_callback
):
dtb = None
vollog.debug(f"Identified banner: {repr(banner)}")
isf_path = linux_banners.get(banner, None)
if isf_path:
table_name = context.symbol_space.free_table_name('LintelStacker')
table = linux.LinuxKernelIntermedSymbols(context,
'temporary.' + table_name,
name = table_name,
isf_url = isf_path)
table_name = context.symbol_space.free_table_name("LintelStacker")
table = linux.LinuxKernelIntermedSymbols(
context,
"temporary." + table_name,
name=table_name,
isf_url=isf_path,
)
context.symbol_space.append(table)
kaslr_shift, aslr_shift = cls.find_aslr(context,
table_name,
layer_name,
progress_callback = progress_callback)
kaslr_shift, aslr_shift = cls.find_aslr(
context, table_name, layer_name, progress_callback=progress_callback
)
layer_class: Type = intel.Intel
if 'init_top_pgt' in table.symbols:
if "init_top_pgt" in table.symbols:
layer_class = intel.Intel32e
dtb_symbol_name = 'init_top_pgt'
elif 'init_level4_pgt' in table.symbols:
dtb_symbol_name = "init_top_pgt"
elif "init_level4_pgt" in table.symbols:
layer_class = intel.Intel32e
dtb_symbol_name = 'init_level4_pgt'
dtb_symbol_name = "init_level4_pgt"
else:
dtb_symbol_name = 'swapper_pg_dir'
dtb_symbol_name = "swapper_pg_dir"
dtb = cls.virtual_to_physical_address(table.get_symbol(dtb_symbol_name).address + kaslr_shift)
dtb = cls.virtual_to_physical_address(
table.get_symbol(dtb_symbol_name).address + kaslr_shift
)
# Build the new layer
new_layer_name = context.layers.free_layer_name("IntelLayer")
config_path = join("IntelHelper", new_layer_name)
context.config[join(config_path, "memory_layer")] = layer_name
context.config[join(config_path, "page_map_offset")] = dtb
context.config[join(config_path, LinuxSymbolFinder.banner_config_key)] = str(banner, 'latin-1')
context.config[
join(config_path, LinuxSymbolFinder.banner_config_key)
] = str(banner, "latin-1")
layer = layer_class(context,
config_path = config_path,
name = new_layer_name,
metadata = {'os': 'Linux'})
layer.config['kernel_virtual_offset'] = aslr_shift
layer = layer_class(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "Linux"},
)
layer.config["kernel_virtual_offset"] = aslr_shift
if layer and dtb:
vollog.debug(f"DTB was found at: 0x{dtb:0x}")
@@ -99,43 +118,63 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
return None
@classmethod
def find_aslr(cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
layer_name: str,
progress_callback: constants.ProgressCallback = None) \
-> Tuple[int, int]:
def find_aslr(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Tuple[int, int]:
"""Determines the offset of the actual DTB in physical space and its
symbol offset."""
init_task_symbol = symbol_table + constants.BANG + 'init_task'
init_task_json_address = context.symbol_space.get_symbol(init_task_symbol).address
init_task_symbol = symbol_table + constants.BANG + "init_task"
init_task_json_address = context.symbol_space.get_symbol(
init_task_symbol
).address
swapper_signature = rb"swapper(\/0|\x00\x00)\x00\x00\x00\x00\x00\x00"
module = context.module(symbol_table, layer_name, 0)
address_mask = context.symbol_space[symbol_table].config.get('symbol_mask', None)
address_mask = context.symbol_space[symbol_table].config.get(
"symbol_mask", None
)
task_symbol = module.get_type('task_struct')
comm_child_offset = task_symbol.relative_child_offset('comm')
task_symbol = module.get_type("task_struct")
comm_child_offset = task_symbol.relative_child_offset("comm")
for offset in context.layers[layer_name].scan(scanner = scanners.RegExScanner(swapper_signature),
context = context,
progress_callback = progress_callback):
for offset in context.layers[layer_name].scan(
scanner=scanners.RegExScanner(swapper_signature),
context=context,
progress_callback=progress_callback,
):
init_task_address = offset - comm_child_offset
init_task = module.object(object_type = 'task_struct', offset = init_task_address, absolute = True)
init_task = module.object(
object_type="task_struct", offset=init_task_address, absolute=True
)
if init_task.pid != 0:
continue
elif init_task.has_member('state') and init_task.state.cast('unsigned int') != 0:
elif (
init_task.has_member("state")
and init_task.state.cast("unsigned int") != 0
):
continue
# This we get for free
aslr_shift = init_task.files.cast('long unsigned int') - module.get_symbol('init_files').address
kaslr_shift = init_task_address - cls.virtual_to_physical_address(init_task_json_address)
aslr_shift = (
init_task.files.cast("long unsigned int")
- module.get_symbol("init_files").address
)
kaslr_shift = init_task_address - cls.virtual_to_physical_address(
init_task_json_address
)
if address_mask:
aslr_shift = aslr_shift & address_mask
if aslr_shift & 0xfff != 0 or kaslr_shift & 0xfff != 0:
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))
vollog.debug(
"Linux ASLR shift values determined: physical {:0x} virtual {:0x}".format(
kaslr_shift, aslr_shift
)
)
return kaslr_shift, aslr_shift
# We don't throw an exception, because we may legitimately not have an ASLR shift, but we report it
@@ -146,16 +185,16 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
def virtual_to_physical_address(cls, addr: int) -> int:
"""Converts a virtual linux address to a physical one (does not account
of ASLR)"""
if addr > 0xffffffff80000000:
return addr - 0xffffffff80000000
return addr - 0xc0000000
if addr > 0xFFFFFFFF80000000:
return addr - 0xFFFFFFFF80000000
return addr - 0xC0000000
class LinuxSymbolFinder(symbol_finder.SymbolFinder):
"""Linux symbol loader based on uname signature strings."""
banner_config_key = "kernel_banner"
operating_system = 'linux'
operating_system = "linux"
symbol_class = "volatility3.framework.symbols.linux.LinuxKernelIntermedSymbols"
find_aslr = lambda cls, *args: LinuxIntelStacker.find_aslr(*args)[1]
exclusion_list = ['mac', 'windows']
exclusion_list = ["mac", "windows"]
+122 -68
View File
@@ -18,19 +18,24 @@ vollog = logging.getLogger(__name__)
class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 35
exclusion_list = ['windows', 'linux']
exclusion_list = ["windows", "linux"]
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
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):
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}")
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
)
return None
# Bail out by default unless we can stack properly
@@ -43,56 +48,76 @@ 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.SqliteCache(identifiers_path).get_identifier_dictionary(
operating_system = 'mac')
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
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("No Mac banners found - if this is a mac plugin, please check your symbol files location")
vollog.info(
"No Mac banners found - if this is a mac plugin, please check your symbol files location"
)
return None
mss = scanners.MultiStringScanner([x for x in mac_banners if x])
for banner_offset, banner in layer.scan(context = context, scanner = mss,
progress_callback = progress_callback):
for banner_offset, banner in layer.scan(
context=context, scanner=mss, progress_callback=progress_callback
):
dtb = None
vollog.debug(f"Identified banner: {repr(banner)}")
isf_path = mac_banners.get(banner, None)
if isf_path:
table_name = context.symbol_space.free_table_name('MacintelStacker')
table = mac.MacKernelIntermedSymbols(context = context,
config_path = join('temporary', table_name),
name = table_name,
isf_url = isf_path)
table_name = context.symbol_space.free_table_name("MacintelStacker")
table = mac.MacKernelIntermedSymbols(
context=context,
config_path=join("temporary", table_name),
name=table_name,
isf_url=isf_path,
)
context.symbol_space.append(table)
kaslr_shift = cls.find_aslr(context = context,
symbol_table = table_name,
layer_name = layer_name,
compare_banner = banner,
compare_banner_offset = banner_offset,
progress_callback = progress_callback)
kaslr_shift = cls.find_aslr(
context=context,
symbol_table=table_name,
layer_name=layer_name,
compare_banner=banner,
compare_banner_offset=banner_offset,
progress_callback=progress_callback,
)
if kaslr_shift == 0:
vollog.log(constants.LOGLEVEL_VVV, f"Invalid kalsr_shift found at offset: {banner_offset}")
vollog.log(
constants.LOGLEVEL_VVV,
f"Invalid kalsr_shift found at offset: {banner_offset}",
)
continue
bootpml4_addr = cls.virtual_to_physical_address(table.get_symbol("BootPML4").address + kaslr_shift)
bootpml4_addr = cls.virtual_to_physical_address(
table.get_symbol("BootPML4").address + kaslr_shift
)
new_layer_name = context.layers.free_layer_name("MacDTBTempLayer")
config_path = join("automagic", "MacIntelHelper", new_layer_name)
context.config[join(config_path, "memory_layer")] = layer_name
context.config[join(config_path, "page_map_offset")] = bootpml4_addr
layer = layers.intel.Intel32e(context,
config_path = config_path,
name = new_layer_name,
metadata = {'os': 'Mac'})
layer = layers.intel.Intel32e(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "Mac"},
)
idlepml4_ptr = table.get_symbol("IdlePML4").address + kaslr_shift
try:
idlepml4_str = layer.read(idlepml4_ptr, 4)
except exceptions.InvalidAddressException:
vollog.log(constants.LOGLEVEL_VVVV, f"Skipping invalid idlepml4_ptr: 0x{idlepml4_ptr:0x}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"Skipping invalid idlepml4_ptr: 0x{idlepml4_ptr:0x}",
)
continue
idlepml4_addr = struct.unpack("<I", idlepml4_str)[0]
@@ -100,7 +125,10 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
tmp_dtb = idlepml4_addr
if tmp_dtb % 4096:
vollog.log(constants.LOGLEVEL_VVV, f"Skipping non-page aligned DTB: 0x{tmp_dtb:0x}")
vollog.log(
constants.LOGLEVEL_VVV,
f"Skipping non-page aligned DTB: 0x{tmp_dtb:0x}",
)
continue
dtb = tmp_dtb
@@ -110,13 +138,17 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
config_path = join("automagic", "MacIntelHelper", new_layer_name)
context.config[join(config_path, "memory_layer")] = layer_name
context.config[join(config_path, "page_map_offset")] = dtb
context.config[join(config_path, MacSymbolFinder.banner_config_key)] = str(banner, 'latin-1')
context.config[
join(config_path, MacSymbolFinder.banner_config_key)
] = str(banner, "latin-1")
new_layer = intel.Intel32e(context,
config_path = config_path,
name = new_layer_name,
metadata = {'os': 'mac'})
new_layer.config['kernel_virtual_offset'] = kaslr_shift
new_layer = intel.Intel32e(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "mac"},
)
new_layer.config["kernel_virtual_offset"] = kaslr_shift
if new_layer and dtb:
vollog.debug(f"DTB was found at: 0x{dtb:0x}")
@@ -125,28 +157,40 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
return None
@classmethod
def find_aslr(cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
layer_name: str,
compare_banner: str = "",
compare_banner_offset: int = 0,
progress_callback: constants.ProgressCallback = None) -> int:
def find_aslr(
cls,
context: interfaces.context.ContextInterface,
symbol_table: str,
layer_name: str,
compare_banner: str = "",
compare_banner_offset: int = 0,
progress_callback: constants.ProgressCallback = None,
) -> int:
"""Determines the offset of the actual DTB in physical space and its
symbol offset."""
version_symbol = symbol_table + constants.BANG + 'version'
version_symbol = symbol_table + constants.BANG + "version"
version_json_address = context.symbol_space.get_symbol(version_symbol).address
version_major_symbol = symbol_table + constants.BANG + 'version_major'
version_major_json_address = context.symbol_space.get_symbol(version_major_symbol).address
version_major_phys_offset = cls.virtual_to_physical_address(version_major_json_address)
version_major_symbol = symbol_table + constants.BANG + "version_major"
version_major_json_address = context.symbol_space.get_symbol(
version_major_symbol
).address
version_major_phys_offset = cls.virtual_to_physical_address(
version_major_json_address
)
version_minor_symbol = symbol_table + constants.BANG + 'version_minor'
version_minor_json_address = context.symbol_space.get_symbol(version_minor_symbol).address
version_minor_phys_offset = cls.virtual_to_physical_address(version_minor_json_address)
version_minor_symbol = symbol_table + constants.BANG + "version_minor"
version_minor_json_address = context.symbol_space.get_symbol(
version_minor_symbol
).address
version_minor_phys_offset = cls.virtual_to_physical_address(
version_minor_json_address
)
if not compare_banner_offset or not compare_banner:
offset_generator = cls._scan_generator(context, layer_name, progress_callback)
offset_generator = cls._scan_generator(
context, layer_name, progress_callback
)
else:
offset_generator = [(compare_banner_offset, compare_banner)]
@@ -155,24 +199,30 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
for offset, banner in offset_generator:
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)
tmp_aslr_shift = offset - cls.virtual_to_physical_address(
version_json_address
)
major_string = context.layers[layer_name].read(version_major_phys_offset + tmp_aslr_shift, 4)
major_string = context.layers[layer_name].read(
version_major_phys_offset + tmp_aslr_shift, 4
)
major = struct.unpack("<I", major_string)[0]
if major != banner_major:
continue
minor_string = context.layers[layer_name].read(version_minor_phys_offset + tmp_aslr_shift, 4)
minor_string = context.layers[layer_name].read(
version_minor_phys_offset + tmp_aslr_shift, 4
)
minor = struct.unpack("<I", minor_string)[0]
if minor != banner_minor:
continue
if tmp_aslr_shift & 0xfff != 0:
if tmp_aslr_shift & 0xFFF != 0:
continue
aslr_shift = tmp_aslr_shift & 0xffffffff
aslr_shift = tmp_aslr_shift & 0xFFFFFFFF
break
vollog.log(constants.LOGLEVEL_VVVV, f"Mac find_aslr returned: {aslr_shift:0x}")
@@ -183,20 +233,24 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
def virtual_to_physical_address(cls, addr: int) -> int:
"""Converts a virtual mac address to a physical one (does not account
of ASLR)"""
if addr > 0xffffff8000000000:
addr = addr - 0xffffff8000000000
if addr > 0xFFFFFF8000000000:
addr = addr - 0xFFFFFF8000000000
else:
addr = addr - 0xff8000000000
addr = addr - 0xFF8000000000
return addr
@classmethod
def _scan_generator(cls, context, layer_name, progress_callback):
darwin_signature = rb"Darwin Kernel Version \d{1,3}\.\d{1,3}\.\d{1,3}: [^\x00]+\x00"
darwin_signature = (
rb"Darwin Kernel Version \d{1,3}\.\d{1,3}\.\d{1,3}: [^\x00]+\x00"
)
for offset in context.layers[layer_name].scan(scanner = scanners.RegExScanner(darwin_signature),
context = context,
progress_callback = progress_callback):
for offset in context.layers[layer_name].scan(
scanner=scanners.RegExScanner(darwin_signature),
context=context,
progress_callback=progress_callback,
):
banner = context.layers[layer_name].read(offset, 128)
@@ -210,8 +264,8 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
class MacSymbolFinder(symbol_finder.SymbolFinder):
"""Mac symbol loader based on uname signature strings."""
banner_config_key = 'kernel_banner'
operating_system = 'mac'
banner_config_key = "kernel_banner"
operating_system = "mac"
find_aslr = MacIntelStacker.find_aslr
symbol_class = "volatility3.framework.symbols.mac.MacKernelIntermedSymbols"
exclusion_list = ['windows', 'linux']
exclusion_list = ["windows", "linux"]
+33 -14
View File
@@ -10,36 +10,55 @@ class KernelModule(interfaces.automagic.AutomagicInterface):
priority = 100
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> None:
new_config_path = interfaces.configuration.path_join(config_path, requirement.name)
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> None:
new_config_path = interfaces.configuration.path_join(
config_path, requirement.name
)
if not isinstance(requirement, configuration.requirements.ModuleRequirement):
# Check subrequirements
for req in requirement.requirements:
self(context, new_config_path, requirement.requirements[req], progress_callback)
self(
context,
new_config_path,
requirement.requirements[req],
progress_callback,
)
return
if not requirement.unsatisfied(context, config_path):
return
# The requirement is unfulfilled and is a ModuleRequirement
context.config[interfaces.configuration.path_join(
new_config_path, 'class')] = 'volatility3.framework.contexts.Module'
context.config[
interfaces.configuration.path_join(new_config_path, "class")
] = "volatility3.framework.contexts.Module"
for req in requirement.requirements:
if requirement.requirements[req].unsatisfied(context, new_config_path) and req != 'offset':
if (
requirement.requirements[req].unsatisfied(context, new_config_path)
and req != "offset"
):
return
# We now just have the offset requirement, but the layer requirement has been fulfilled.
# Unfortunately we don't know the layer name requirement's exact name
for req in requirement.requirements:
if isinstance(requirement.requirements[req], configuration.requirements.TranslationLayerRequirement):
layer_kvo_config_path = interfaces.configuration.path_join(new_config_path, req,
'kernel_virtual_offset')
offset_config_path = interfaces.configuration.path_join(new_config_path, 'offset')
if isinstance(
requirement.requirements[req],
configuration.requirements.TranslationLayerRequirement,
):
layer_kvo_config_path = interfaces.configuration.path_join(
new_config_path, req, "kernel_virtual_offset"
)
offset_config_path = interfaces.configuration.path_join(
new_config_path, "offset"
)
offset = context.config[layer_kvo_config_path]
context.config[offset_config_path] = offset
+222 -121
View File
@@ -22,7 +22,9 @@ from volatility3.framework.symbols.windows.pdbutil import PDBUtility
if __name__ == "__main__":
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))))
sys.path.append(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
)
vollog = logging.getLogger(__name__)
@@ -43,12 +45,17 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
searches for a particular structure that lists the kernel module's virtual address, its size (not checked) and the
module's name. This value is then used if one was not found using the previous method.
"""
priority = 30
max_pdb_size = 0x400000
exclusion_list = ['linux', 'mac']
exclusion_list = ["linux", "mac"]
def find_virtual_layers_from_req(self, context: interfaces.context.ContextInterface, config_path: str,
requirement: interfaces.configuration.RequirementInterface) -> List[str]:
def find_virtual_layers_from_req(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
) -> List[str]:
"""Traverses the requirement tree, rooted at `requirement` looking for
virtual layers that might contain a windows PDB.
@@ -62,27 +69,36 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
Returns:
A list of (layer_name, scan_results)
"""
sub_config_path = interfaces.configuration.path_join(config_path, requirement.name)
sub_config_path = interfaces.configuration.path_join(
config_path, requirement.name
)
results: List[str] = []
if isinstance(requirement, requirements.TranslationLayerRequirement):
# Check for symbols in this layer
# FIXME: optionally allow a full (slow) scan
# FIXME: Determine the physical layer no matter the virtual layer
virtual_layer_name = context.config.get(sub_config_path, None)
layer_name = context.config.get(interfaces.configuration.path_join(sub_config_path, "memory_layer"), None)
layer_name = context.config.get(
interfaces.configuration.path_join(sub_config_path, "memory_layer"),
None,
)
if layer_name and virtual_layer_name:
memlayer = context.layers[virtual_layer_name]
if isinstance(memlayer, intel.Intel):
results = [virtual_layer_name]
else:
for subreq in requirement.requirements.values():
results += self.find_virtual_layers_from_req(context, sub_config_path, subreq)
results += self.find_virtual_layers_from_req(
context, sub_config_path, subreq
)
return results
def recurse_symbol_fulfiller(self,
context: interfaces.context.ContextInterface,
valid_kernel: ValidKernelType,
progress_callback: constants.ProgressCallback = None) -> None:
def recurse_symbol_fulfiller(
self,
context: interfaces.context.ContextInterface,
valid_kernel: ValidKernelType,
progress_callback: constants.ProgressCallback = None,
) -> None:
"""Fulfills the SymbolTableRequirements in `self._symbol_requirements`
found by the `recurse_symbol_requirements`.
@@ -99,22 +115,28 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
if valid_kernel:
# TODO: Check that the symbols for this kernel will fulfill the requirement
virtual_layer, _kvo, kernel = valid_kernel
if not isinstance(kernel['pdb_name'], str) or not isinstance(kernel['GUID'], str):
if not isinstance(kernel["pdb_name"], str) or not isinstance(
kernel["GUID"], str
):
raise TypeError("PDB name or GUID not a string value")
PDBUtility.load_windows_symbol_table(
context = context,
guid = kernel['GUID'],
age = kernel['age'],
pdb_name = kernel['pdb_name'],
symbol_table_class = "volatility3.framework.symbols.windows.WindowsKernelIntermedSymbols",
config_path = sub_config_path,
progress_callback = progress_callback)
context=context,
guid=kernel["GUID"],
age=kernel["age"],
pdb_name=kernel["pdb_name"],
symbol_table_class="volatility3.framework.symbols.windows.WindowsKernelIntermedSymbols",
config_path=sub_config_path,
progress_callback=progress_callback,
)
else:
vollog.debug("No suitable kernel pdb signature found")
def set_kernel_virtual_offset(self, context: interfaces.context.ContextInterface,
valid_kernel: ValidKernelType) -> None:
def set_kernel_virtual_offset(
self,
context: interfaces.context.ContextInterface,
valid_kernel: ValidKernelType,
) -> None:
"""Traverses the requirement tree, looking for kernel_virtual_offset
values that may need setting and sets it based on the previously
identified `valid_kernel`.
@@ -127,71 +149,98 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
# Set the virtual offset under the TranslationLayer it applies to
virtual_layer, kvo, kernel = valid_kernel
if kvo is not None:
kvo_path = interfaces.configuration.path_join(context.layers[virtual_layer].config_path,
'kernel_virtual_offset')
kvo_path = interfaces.configuration.path_join(
context.layers[virtual_layer].config_path, "kernel_virtual_offset"
)
context.config[kvo_path] = kvo
vollog.debug(f"Setting kernel_virtual_offset to {hex(kvo)}")
def get_physical_layer_name(self, context, vlayer):
return context.config.get(interfaces.configuration.path_join(vlayer.config_path, 'memory_layer'), None)
return context.config.get(
interfaces.configuration.path_join(vlayer.config_path, "memory_layer"), None
)
def method_slow_scan(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def test_virtual_kernel(physical_layer_name, virtual_layer_name: str, kernel: Dict[str, Any]) -> Optional[
ValidKernelType]:
def method_slow_scan(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
def test_virtual_kernel(
physical_layer_name, virtual_layer_name: str, kernel: Dict[str, Any]
) -> Optional[ValidKernelType]:
# It seems the kernel is loaded at a fixed mapping (presumably because the memory manager hasn't started yet)
if kernel['mz_offset'] is None or not isinstance(kernel['mz_offset'], int):
if kernel["mz_offset"] is None or not isinstance(kernel["mz_offset"], int):
# Rule out kernels that couldn't find a suitable MZ header
return None
return (virtual_layer_name, kernel['mz_offset'], kernel)
return (virtual_layer_name, kernel["mz_offset"], kernel)
vollog.debug("Kernel base determination - optimized scan virtual layer")
valid_kernel = self._method_layer_pdb_scan(context, vlayer, test_virtual_kernel, True, False, progress_callback)
valid_kernel = self._method_layer_pdb_scan(
context, vlayer, test_virtual_kernel, True, False, progress_callback
)
if valid_kernel is not None:
return valid_kernel
vollog.debug("Kernel base determination - slow scan virtual layer")
return self._method_layer_pdb_scan(context, vlayer, test_virtual_kernel, False, False, progress_callback)
return self._method_layer_pdb_scan(
context, vlayer, test_virtual_kernel, False, False, progress_callback
)
def method_fixed_mapping(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def test_physical_kernel(physical_layer_name: str, virtual_layer_name: str, kernel: Dict[str, Any]) -> Optional[
ValidKernelType]:
def method_fixed_mapping(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
def test_physical_kernel(
physical_layer_name: str, virtual_layer_name: str, kernel: Dict[str, Any]
) -> Optional[ValidKernelType]:
# It seems the kernel is loaded at a fixed mapping (presumably because the memory manager hasn't started yet)
if kernel['mz_offset'] is None or not isinstance(kernel['mz_offset'], int):
if kernel["mz_offset"] is None or not isinstance(kernel["mz_offset"], int):
# Rule out kernels that couldn't find a suitable MZ header
return None
if vlayer.bits_per_register == 64:
kvo = kernel['mz_offset'] + (31 << int(math.ceil(math.log2(vlayer.maximum_address + 1)) - 5))
kvo = kernel["mz_offset"] + (
31 << int(math.ceil(math.log2(vlayer.maximum_address + 1)) - 5)
)
else:
kvo = kernel['mz_offset'] + (1 << (vlayer.bits_per_register - 1))
kvo = kernel["mz_offset"] + (1 << (vlayer.bits_per_register - 1))
try:
kvp = vlayer.mapping(kvo, 0)
if (any([(p == kernel['mz_offset'] and layer_name == physical_layer_name)
for (_, _, p, _, layer_name) in kvp])):
if any(
[
(p == kernel["mz_offset"] and layer_name == physical_layer_name)
for (_, _, p, _, layer_name) in kvp
]
):
return (virtual_layer_name, kvo, kernel)
else:
vollog.debug("Potential kernel_virtual_offset did not map to expected location: {}".format(
hex(kvo)))
vollog.debug(
"Potential kernel_virtual_offset did not map to expected location: {}".format(
hex(kvo)
)
)
except exceptions.InvalidAddressException:
vollog.debug(f"Potential kernel_virtual_offset caused a page fault: {hex(kvo)}")
vollog.debug(
f"Potential kernel_virtual_offset caused a page fault: {hex(kvo)}"
)
return None
vollog.debug("Kernel base determination - testing fixed base address")
return self._method_layer_pdb_scan(context, vlayer, test_physical_kernel, False, True, progress_callback)
return self._method_layer_pdb_scan(
context, vlayer, test_physical_kernel, False, True, progress_callback
)
def _method_layer_pdb_scan(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
test_kernel: Callable,
optimized: bool = False,
physical: bool = True,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def _method_layer_pdb_scan(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
test_kernel: Callable,
optimized: bool = False,
physical: bool = True,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
# TODO: Verify this is a windows image
valid_kernel = None
virtual_layer_name = vlayer.name
@@ -202,102 +251,145 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
layer_to_scan = virtual_layer_name
start_scan_address = 0
if optimized and not physical and context.layers[layer_to_scan].metadata.architecture in ["Intel64"]:
if (
optimized
and not physical
and context.layers[layer_to_scan].metadata.architecture in ["Intel64"]
):
# TODO: change this value accordingly when 5-Level paging is supported.
start_scan_address = (0x1f0 << 39)
start_scan_address = 0x1F0 << 39
kernel_pdb_names = [bytes(name + ".pdb", "utf-8") for name in constants.windows.KERNEL_MODULE_NAMES]
kernels = PDBUtility.pdbname_scan(ctx = context,
layer_name = layer_to_scan,
start = start_scan_address,
page_size = vlayer.page_size,
pdb_names = kernel_pdb_names,
progress_callback = progress_callback)
kernel_pdb_names = [
bytes(name + ".pdb", "utf-8")
for name in constants.windows.KERNEL_MODULE_NAMES
]
kernels = PDBUtility.pdbname_scan(
ctx=context,
layer_name=layer_to_scan,
start=start_scan_address,
page_size=vlayer.page_size,
pdb_names=kernel_pdb_names,
progress_callback=progress_callback,
)
for kernel in kernels:
valid_kernel = test_kernel(physical_layer_name, virtual_layer_name, kernel)
if valid_kernel is not None:
break
return valid_kernel
def _method_offset(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
pattern: bytes,
result_offset: int,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def _method_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
pattern: bytes,
result_offset: int,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
"""Method for finding a suitable kernel offset based on a module
table."""
vollog.debug("Kernel base determination - searching layer module list structure")
vollog.debug(
"Kernel base determination - searching layer module list structure"
)
valid_kernel: Optional[ValidKernelType] = None
# If we're here, chances are high we're in a Win10 x64 image with kernel base randomization
physical_layer_name = self.get_physical_layer_name(context, vlayer)
physical_layer = context.layers[physical_layer_name]
# TODO: On older windows, this might be \WINDOWS\system32\nt rather than \SystemRoot\system32\nt
results = physical_layer.scan(context, scanners.BytesScanner(pattern), progress_callback = progress_callback)
results = physical_layer.scan(
context, scanners.BytesScanner(pattern), progress_callback=progress_callback
)
seen: Set[int] = set()
# Because this will launch a scan of the virtual layer, we want to be careful
for result in results:
# TODO: Identify the specific structure we're finding and document this a bit better
pointer = context.object("pdbscan!unsigned long long",
offset = (result + result_offset),
layer_name = physical_layer_name)
pointer = context.object(
"pdbscan!unsigned long long",
offset=(result + result_offset),
layer_name=physical_layer_name,
)
address = pointer & vlayer.address_mask
if address in seen:
continue
seen.add(address)
valid_kernel = self.check_kernel_offset(context, vlayer, address, progress_callback)
valid_kernel = self.check_kernel_offset(
context, vlayer, address, progress_callback
)
if valid_kernel:
break
return valid_kernel
def method_module_offset(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
return self._method_offset(context, vlayer, b"\\SystemRoot\\system32\\nt",
-16 - int(vlayer.bits_per_register / 8), progress_callback)
def method_module_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
return self._method_offset(
context,
vlayer,
b"\\SystemRoot\\system32\\nt",
-16 - int(vlayer.bits_per_register / 8),
progress_callback,
)
def method_kdbg_offset(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def method_kdbg_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
return self._method_offset(context, vlayer, b"KDBG", 8, progress_callback)
def check_kernel_offset(self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
address: int,
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def check_kernel_offset(
self,
context: interfaces.context.ContextInterface,
vlayer: layers.intel.Intel,
address: int,
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
"""Scans a virtual address."""
# Scan a few megs of the virtual space at the location to see if they're potential kernels
valid_kernel: Optional[ValidKernelType] = None
kernel_pdb_names = [bytes(name + ".pdb", "utf-8") for name in constants.windows.KERNEL_MODULE_NAMES]
kernel_pdb_names = [
bytes(name + ".pdb", "utf-8")
for name in constants.windows.KERNEL_MODULE_NAMES
]
virtual_layer_name = vlayer.name
with contextlib.suppress(exceptions.InvalidAddressException):
if vlayer.read(address, 0x2) == b'MZ':
if vlayer.read(address, 0x2) == b"MZ":
res = list(
PDBUtility.pdbname_scan(ctx = context,
layer_name = vlayer.name,
page_size = vlayer.page_size,
pdb_names = kernel_pdb_names,
progress_callback = progress_callback,
start = address,
end = address + self.max_pdb_size))
PDBUtility.pdbname_scan(
ctx=context,
layer_name=vlayer.name,
page_size=vlayer.page_size,
pdb_names=kernel_pdb_names,
progress_callback=progress_callback,
start=address,
end=address + self.max_pdb_size,
)
)
if res:
valid_kernel = (virtual_layer_name, address, res[0])
return valid_kernel
# List of methods to be run, in order, to determine the valid kernels
methods = [method_kdbg_offset, method_module_offset, method_fixed_mapping, method_slow_scan]
methods = [
method_kdbg_offset,
method_module_offset,
method_fixed_mapping,
method_slow_scan,
]
def determine_valid_kernel(self,
context: interfaces.context.ContextInterface,
potential_layers: List[str],
progress_callback: constants.ProgressCallback = None) -> Optional[ValidKernelType]:
def determine_valid_kernel(
self,
context: interfaces.context.ContextInterface,
potential_layers: List[str],
progress_callback: constants.ProgressCallback = None,
) -> Optional[ValidKernelType]:
"""Runs through the identified potential kernels and verifies their
suitability.
@@ -326,27 +418,36 @@ class KernelPDBScanner(interfaces.automagic.AutomagicInterface):
vollog.info("No suitable kernels found during pdbscan")
return valid_kernel
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> None:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> None:
if requirement.unsatisfied(context, config_path):
if "pdbscan" not in context.symbol_space:
context.symbol_space.append(native.NativeTable("pdbscan", native.std_ctypes))
context.symbol_space.append(
native.NativeTable("pdbscan", native.std_ctypes)
)
# TODO: check if this is a windows symbol requirement, otherwise ignore it
self._symbol_requirements = self.find_requirements(context, config_path, requirement,
requirements.SymbolTableRequirement)
potential_layers = self.find_virtual_layers_from_req(context = context,
config_path = config_path,
requirement = requirement)
self._symbol_requirements = self.find_requirements(
context, config_path, requirement, requirements.SymbolTableRequirement
)
potential_layers = self.find_virtual_layers_from_req(
context=context, config_path=config_path, requirement=requirement
)
for sub_config_path, symbol_req in self._symbol_requirements:
parent_path = interfaces.configuration.parent_path(sub_config_path)
if symbol_req.unsatisfied(context, parent_path):
valid_kernel = self.determine_valid_kernel(context, potential_layers, progress_callback)
valid_kernel = self.determine_valid_kernel(
context, potential_layers, progress_callback
)
if valid_kernel:
self.set_kernel_virtual_offset(context, valid_kernel)
self.recurse_symbol_fulfiller(context, valid_kernel, progress_callback)
self.recurse_symbol_fulfiller(
context, valid_kernel, progress_callback
)
if progress_callback is not None:
progress_callback(100, "PDB scanning finished")
+119 -52
View File
@@ -35,6 +35,7 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
Upon completion it will re-call the :class:`~volatility3.framework.automagic.construct_layers.ConstructionMagic`,
so that any stacked layers are actually constructed and added to the context.
"""
# Most important automagic, must happen first!
priority = 10
@@ -42,14 +43,16 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
super().__init__(*args, **kwargs)
self._cached = None
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> Optional[List[str]]:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> Optional[List[str]]:
"""Runs the automagic over the configurable."""
framework.import_files(sys.modules['volatility3.framework.layers'])
framework.import_files(sys.modules["volatility3.framework.layers"])
# Quick exit if we're not needed
if not requirement.unsatisfied(context, config_path):
@@ -58,10 +61,14 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
# Bow out quickly if the UI hasn't provided a single_location
unsatisfied = self.unsatisfied(self.context, self.config_path)
if unsatisfied:
vollog.info(f"Unable to run LayerStacker, unsatisfied requirement: {unsatisfied}")
vollog.info(
f"Unable to run LayerStacker, unsatisfied requirement: {unsatisfied}"
)
return list(unsatisfied)
if not self.config or not self.config.get('single_location', None):
raise ValueError("Unable to run LayerStacker, single_location parameter not provided")
if not self.config or not self.config.get("single_location", None):
raise ValueError(
"Unable to run LayerStacker, single_location parameter not provided"
)
# Search for suitable requirements
self.stack(context, config_path, requirement, progress_callback)
@@ -70,9 +77,13 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
progress_callback(100, "Stacking attempts finished")
return None
def stack(self, context: interfaces.context.ContextInterface, config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback) -> None:
def stack(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback,
) -> None:
"""Stacks the various layers and attaches these to a specific
requirement.
@@ -85,7 +96,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
# If we're cached, find Now we need to find where to apply the stack configuration
if self._cached:
top_layer_name, subconfig = self._cached
result = self.find_suitable_requirements(context, config_path, requirement, [top_layer_name])
result = self.find_suitable_requirements(
context, config_path, requirement, [top_layer_name]
)
if result:
appropriate_config_path, layer_name = result
context.config.merge(appropriate_config_path, subconfig)
@@ -94,43 +107,65 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
self._cached = None
new_context = context.clone()
location = self.config.get('single_location', None)
location = self.config.get("single_location", None)
# Setup the local copy of the resource
current_layer_name = context.layers.free_layer_name("FileLayer")
current_config_path = interfaces.configuration.path_join(config_path, "stack", current_layer_name)
current_config_path = interfaces.configuration.path_join(
config_path, "stack", current_layer_name
)
# This must be specific to get us started, setup the config and run
new_context.config[interfaces.configuration.path_join(current_config_path, "location")] = location
physical_layer = physical.FileLayer(new_context, current_config_path, current_layer_name)
new_context.config[
interfaces.configuration.path_join(current_config_path, "location")
] = location
physical_layer = physical.FileLayer(
new_context, current_config_path, current_layer_name
)
new_context.add_layer(physical_layer)
stacked_layers = self.stack_layer(new_context, current_layer_name, self.create_stackers_list(),
progress_callback)
stacked_layers = self.stack_layer(
new_context,
current_layer_name,
self.create_stackers_list(),
progress_callback,
)
if stacked_layers is not None:
# Applies the stacked_layers to each requirement in the requirements list
result = self.find_suitable_requirements(new_context, config_path, requirement, stacked_layers)
result = self.find_suitable_requirements(
new_context, config_path, requirement, stacked_layers
)
if result:
path, layer = result
# splice in the new configuration into the original context
context.config.merge(path, new_context.layers[layer].build_configuration())
context.config.merge(
path, new_context.layers[layer].build_configuration()
)
# Call the construction magic now we may have new things to construct
constructor = construct_layers.ConstructionMagic(
context, interfaces.configuration.path_join(self.config_path, "ConstructionMagic"))
context,
interfaces.configuration.path_join(
self.config_path, "ConstructionMagic"
),
)
constructor(context, config_path, requirement)
# Stash the changed config items
self._cached = context.config.get(path, None), context.config.branch(path)
self._cached = context.config.get(path, None), context.config.branch(
path
)
vollog.debug(f"Stacked layers: {stacked_layers}")
@classmethod
def stack_layer(cls,
context: interfaces.context.ContextInterface,
initial_layer: str,
stack_set: List[Type[interfaces.automagic.StackerLayerInterface]] = None,
progress_callback: constants.ProgressCallback = None):
def stack_layer(
cls,
context: interfaces.context.ContextInterface,
initial_layer: str,
stack_set: 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.
WARNING: This modifies the context provided and may pollute it with unnecessary layers
@@ -154,11 +189,15 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
stacked = True
stacked_layers = [initial_layer]
if stack_set is None:
stack_set = list(framework.class_subclasses(interfaces.automagic.StackerLayerInterface))
stack_set = list(
framework.class_subclasses(interfaces.automagic.StackerLayerInterface)
)
for stacker_item in stack_set:
if not issubclass(stacker_item, interfaces.automagic.StackerLayerInterface):
raise TypeError(f"Stacker {stacker_item.__name__} is not a descendent of StackerLayerInterface")
raise TypeError(
f"Stacker {stacker_item.__name__} is not a descendent of StackerLayerInterface"
)
while stacked:
stacked = False
@@ -167,17 +206,27 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
for stacker_cls in stack_set:
stacker = stacker_cls()
try:
vollog.log(constants.LOGLEVEL_VV, f"Attempting to stack using {stacker_cls.__name__}")
vollog.log(
constants.LOGLEVEL_VV,
f"Attempting to stack using {stacker_cls.__name__}",
)
new_layer = stacker.stack(context, initial_layer, progress_callback)
if new_layer:
context.layers.add_layer(new_layer)
vollog.log(constants.LOGLEVEL_VV,
f"Stacked {new_layer.name} using {stacker_cls.__name__}")
vollog.log(
constants.LOGLEVEL_VV,
f"Stacked {new_layer.name} using {stacker_cls.__name__}",
)
break
except Exception as excp:
# Stacking exceptions are likely only of interest to developers, so the lowest level of logging
fulltrace = traceback.TracebackException.from_exception(excp).format(chain = True)
vollog.log(constants.LOGLEVEL_VVV, f"Exception during stacking: {str(excp)}")
fulltrace = traceback.TracebackException.from_exception(
excp
).format(chain=True)
vollog.log(
constants.LOGLEVEL_VVV,
f"Exception during stacking: {str(excp)}",
)
vollog.log(constants.LOGLEVEL_VVVV, "\n".join(fulltrace))
else:
stacked = False
@@ -188,11 +237,15 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
stack_set.remove(stacker_cls)
return stacked_layers
def create_stackers_list(self) -> List[Type[interfaces.automagic.StackerLayerInterface]]:
def create_stackers_list(
self,
) -> List[Type[interfaces.automagic.StackerLayerInterface]]:
"""Creates the list of stackers to use based on the config option"""
stack_set = sorted(framework.class_subclasses(interfaces.automagic.StackerLayerInterface),
key = lambda x: x.stack_order)
stacker_list = self.config.get('stackers', [])
stack_set = sorted(
framework.class_subclasses(interfaces.automagic.StackerLayerInterface),
key=lambda x: x.stack_order,
)
stacker_list = self.config.get("stackers", [])
if len(stacker_list):
result = []
for stacker in stack_set:
@@ -202,9 +255,13 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
return stack_set
@classmethod
def find_suitable_requirements(cls, context: interfaces.context.ContextInterface, config_path: str,
requirement: interfaces.configuration.RequirementInterface,
stacked_layers: List[str]) -> Optional[Tuple[str, str]]:
def find_suitable_requirements(
cls,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
stacked_layers: List[str],
) -> Optional[Tuple[str, str]]:
"""Looks for translation layer requirements and attempts to apply the
stacked layers to it. If it succeeds it returns the configuration path
and layer name where the stacked nodes were spliced into the tree.
@@ -213,7 +270,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
A tuple of a configuration path and layer name for the top of the stacked layers
or None if suitable requirements are not found
"""
child_config_path = interfaces.configuration.path_join(config_path, requirement.name)
child_config_path = interfaces.configuration.path_join(
config_path, requirement.name
)
if isinstance(requirement, requirements.TranslationLayerRequirement):
if requirement.unsatisfied(context, config_path):
original_setting = context.config.get(child_config_path, None)
@@ -229,7 +288,9 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
else:
return child_config_path, context.config.get(child_config_path, None)
for req_name, req in requirement.requirements.items():
result = cls.find_suitable_requirements(context, child_config_path, req, stacked_layers)
result = cls.find_suitable_requirements(
context, child_config_path, req, stacked_layers
)
if result:
return result
return None
@@ -238,23 +299,29 @@ class LayerStacker(interfaces.automagic.AutomagicInterface):
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# This is not optional for the stacker to run, so optional must be marked as False
return [
requirements.URIRequirement(name = "single_location",
description = "Specifies a base location on which to stack",
optional = True),
requirements.ListRequirement(name = "stackers", description = "List of stackers", optional = True)
requirements.URIRequirement(
name="single_location",
description="Specifies a base location on which to stack",
optional=True,
),
requirements.ListRequirement(
name="stackers", description="List of stackers", optional=True
),
]
def choose_os_stackers(plugin: Type[interfaces.plugins.PluginInterface]) -> List[str]:
"""Identifies the stackers that should be run, based on the plugin (and thus os) provided"""
plugin_first_level = plugin.__module__.split('.')[2]
plugin_first_level = plugin.__module__.split(".")[2]
# Ensure all stackers are loaded
framework.import_files(sys.modules['volatility3.framework.layers'])
framework.import_files(sys.modules["volatility3.framework.layers"])
result = []
for stacker in sorted(framework.class_subclasses(interfaces.automagic.StackerLayerInterface),
key = lambda x: x.stack_order):
for stacker in sorted(
framework.class_subclasses(interfaces.automagic.StackerLayerInterface),
key=lambda x: x.stack_order,
):
if plugin_first_level in stacker.exclusion_list:
continue
result.append(stacker.__name__)
+209 -89
View File
@@ -2,6 +2,7 @@
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import base64
import datetime
import json
import logging
import os
@@ -25,6 +26,7 @@ BannersType = Dict[bytes, List[str]]
### Identifiers
class IdentifierProcessor:
operating_system = None
@@ -39,47 +41,53 @@ class IdentifierProcessor:
Returns:
identifier is valid or None if not found
"""
raise NotImplementedError("This base class has no get_identifier method defined")
raise NotImplementedError(
"This base class has no get_identifier method defined"
)
class WindowsIdentifier(IdentifierProcessor):
operating_system = 'windows'
separator = '|'
operating_system = "windows"
separator = "|"
@classmethod
def get_identifier(cls, json) -> Optional[bytes]:
"""Returns the identifier for the file if one can be found"""
windows_metadata = json.get('metadata', {}).get('windows', {}).get('pdb', {})
windows_metadata = json.get("metadata", {}).get("windows", {}).get("pdb", {})
if windows_metadata:
guid = windows_metadata.get('GUID', None)
age = windows_metadata.get('age', None)
database = windows_metadata.get('database', None)
guid = windows_metadata.get("GUID", None)
age = windows_metadata.get("age", None)
database = windows_metadata.get("database", None)
if guid and age and database:
return cls.generate(database, guid, age)
return None
@classmethod
def generate(cls, pdb_name: str, guid: str, age: int) -> bytes:
return bytes(cls.separator.join([pdb_name, guid.upper(), str(age)]), 'latin-1')
return bytes(cls.separator.join([pdb_name, guid.upper(), str(age)]), "latin-1")
class MacIdentifier(IdentifierProcessor):
operating_system = 'mac'
operating_system = "mac"
@classmethod
def get_identifier(cls, json) -> Optional[bytes]:
mac_banner = json.get('symbols', {}).get('version', {}).get('constant_data', None)
mac_banner = (
json.get("symbols", {}).get("version", {}).get("constant_data", None)
)
if mac_banner:
return base64.b64decode(mac_banner)
return None
class LinuxIdentifier(IdentifierProcessor):
operating_system = 'linux'
operating_system = "linux"
@classmethod
def get_identifier(cls, json) -> Optional[bytes]:
linux_banner = json.get('symbols', {}).get('linux_banner', {}).get('constant_data', None)
linux_banner = (
json.get("symbols", {}).get("linux_banner", {}).get("constant_data", None)
)
if linux_banner:
return base64.b64decode(linux_banner)
return None
@@ -87,6 +95,7 @@ class LinuxIdentifier(IdentifierProcessor):
### CacheManagers
class CacheManagerInterface(interfaces.configuration.VersionableInterface):
def __init__(self, filename: str):
super().__init__()
@@ -99,7 +108,9 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
"""Adds an identifier to the store"""
pass
def find_location(self, identifier: bytes, operating_system: Optional[str]) -> Optional[str]:
def find_location(
self, identifier: bytes, operating_system: Optional[str]
) -> Optional[str]:
"""Returns the location of the symbol file given the identifier
Args:
@@ -122,8 +133,9 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
"""
pass
def get_identifier_dictionary(self, operating_system: Optional[str] = None, local_only: bool = False) -> \
Dict[bytes, str]:
def get_identifier_dictionary(
self, operating_system: Optional[str] = None, local_only: bool = False
) -> Dict[bytes, str]:
"""Returns a dictionary of identifiers and locations
Args:
@@ -143,7 +155,9 @@ class CacheManagerInterface(interfaces.configuration.VersionableInterface):
"""Returns all identifiers for a particular operating system"""
pass
def get_location_statistics(self, location: str) -> Optional[Tuple[int, int, int, int]]:
def get_location_statistics(
self, location: str
) -> Optional[Tuple[int, int, int, int]]:
"""Returns ISF statistics based on the location
Returns:
@@ -157,7 +171,6 @@ class SqliteCache(CacheManagerInterface):
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
def __init__(self, filename: str):
super().__init__(filename)
self.cache_period = constants.SQLITE_CACHE_PERIOD
@@ -170,28 +183,41 @@ class SqliteCache(CacheManagerInterface):
def _connect_storage(self, path: str) -> sqlite3.Connection:
database = sqlite3.connect(path)
database.row_factory = sqlite3.Row
database.cursor().execute(
f'CREATE TABLE IF NOT EXISTS database_info (schema_version INT DEFAULT {constants.CACHE_SQLITE_SCHEMA_VERSION})')
schema_version = database.cursor().execute('SELECT schema_version FROM database_info').fetchone()
f"CREATE TABLE IF NOT EXISTS database_info (schema_version INT DEFAULT {constants.CACHE_SQLITE_SCHEMA_VERSION})"
)
schema_version = (
database.cursor()
.execute("SELECT schema_version FROM database_info")
.fetchone()
)
if not schema_version:
database.cursor().execute(f'INSERT INTO database_info VALUES ({constants.CACHE_SQLITE_SCHEMA_VERSION})')
elif schema_version['schema_version'] == constants.CACHE_SQLITE_SCHEMA_VERSION:
database.cursor().execute(
f"INSERT INTO database_info VALUES ({constants.CACHE_SQLITE_SCHEMA_VERSION})"
)
elif schema_version["schema_version"] == constants.CACHE_SQLITE_SCHEMA_VERSION:
# All good, so pass and move on
pass
else:
vollog.info(f"Previous cache schema version found: {schema_version['schema_version']}")
vollog.info(
f"Previous cache schema version found: {schema_version['schema_version']}"
)
# TODO: Implement code if the schema changes
# Current this should never happen so we start over again
database.close()
os.unlink(path)
return self._connect_storage(path)
database.cursor().execute(
'CREATE TABLE IF NOT EXISTS cache (location TEXT UNIQUE NOT NULL, identifier TEXT, operating_system TEXT, hash TEXT,'
'stats_base_types INT DEFAULT 0, stats_types INT DEFAULT 0, stats_enums INT DEFAULT 0, stats_symbols INT DEFAULT 0, local BOOL, cached DATETIME)')
"CREATE TABLE IF NOT EXISTS cache (location TEXT UNIQUE NOT NULL, identifier TEXT, operating_system TEXT, hash TEXT,"
"stats_base_types INT DEFAULT 0, stats_types INT DEFAULT 0, stats_enums INT DEFAULT 0, stats_symbols INT DEFAULT 0, local BOOL, cached DATETIME)"
)
database.commit()
return database
def find_location(self, identifier: bytes, operating_system: Optional[str]) -> Optional[str]:
def find_location(
self, identifier: bytes, operating_system: Optional[str]
) -> Optional[str]:
"""Returns the location of the symbol file given the identifier.
If multiple locations exist for an identifier, the last found is returned
@@ -202,55 +228,82 @@ class SqliteCache(CacheManagerInterface):
Returns:
The location of the symbols file that matches the identifier or None
"""
statement = 'SELECT location FROM cache WHERE identifier = ?'
statement = "SELECT location FROM cache WHERE identifier = ?"
parameters = (identifier,)
if operating_system is not None:
statement = 'SELECT location FROM cache WHERE identifier = ? AND operating_system = ?'
statement = "SELECT location FROM cache WHERE identifier = ? AND operating_system = ?"
parameters = (identifier, operating_system)
results = self._database.cursor().execute(statement, parameters).fetchall()
result = None
for row in results:
result = row['location']
result = row["location"]
return result
def get_local_locations(self) -> Generator[str, None, None]:
result = self._database.cursor().execute('SELECT DISTINCT location FROM cache WHERE local = 1').fetchall()
result = (
self._database.cursor()
.execute("SELECT DISTINCT location FROM cache WHERE local = 1")
.fetchall()
)
for row in result:
yield row['location']
yield row["location"]
def is_url_local(self, url: str) -> bool:
"""Determines whether an url is local or not"""
parsed = urllib.parse.urlparse(url)
if parsed.scheme in ['file', 'jar']:
return True
return parsed.scheme in ["file", "jar"]
def get_identifier(self, location: str) -> Optional[bytes]:
results = self._database.cursor().execute('SELECT identifier FROM cache WHERE location = ?',
(location,)).fetchall()
results = (
self._database.cursor()
.execute("SELECT identifier FROM cache WHERE location = ?", (location,))
.fetchall()
)
for row in results:
return row['identifier']
return row["identifier"]
return None
def get_location_statistics(self, location: str) -> Optional[Tuple[int, int, int, int]]:
results = self._database.cursor().execute(
'SELECT stats_base_types, stats_types, stats_enums, stats_symbols FROM cache WHERE location = ?',
(location,)).fetchall()
def get_location_statistics(
self, location: str
) -> Optional[Tuple[int, int, int, int]]:
results = (
self._database.cursor()
.execute(
"SELECT stats_base_types, stats_types, stats_enums, stats_symbols FROM cache WHERE location = ?",
(location,),
)
.fetchall()
)
for row in results:
return row['stats_base_types'], row['stats_types'], row['stats_enums'], row['stats_symbols']
return (
row["stats_base_types"],
row["stats_types"],
row["stats_enums"],
row["stats_symbols"],
)
return None
def get_hash(self, location: str) -> Optional[str]:
results = self._database.cursor().execute('SELECT hash FROM cache WHERE location = ?',
(location,)).fetchall()
results = (
self._database.cursor()
.execute("SELECT hash FROM cache WHERE location = ?", (location,))
.fetchall()
)
for row in results:
return row['hash']
return row["hash"]
return None
def update(self, progress_callback = None):
def update(self, progress_callback=None):
"""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.
"""
on_disk_locations = set([filename for filename in intermed.IntermediateSymbolTable.file_symbol_url('')])
on_disk_locations = set(
[
filename
for filename in intermed.IntermediateSymbolTable.file_symbol_url("")
]
)
cached_locations = set(self.get_local_locations())
new_locations = on_disk_locations.difference(cached_locations)
@@ -259,11 +312,41 @@ class SqliteCache(CacheManagerInterface):
cache_update = set()
files_to_timestamp = on_disk_locations.intersection(cached_locations)
if files_to_timestamp:
result = self._database.cursor().execute("SELECT location FROM cache WHERE local = 1 "
f"AND cached < date('now', '{self.cache_period}');")
result = self._database.cursor().execute(
"SELECT location, cached FROM cache WHERE local = 1 "
f"AND cached < date('now', '{self.cache_period}');"
)
for row in result:
if row['location'] in files_to_timestamp:
cache_update.add(row['location'])
location = row["location"]
stored_timestamp = datetime.datetime.fromisoformat(row["cached"])
timestamp = stored_timestamp # Default to requiring update
# See if the file is a local URL type we can handle:
parsed = urllib.parse.urlparse(location)
pathname = None
if parsed.scheme == "file":
pathname = urllib.request.url2pathname(parsed.path)
if parsed.scheme == "jar":
inner_url = urllib.parse.urlparse(parsed.path)
if inner_url.scheme == "file":
pathname = inner_url.path.split("!")[0]
if pathname:
timestamp = datetime.datetime.fromtimestamp(
os.stat(pathname).st_mtime
)
else:
vollog.log(
constants.LOGLEVEL_VVVV,
"File location in database classed as local but not file/jar URL",
)
# If we're supposed to include it, and our last check is older than (or equal to) the file timestamp
if (
row["location"] in files_to_timestamp
and stored_timestamp < timestamp
):
cache_update.add(row["location"])
idextractors = list(framework.class_subclasses(IdentifierProcessor))
@@ -275,8 +358,10 @@ class SqliteCache(CacheManagerInterface):
try:
for counter, location in enumerate(files_to_process):
# Open location
progress_callback(counter * 100 / number_files_to_process,
f"Updating caches for {number_files_to_process} files...")
progress_callback(
counter * 100 / number_files_to_process,
f"Updating caches for {number_files_to_process} files...",
)
try:
with resources.ResourceAccessor().open(location) as fp:
json_obj = json.load(fp)
@@ -284,10 +369,10 @@ class SqliteCache(CacheManagerInterface):
identifier = None
# Get stats
stats_base_types = len(json_obj.get('base_types', {}))
stats_types = len(json_obj.get('types', {}))
stats_enums = len(json_obj.get('enums', {}))
stats_symbols = len(json_obj.get('symbols', {}))
stats_base_types = len(json_obj.get("base_types", {}))
stats_types = len(json_obj.get("types", {}))
stats_enums = len(json_obj.get("enums", {}))
stats_symbols = len(json_obj.get("symbols", {}))
operating_system = None
for idextractor in idextractors:
@@ -311,12 +396,19 @@ class SqliteCache(CacheManagerInterface):
stats_types,
stats_enums,
stats_symbols,
self.is_url_local(location)
))
self.is_url_local(location),
),
)
if identifier is not None:
vollog.log(constants.LOGLEVEL_VV, f"Identified {location} as {identifier}")
vollog.log(
constants.LOGLEVEL_VV,
f"Identified {location} as {identifier}",
)
else:
vollog.log(constants.LOGLEVEL_VVVV, f"No identifier found for {location}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"No identifier found for {location}",
)
except Exception as excp:
vollog.log(constants.LOGLEVEL_VVVV, excp)
finally:
@@ -325,20 +417,23 @@ class SqliteCache(CacheManagerInterface):
# Remote Entries
if not constants.OFFLINE and constants.REMOTE_ISF_URL:
progress_callback(0, 'Reading remote ISF list')
progress_callback(0, "Reading remote ISF list")
cursor = self._database.cursor()
cursor.execute(
f"SELECT cached FROM cache WHERE local = 0 and cached < datetime('now', {self.cache_period})")
f"SELECT cached FROM cache WHERE local = 0 and cached < datetime('now', {self.cache_period})"
)
remote_identifiers = RemoteIdentifierFormat(constants.REMOTE_ISF_URL)
progress_callback(50, 'Reading remote ISF list')
progress_callback(50, "Reading remote ISF list")
for operating_system in constants.OS_CATEGORIES:
identifiers = remote_identifiers.process({}, operating_system = operating_system)
identifiers = remote_identifiers.process(
{}, operating_system=operating_system
)
for identifier, location in identifiers:
cursor.execute(
"INSERT OR REPLACE INTO cache(identifier, location, operating_system, local, cached) VALUES (?, ?, ?, ?, datetime('now'))",
(location, identifier, operating_system, False)
(location, identifier, operating_system, False),
)
progress_callback(100, 'Reading remote ISF list')
progress_callback(100, "Reading remote ISF list")
self._database.commit()
# Missing entries
@@ -346,52 +441,69 @@ class SqliteCache(CacheManagerInterface):
if missing_locations:
self._database.cursor().execute(
f"DELETE FROM cache WHERE location IN ({','.join(['?'] * len(missing_locations))})",
[x for x in missing_locations])
[x for x in missing_locations],
)
self._database.commit()
def get_identifier_dictionary(self, operating_system: Optional[str] = None, local_only: bool = False) -> \
Dict[bytes, str]:
def get_identifier_dictionary(
self, operating_system: Optional[str] = None, local_only: bool = False
) -> Dict[bytes, str]:
output = {}
additions = []
statement = 'SELECT location, identifier FROM cache'
statement = "SELECT location, identifier FROM cache"
if local_only:
additions.append('local = 1')
additions.append("local = 1")
if operating_system:
additions.append(f"operating_system = '{operating_system}'")
if additions:
statement += f" WHERE {' AND '.join(additions)}"
results = self._database.cursor().execute(statement)
for row in results:
if row['identifier'] in output and row['identifier'] and row['location']:
if row["identifier"] in output and row["identifier"] and row["location"]:
vollog.debug(
f"Duplicate entry for identifier {row['identifier']}: {row['location']} and {output[row['identifier']]}")
output[row['identifier']] = row['location']
f"Duplicate entry for identifier {row['identifier']}: {row['location']} and {output[row['identifier']]}"
)
output[row["identifier"]] = row["location"]
return output
def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]:
if operating_system:
results = self._database.cursor().execute('SELECT identifier FROM cache WHERE operating_system = ?',
(operating_system,)).fetchall()
results = (
self._database.cursor()
.execute(
"SELECT identifier FROM cache WHERE operating_system = ?",
(operating_system,),
)
.fetchall()
)
else:
results = self._database.cursor().execute('SELECT identifier FROM cache').fetchall()
results = (
self._database.cursor()
.execute("SELECT identifier FROM cache")
.fetchall()
)
output = []
for row in results:
output.append(row['identifier'])
output.append(row["identifier"])
return output
### Automagic
class SymbolCacheMagic(interfaces.automagic.AutomagicInterface):
"""Runs through all symbol tables and caches their identifiers"""
priority = 0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
identifiers_path = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
self._cache = SqliteCache(identifiers_path)
def __call__(self, context, config_path, configurable, progress_callback = None):
def __call__(self, context, config_path, configurable, progress_callback=None):
"""Runs the automagic over the configurable."""
self._cache.update(progress_callback)
@@ -399,37 +511,45 @@ class SymbolCacheMagic(interfaces.automagic.AutomagicInterface):
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
"""Returns a list of RequirementInterface objects required by this
object."""
return [requirements.VersionRequirement(name = 'SQLiteCache', component = SqliteCache, version = (1, 0, 0))]
return [
requirements.VersionRequirement(
name="SQLiteCache", component=SqliteCache, version=(1, 0, 0)
)
]
class RemoteIdentifierFormat:
def __init__(self, location: str):
self._location = location
with resources.ResourceAccessor().open(url = location) as fp:
with resources.ResourceAccessor().open(url=location) as fp:
self._data = json.load(fp)
if not self._verify():
raise ValueError("Unsupported version for remote identifier list format")
def _verify(self) -> bool:
version = self._data.get('version', 0)
version = self._data.get("version", 0)
if version in [1]:
setattr(self, 'process', getattr(self, f'process_v{version}'))
setattr(self, "process", getattr(self, f"process_v{version}"))
return True
return False
def process(self, identifiers: Dict[bytes, List[str]], operating_system: Optional[str]) -> Generator[
Tuple[bytes, str], None, None]:
def process(
self, identifiers: Dict[bytes, List[str]], operating_system: Optional[str]
) -> Generator[Tuple[bytes, str], None, None]:
raise ValueError("Identifier List version not verified")
def process_v1(self, identifiers: Optional[Dict[bytes, List[str]]], operating_system: Optional[str]) -> Generator[
Tuple[bytes, str], None, None]:
def process_v1(
self,
identifiers: Optional[Dict[bytes, List[str]]],
operating_system: Optional[str],
) -> Generator[Tuple[bytes, str], None, None]:
if operating_system in self._data:
for identifier in self._data[operating_system]:
binary_identifier = base64.b64decode(identifier)
for value in self._data[operating_system][identifier]:
yield binary_identifier, value
if 'additional' in self._data:
for location in self._data['additional']:
if "additional" in self._data:
for location in self._data["additional"]:
try:
subrbf = RemoteIdentifierFormat(location)
yield from subrbf.process(identifiers, operating_system)
@@ -16,6 +16,7 @@ vollog = logging.getLogger(__name__)
class SymbolFinder(interfaces.automagic.AutomagicInterface):
"""Symbol loader based on signature strings."""
priority = 40
banner_config_key: str = "banner"
@@ -23,17 +24,23 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
symbol_class: Optional[str] = None
find_aslr: Optional[Callable] = None
def __init__(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
super().__init__(context, config_path)
self._requirements: List[Tuple[str, interfaces.configuration.RequirementInterface]] = []
self._requirements: List[
Tuple[str, interfaces.configuration.RequirementInterface]
] = []
self._banners: symbol_cache.BannersType = {}
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.VersionRequirement(name = 'SQLiteCache',
component = symbol_cache.SqliteCache,
version = (1, 0, 0))
requirements.VersionRequirement(
name="SQLiteCache",
component=symbol_cache.SqliteCache,
version=(1, 0, 0),
)
]
@property
@@ -41,16 +48,22 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
"""Creates a cached copy of the results, but only it's been
requested."""
if not self._banners:
identifiers_path = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
cache = symbol_cache.SqliteCache(identifiers_path)
self._banners = cache.get_identifier_dictionary(operating_system = self.operating_system)
self._banners = cache.get_identifier_dictionary(
operating_system=self.operating_system
)
return self._banners
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> None:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> None:
"""Searches for SymbolTableRequirements and attempt to populate
them."""
@@ -61,30 +74,47 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
self._requirements = self.find_requirements(
context,
config_path,
requirement, (requirements.TranslationLayerRequirement, requirements.SymbolTableRequirement),
shortcut = False)
requirement,
(
requirements.TranslationLayerRequirement,
requirements.SymbolTableRequirement,
),
shortcut=False,
)
for (sub_path, requirement) in self._requirements:
parent_path = interfaces.configuration.parent_path(sub_path)
if (isinstance(requirement, requirements.SymbolTableRequirement)
and requirement.unsatisfied(context, parent_path)):
if isinstance(
requirement, requirements.SymbolTableRequirement
) and requirement.unsatisfied(context, parent_path):
for (tl_sub_path, tl_requirement) in self._requirements:
tl_parent_path = interfaces.configuration.parent_path(tl_sub_path)
# Find the TranslationLayer sibling to the SymbolTableRequirement
if (isinstance(tl_requirement, requirements.TranslationLayerRequirement)
and tl_parent_path == parent_path):
if (
isinstance(
tl_requirement, requirements.TranslationLayerRequirement
)
and tl_parent_path == parent_path
):
if context.config.get(tl_sub_path, None):
self._banner_scan(context, parent_path, requirement, context.config[tl_sub_path],
progress_callback)
self._banner_scan(
context,
parent_path,
requirement,
context.config[tl_sub_path],
progress_callback,
)
break
def _banner_scan(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.ConstructableRequirementInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> None:
def _banner_scan(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.ConstructableRequirementInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> None:
"""Accepts a context, config_path and SymbolTableRequirement, with a
constructed layer_name and scans the layer for banners."""
@@ -98,15 +128,18 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
# Check if the Stacker has already found what we're looking for
if layer.config.get(self.banner_config_key, None):
banner_list = [(0, bytes(layer.config[self.banner_config_key],
'raw_unicode_escape'))] # type: Iterable[Any]
banner_list = [
(0, bytes(layer.config[self.banner_config_key], "raw_unicode_escape"))
] # type: Iterable[Any]
else:
# Swap to the physical layer for scanning
# Only traverse down a layer if it's an intel layer
# TODO: Fix this so it works for layers other than just Intel
if isinstance(layer, layers.intel.Intel):
layer = context.layers[layer.config['memory_layer']]
banner_list = layer.scan(context = context, scanner = mss, progress_callback = progress_callback)
layer = context.layers[layer.config["memory_layer"]]
banner_list = layer.scan(
context=context, scanner=mss, progress_callback=progress_callback
)
for _, banner in banner_list:
vollog.debug(f"Identified banner: {repr(banner)}")
@@ -117,9 +150,15 @@ class SymbolFinder(interfaces.automagic.AutomagicInterface):
clazz = self.symbol_class
# Set the discovered options
path_join = interfaces.configuration.path_join
context.config[path_join(config_path, requirement.name, "class")] = clazz
context.config[path_join(config_path, requirement.name, "isf_url")] = isf_path
context.config[path_join(config_path, requirement.name, "symbol_mask")] = layer.address_mask
context.config[
path_join(config_path, requirement.name, "class")
] = clazz
context.config[
path_join(config_path, requirement.name, "isf_url")
] = isf_path
context.config[
path_join(config_path, requirement.name, "symbol_mask")
] = layer.address_mask
# Construct the appropriate symbol table
requirement.construct(context, config_path)
+201 -106
View File
@@ -41,8 +41,14 @@ class DtbSelfReferential:
"""A generic DTB test which looks for a self-referential pointer at *any*
index within the page."""
def __init__(self, layer_type: Type[layers.intel.Intel], ptr_struct: str, mask: int,
valid_range: Iterable[int], reserved_bits: int) -> None:
def __init__(
self,
layer_type: Type[layers.intel.Intel],
ptr_struct: str,
mask: int,
valid_range: Iterable[int],
reserved_bits: int,
) -> None:
self.layer_type = layer_type
self.ptr_struct = ptr_struct
self.ptr_size = struct.calcsize(ptr_struct)
@@ -51,22 +57,26 @@ class DtbSelfReferential:
self.valid_range = valid_range
self.reserved_bits = reserved_bits
def __call__(self, data: bytes, data_offset: int, page_offset: int) -> Optional[Tuple[int, int]]:
page = data[page_offset:page_offset + self.page_size]
def __call__(
self, data: bytes, data_offset: int, page_offset: int
) -> Optional[Tuple[int, int]]:
page = data[page_offset : page_offset + self.page_size]
if not page:
return None
ref_pages = set()
for ref in range(0, self.page_size, self.ptr_size):
ptr_data = page[ref:ref + self.ptr_size]
ptr, = struct.unpack(self.ptr_struct, ptr_data)
ptr_data = page[ref : ref + self.ptr_size]
(ptr,) = struct.unpack(self.ptr_struct, ptr_data)
# For both Intel-32e, bit 7 is reserved (more are reserved in PAE), so if that's ever set,
# we can move on
if (ptr & self.reserved_bits) and (ptr & 0x01):
return None
if ((ptr & self.mask) == (data_offset + page_offset)) and (data_offset + page_offset > 0):
if ((ptr & self.mask) == (data_offset + page_offset)) and (
data_offset + page_offset > 0
):
# Pointer must be valid
if (ptr & 0x01):
if ptr & 0x01:
ref_pages.add(ref)
# The DTB is extremely unlikely to refer back to itself. so the number of reference should always be exactly 1
@@ -78,62 +88,78 @@ class DtbSelfReferential:
class DtbSelfRef32bit(DtbSelfReferential):
def __init__(self):
super().__init__(layer_type = layers.intel.WindowsIntel,
ptr_struct = "I",
mask = 0xFFFFF000,
valid_range = [0x300],
reserved_bits = 0x0)
super().__init__(
layer_type=layers.intel.WindowsIntel,
ptr_struct="I",
mask=0xFFFFF000,
valid_range=[0x300],
reserved_bits=0x0,
)
class DtbSelfRef64bit(DtbSelfReferential):
def __init__(self) -> None:
super().__init__(layer_type = layers.intel.WindowsIntel32e,
ptr_struct = "Q",
mask = 0x3FFFFFFFFFF000,
valid_range = range(0x100, 0x1ff),
reserved_bits = 0x80)
super().__init__(
layer_type=layers.intel.WindowsIntel32e,
ptr_struct="Q",
mask=0x3FFFFFFFFFF000,
valid_range=range(0x100, 0x1FF),
reserved_bits=0x80,
)
class DtbSelfRef64bitOldWindows(DtbSelfReferential):
def __init__(self) -> None:
super().__init__(layer_type = layers.intel.WindowsIntel32e,
ptr_struct = "Q",
mask = 0x3FFFFFFFFFF000,
valid_range = [0x1ed],
reserved_bits = 0x80)
super().__init__(
layer_type=layers.intel.WindowsIntel32e,
ptr_struct="Q",
mask=0x3FFFFFFFFFF000,
valid_range=[0x1ED],
reserved_bits=0x80,
)
class DtbSelfRefPae(DtbSelfReferential):
def __init__(self) -> None:
super().__init__(layer_type = layers.intel.WindowsIntelPAE,
ptr_struct = "Q",
valid_range = [0x3],
mask = 0x3FFFFFFFFFF000,
reserved_bits = 0x0)
super().__init__(
layer_type=layers.intel.WindowsIntelPAE,
ptr_struct="Q",
valid_range=[0x3],
mask=0x3FFFFFFFFFF000,
reserved_bits=0x0,
)
@staticmethod
def _and_bytes(abytes, bbytes):
return bytes([a & b for a, b in zip(abytes[::-1], bbytes[::-1])][::-1])
def __call__(self, data: bytes, data_offset: int, page_offset: int) -> Optional[Tuple[int, int]]:
def __call__(
self, data: bytes, data_offset: int, page_offset: int
) -> Optional[Tuple[int, int]]:
dtb = super().__call__(data, data_offset, page_offset)
if dtb:
# Find the top page
top_pae_page = dtb[0] - 0x4000
# The top page should map to the next four pages after it
# Build what we expect the page table to be
expected_table = b''.join([struct.pack(self.ptr_struct, top_pae_page + (i * 0x1000)) for i in range(1, 5)])
expected_table = b"".join(
[
struct.pack(self.ptr_struct, top_pae_page + (i * 0x1000))
for i in range(1, 5)
]
)
# Mask off the page bits of top level page map
page_table_mask = b"\x00\xf0\xff\xff\xff\xff\xff\xff" * 4
page_table = data[top_pae_page - data_offset: top_pae_page - data_offset + (4 * self.ptr_size)]
page_table = data[
top_pae_page
- data_offset : top_pae_page
- data_offset
+ (4 * self.ptr_size)
]
# Compare them
anded_bytes = self._and_bytes(page_table, page_table_mask)
if (anded_bytes == expected_table):
if anded_bytes == expected_table:
return top_pae_page, dtb[1]
# Return None since the dtb value *isn't* None
return None
@@ -143,6 +169,7 @@ class DtbSelfRefPae(DtbSelfReferential):
class PageMapScanner(interfaces.layers.ScannerInterface):
"""Scans through all pages using DTB tests to determine a dtb offset and
architecture."""
overlap = 0x4000
thread_safe = True
tests = [DtbSelfRef64bit(), DtbSelfRefPae(), DtbSelfRef32bit()]
@@ -153,7 +180,9 @@ class PageMapScanner(interfaces.layers.ScannerInterface):
if tests:
self.tests = tests
def __call__(self, data: bytes, data_offset: int) -> Generator[Tuple[DtbSelfReferential, int], None, None]:
def __call__(
self, data: bytes, data_offset: int
) -> Generator[Tuple[DtbSelfReferential, int], None, None]:
for page_offset in range(0, len(data), 0x1000):
for test in self.tests:
result = test(data, data_offset, page_offset)
@@ -163,20 +192,29 @@ class PageMapScanner(interfaces.layers.ScannerInterface):
class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 40
exclusion_list = ['mac', 'linux']
exclusion_list = ["mac", "linux"]
# Group these by region so we only run over the data once
test_sets = [("Detecting Self-referential pointer for recent windows",
[DtbSelfRef64bit()], [(0x150000, 0x150000), (0x650000, 0xa0000)]),
("Older windows fixed location self-referential pointers",
[DtbSelfRefPae(), DtbSelfRef32bit(), DtbSelfRef64bitOldWindows()], [(0x30000, 0x1000000)])
]
test_sets = [
(
"Detecting Self-referential pointer for recent windows",
[DtbSelfRef64bit()],
[(0x150000, 0x150000), (0x650000, 0xA0000)],
),
(
"Older windows fixed location self-referential pointers",
[DtbSelfRefPae(), DtbSelfRef32bit(), DtbSelfRef64bitOldWindows()],
[(0x30000, 0x1000000)],
),
]
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempts to determine and stack an intel layer on a physical layer
where possible.
@@ -192,29 +230,43 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
base_layer = context.layers[layer_name]
if isinstance(base_layer, intel.Intel):
return None
if base_layer.metadata.get('os', None) not in ['Windows', 'Unknown']:
if base_layer.metadata.get("os", None) not in ["Windows", "Unknown"]:
return None
layer = config_path = None
# Check the metadata
if (base_layer.metadata.get('os', None) == 'Windows' and base_layer.metadata.get('page_map_offset')):
arch = base_layer.metadata.get('architecture', None)
if arch not in ['Intel32', 'Intel64']:
if base_layer.metadata.get("os", None) == "Windows" and base_layer.metadata.get(
"page_map_offset"
):
arch = base_layer.metadata.get("architecture", None)
if arch not in ["Intel32", "Intel64"]:
return None
# Set the layer type
layer_type: Type = intel.WindowsIntel
if arch == 'Intel64':
if arch == "Intel64":
layer_type = intel.WindowsIntel32e
elif base_layer.metadata.get('pae', False):
elif base_layer.metadata.get("pae", False):
layer_type = intel.WindowsIntelPAE
# Construct the layer
new_layer_name = context.layers.free_layer_name("IntelLayer")
config_path = interfaces.configuration.path_join("IntelHelper", new_layer_name)
context.config[interfaces.configuration.path_join(config_path, "memory_layer")] = layer_name
context.config[interfaces.configuration.path_join(
config_path, "page_map_offset")] = base_layer.metadata['page_map_offset']
layer = layer_type(context, config_path = config_path, name = new_layer_name, metadata = {'os': 'Windows'})
page_map_offset = context.config[interfaces.configuration.path_join(config_path, "page_map_offset")]
config_path = interfaces.configuration.path_join(
"IntelHelper", new_layer_name
)
context.config[
interfaces.configuration.path_join(config_path, "memory_layer")
] = layer_name
context.config[
interfaces.configuration.path_join(config_path, "page_map_offset")
] = base_layer.metadata["page_map_offset"]
layer = layer_type(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "Windows"},
)
page_map_offset = context.config[
interfaces.configuration.path_join(config_path, "page_map_offset")
]
vollog.debug(f"DTB was given to us by base layer: {hex(page_map_offset)}")
return layer
@@ -222,10 +274,12 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
for description, tests, sections in cls.test_sets:
vollog.debug(description)
# There is a very high chance that the DTB will live in these very narrow segments, assuming we couldn't find them previously
hits = base_layer.scan(context,
PageMapScanner(tests = tests),
sections = sections,
progress_callback = progress_callback)
hits = base_layer.scan(
context,
PageMapScanner(tests=tests),
sections=sections,
progress_callback=progress_callback,
)
# Flatten the generator
def sort_by_tests(x):
@@ -236,13 +290,19 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
"""Determines a pointer from a page_table"""
max_ptr = 0
for index in range(0, len(page_table), ptr_size):
pointer = struct.unpack(test.ptr_struct, page_table[index:index + ptr_size])[0]
pointer = struct.unpack(
test.ptr_struct, page_table[index : index + ptr_size]
)[0]
# Make sure the pointer is valid, ignore large pages which would require more calculation
if pointer & 0x1 and not pointer & 0x80:
max_ptr = max(max_ptr, (pointer ^ (pointer & 0xfff)) % test.layer_type.maximum_address)
max_ptr = max(
max_ptr,
(pointer ^ (pointer & 0xFFF))
% test.layer_type.maximum_address,
)
return max_ptr
hits = sorted(list(hits), key = sort_by_tests)
hits = sorted(list(hits), key=sort_by_tests)
for test, page_map_offset in hits:
# Turn the page tables into integers and find the largest one
@@ -251,26 +311,45 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface):
max_pointer = get_max_pointer(page_table, test, ptr_size)
if max_pointer <= base_layer.maximum_address:
vollog.debug(f"{test.__class__.__name__} test succeeded at {hex(page_map_offset)}")
vollog.debug(
f"{test.__class__.__name__} test succeeded at {hex(page_map_offset)}"
)
new_layer_name = context.layers.free_layer_name("IntelLayer")
config_path = interfaces.configuration.path_join("IntelHelper", new_layer_name)
context.config[interfaces.configuration.path_join(config_path, "memory_layer")] = layer_name
config_path = interfaces.configuration.path_join(
"IntelHelper", new_layer_name
)
context.config[
interfaces.configuration.path_join(config_path, "page_map_offset")] = page_map_offset
layer = test.layer_type(context,
config_path = config_path,
name = new_layer_name,
metadata = {'os': 'Windows'})
interfaces.configuration.path_join(config_path, "memory_layer")
] = layer_name
context.config[
interfaces.configuration.path_join(
config_path, "page_map_offset"
)
] = page_map_offset
layer = test.layer_type(
context,
config_path=config_path,
name=new_layer_name,
metadata={"os": "Windows"},
)
break
else:
vollog.debug(
f"Max pointer for hit with test {test.__class__.__name__} not met: {hex(max_pointer)} > {hex(base_layer.maximum_address)}")
f"Max pointer for hit with test {test.__class__.__name__} not met: {hex(max_pointer)} > {hex(base_layer.maximum_address)}"
)
if layer is not None and config_path:
break
if layer is not None and config_path:
vollog.debug("DTB was found at: 0x{:0x}".format(context.config[interfaces.configuration.path_join(
config_path, "page_map_offset")]))
vollog.debug(
"DTB was found at: 0x{:0x}".format(
context.config[
interfaces.configuration.path_join(
config_path, "page_map_offset"
)
]
)
)
return layer
@@ -278,31 +357,37 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
"""Class to read swap_layers filenames from single-swap-layers, create the
layers and populate the single-layers swap_layers."""
exclusion_list = ['linux', 'mac']
exclusion_list = ["linux", "mac"]
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> None:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> None:
"""Finds translation layers that can have swap layers added."""
path_join = interfaces.configuration.path_join
self._translation_requirement = self.find_requirements(context,
config_path,
requirement,
requirements.TranslationLayerRequirement,
shortcut = False)
self._translation_requirement = self.find_requirements(
context,
config_path,
requirement,
requirements.TranslationLayerRequirement,
shortcut=False,
)
for trans_sub_config, trans_req in self._translation_requirement:
if not isinstance(trans_req, requirements.TranslationLayerRequirement):
# We need this so the type-checker knows we're a TranslationLayerRequirement
continue
swap_sub_config, swap_req = self.find_swap_requirement(trans_sub_config, trans_req)
swap_sub_config, swap_req = self.find_swap_requirement(
trans_sub_config, trans_req
)
counter = 0
swap_config = interfaces.configuration.parent_path(swap_sub_config)
if swap_req and swap_req.unsatisfied(context, swap_config):
# See if any of them need constructing
for swap_location in self.config.get('single_swap_locations', []):
for swap_location in self.config.get("single_swap_locations", []):
# Setup config locations/paths
current_layer_name = swap_req.name + str(counter)
current_layer_path = path_join(swap_sub_config, current_layer_name)
@@ -314,32 +399,41 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
if swap_location:
context.config[current_layer_path] = current_layer_name
context.config[layer_loc_path] = swap_location
context.config[layer_class_path] = 'volatility3.framework.layers.physical.FileLayer'
context.config[
layer_class_path
] = "volatility3.framework.layers.physical.FileLayer"
# Add the requirement
new_req = requirements.TranslationLayerRequirement(name = current_layer_name,
description = "Swap Layer",
optional = False)
new_req = requirements.TranslationLayerRequirement(
name=current_layer_name,
description="Swap Layer",
optional=False,
)
swap_req.add_requirement(new_req)
context.config[path_join(swap_sub_config, 'number_of_elements')] = counter
context.config[
path_join(swap_sub_config, "number_of_elements")
] = counter
context.config[swap_sub_config] = True
swap_req.construct(context, swap_config)
@staticmethod
def find_swap_requirement(config: str,
requirement: requirements.TranslationLayerRequirement) \
-> Tuple[str, Optional[requirements.LayerListRequirement]]:
def find_swap_requirement(
config: str, requirement: requirements.TranslationLayerRequirement
) -> Tuple[str, Optional[requirements.LayerListRequirement]]:
"""Takes a Translation layer and returns its swap_layer requirement."""
swap_req = None
for req_name in requirement.requirements:
req = requirement.requirements[req_name]
if isinstance(req, requirements.LayerListRequirement) and req.name == 'swap_layers':
if (
isinstance(req, requirements.LayerListRequirement)
and req.name == "swap_layers"
):
swap_req = req
continue
swap_config = interfaces.configuration.path_join(config, 'swap_layers')
swap_config = interfaces.configuration.path_join(config, "swap_layers")
return swap_config, swap_req
@classmethod
@@ -347,10 +441,11 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
"""Returns the requirements of this plugin."""
return [
requirements.ListRequirement(
name = "single_swap_locations",
element_type = str,
min_elements = 0,
max_elements = 16,
description = "Specifies a list of swap layer URIs for use with single-location",
optional = True)
name="single_swap_locations",
element_type=str,
min_elements=0,
max_elements=16,
description="Specifies a list of swap layer URIs for use with single-location",
optional=True,
)
]
@@ -24,23 +24,27 @@ class MultiRequirement(interfaces.configuration.RequirementInterface):
so this is a concrete implementation.
"""
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
return self.unsatisfied_children(context, config_path)
class BooleanRequirement(interfaces.configuration.SimpleTypeRequirement):
"""A requirement type that contains a boolean value."""
# Note, this must be a separate class in order to differentiate between Booleans and other instance requirements
class IntRequirement(interfaces.configuration.SimpleTypeRequirement):
"""A requirement type that contains a single integer."""
instance_type: ClassVar[Type] = int
class StringRequirement(interfaces.configuration.SimpleTypeRequirement):
"""A requirement type that contains a single unicode string."""
# TODO: Maybe add string length limits?
instance_type: ClassVar[Type] = str
@@ -48,11 +52,13 @@ class StringRequirement(interfaces.configuration.SimpleTypeRequirement):
class URIRequirement(StringRequirement):
"""A requirement type that contains a single unicode string that is a valid
URI."""
# TODO: Maybe a a check that to unsatisfied that the path really is a URL?
class BytesRequirement(interfaces.configuration.SimpleTypeRequirement):
"""A requirement type that contains a byte string."""
instance_type: ClassVar[Type] = bytes
@@ -67,12 +73,14 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
and does not allow for a dynamic number of values.
"""
def __init__(self,
element_type: Type[interfaces.configuration.SimpleTypes] = str,
max_elements: Optional[int] = 0,
min_elements: Optional[int] = None,
*args,
**kwargs) -> None:
def __init__(
self,
element_type: Type[interfaces.configuration.SimpleTypes] = str,
max_elements: Optional[int] = 0,
min_elements: Optional[int] = None,
*args,
**kwargs,
) -> None:
"""Constructs the object.
Args:
@@ -82,24 +90,33 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
"""
super().__init__(*args, **kwargs)
if not issubclass(element_type, interfaces.configuration.BasicTypes):
raise TypeError("ListRequirements can only be populated with simple InstanceRequirements")
raise TypeError(
"ListRequirements can only be populated with simple InstanceRequirements"
)
self.element_type: Type = element_type
self.min_elements: int = min_elements or 0
self.max_elements: Optional[int] = max_elements
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Check the types on each of the returned values and their number and
then call the element type's check for each one."""
config_path = interfaces.configuration.path_join(config_path, self.name)
default = None
value = self.config_value(context, config_path, default)
if not value and self.min_elements > 0:
vollog.log(constants.LOGLEVEL_V, "ListRequirement Unsatisfied - ListRequirement has non-zero min_elements")
vollog.log(
constants.LOGLEVEL_V,
"ListRequirement Unsatisfied - ListRequirement has non-zero min_elements",
)
return {config_path: self}
if value is None and not self.optional:
# We need to differentiate between no value and an empty list
vollog.log(constants.LOGLEVEL_V, "ListRequirement Unsatisfied - Value was not specified")
vollog.log(
constants.LOGLEVEL_V,
"ListRequirement Unsatisfied - Value was not specified",
)
return {config_path: self}
elif value is None:
context.config[config_path] = []
@@ -107,13 +124,22 @@ class ListRequirement(interfaces.configuration.RequirementInterface):
# TODO: Check this is the correct response for an error
raise TypeError(f"Unexpected config value found: {repr(value)}")
if not (self.min_elements <= len(value)):
vollog.log(constants.LOGLEVEL_V, "TypeError - Too few values provided to list option.")
vollog.log(
constants.LOGLEVEL_V,
"TypeError - Too few values provided to list option.",
)
return {config_path: self}
if self.max_elements and not (len(value) < self.max_elements):
vollog.log(constants.LOGLEVEL_V, "TypeError - Too many values provided to list option.")
vollog.log(
constants.LOGLEVEL_V,
"TypeError - Too many values provided to list option.",
)
return {config_path: self}
if not all([isinstance(element, self.element_type) for element in value]):
vollog.log(constants.LOGLEVEL_V, "TypeError - At least one element in the list is not of the correct type.")
vollog.log(
constants.LOGLEVEL_V,
"TypeError - At least one element in the list is not of the correct type.",
)
return {config_path: self}
return {}
@@ -128,37 +154,48 @@ class ChoiceRequirement(interfaces.configuration.RequirementInterface):
choices: A list of possible string options that can be chosen from
"""
super().__init__(*args, **kwargs)
if not isinstance(choices, list) or any([not isinstance(choice, str) for choice in choices]):
if not isinstance(choices, list) or any(
[not isinstance(choice, str) for choice in choices]
):
raise TypeError("ChoiceRequirement takes a list of strings as choices")
self.choices = choices
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Validates the provided value to ensure it is one of the available
choices."""
config_path = interfaces.configuration.path_join(config_path, self.name)
value = self.config_value(context, config_path)
if value not in self.choices:
vollog.log(constants.LOGLEVEL_V, "ValueError - Value is not within the set of available choices")
vollog.log(
constants.LOGLEVEL_V,
"ValueError - Value is not within the set of available choices",
)
return {config_path: self}
return {}
class ComplexListRequirement(MultiRequirement,
interfaces.configuration.ConfigurableRequirementInterface,
metaclass = abc.ABCMeta):
class ComplexListRequirement(
MultiRequirement,
interfaces.configuration.ConfigurableRequirementInterface,
metaclass=abc.ABCMeta,
):
"""Allows a variable length list of requirements."""
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Validates the provided value to ensure it is one of the available
choices."""
config_path = interfaces.configuration.path_join(config_path, self.name)
ret_list = super().unsatisfied(context, config_path)
if ret_list:
return ret_list
if (self.config_value(context, config_path, None) is None
or self.config_value(context, interfaces.configuration.path_join(config_path, 'number_of_elements'))):
if self.config_value(context, config_path, None) is None or self.config_value(
context,
interfaces.configuration.path_join(config_path, "number_of_elements"),
):
return {config_path: self}
return {}
@@ -166,13 +203,17 @@ class ComplexListRequirement(MultiRequirement,
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# This is not optional for the stacker to run, so optional must be marked as False
return [
IntRequirement("number_of_elements",
description = "Determines how many layers are in this list",
optional = False)
IntRequirement(
"number_of_elements",
description="Determines how many layers are in this list",
optional=False,
)
]
@abc.abstractmethod
def construct(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def construct(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
"""Method for constructing within the context any required elements
from subrequirements."""
@@ -180,17 +221,22 @@ class ComplexListRequirement(MultiRequirement,
def new_requirement(self, index) -> interfaces.configuration.RequirementInterface:
"""Builds a new requirement based on the specified index."""
def build_configuration(self, context: interfaces.context.ContextInterface, config_path: str,
_: Any) -> interfaces.configuration.HierarchicalDict:
def build_configuration(
self, context: interfaces.context.ContextInterface, config_path: str, _: Any
) -> interfaces.configuration.HierarchicalDict:
result = interfaces.configuration.HierarchicalDict()
num_elem_config_path = interfaces.configuration.path_join(config_path, self.name, 'number_of_elements')
num_elem_config_path = interfaces.configuration.path_join(
config_path, self.name, "number_of_elements"
)
num_elements = context.config.get(num_elem_config_path, None)
if num_elements is not None:
result["number_of_elements"] = num_elements
for i in range(num_elements):
req = self.new_requirement(i)
self.add_requirement(req)
value_path = interfaces.configuration.path_join(config_path, self.name, req.name)
value_path = interfaces.configuration.path_join(
config_path, self.name, req.name
)
value = context.config.get(value_path, None)
if value is not None:
result.splice(req.name, context.layers[value].build_configuration())
@@ -201,11 +247,15 @@ class ComplexListRequirement(MultiRequirement,
class LayerListRequirement(ComplexListRequirement):
"""Allows a variable length list of layers that must exist."""
def construct(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def construct(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
"""Method for constructing within the context any required elements
from subrequirements."""
new_config_path = interfaces.configuration.path_join(config_path, self.name)
num_layers_path = interfaces.configuration.path_join(new_config_path, "number_of_elements")
num_layers_path = interfaces.configuration.path_join(
new_config_path, "number_of_elements"
)
number_of_layers = context.config[num_layers_path]
if not isinstance(number_of_layers, int):
@@ -214,28 +264,36 @@ class LayerListRequirement(ComplexListRequirement):
# Build all the layers that can be built
for i in range(number_of_layers):
layer_req = self.requirements.get(self.name + str(i), None)
if layer_req is not None and isinstance(layer_req, TranslationLayerRequirement):
if layer_req is not None and isinstance(
layer_req, TranslationLayerRequirement
):
layer_req.construct(context, new_config_path)
def new_requirement(self, index) -> interfaces.configuration.RequirementInterface:
"""Constructs a new requirement based on the specified index."""
return TranslationLayerRequirement(name = self.name + str(index),
description = "Layer for swap space",
optional = False)
return TranslationLayerRequirement(
name=self.name + str(index),
description="Layer for swap space",
optional=False,
)
class TranslationLayerRequirement(interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface):
class TranslationLayerRequirement(
interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface,
):
"""Class maintaining the limitations on what sort of translation layers are
acceptable."""
def __init__(self,
name: str,
description: str = None,
default: interfaces.configuration.ConfigSimpleType = None,
optional: bool = False,
oses: List = None,
architectures: List = None) -> None:
def __init__(
self,
name: str,
description: str = None,
default: interfaces.configuration.ConfigSimpleType = None,
optional: bool = False,
oses: List = None,
architectures: List = None,
) -> None:
"""Constructs a Translation Layer Requirement.
The configuration option's value will be the name of the layer once it exists in the store
@@ -256,28 +314,46 @@ class TranslationLayerRequirement(interfaces.configuration.ConstructableRequirem
self.architectures = architectures
super().__init__(name, description, default, optional)
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Validate that the value is a valid layer name and that the layer
adheres to the requirements."""
config_path = interfaces.configuration.path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
if isinstance(value, str):
if value not in context.layers:
vollog.log(constants.LOGLEVEL_V, f"IndexError - Layer not found in memory space: {value}")
vollog.log(
constants.LOGLEVEL_V,
f"IndexError - Layer not found in memory space: {value}",
)
return {config_path: self}
if self.oses and context.layers[value].metadata.get('os', None) not in self.oses:
vollog.log(constants.LOGLEVEL_V, f"TypeError - Layer is not the required OS: {value}")
if (
self.oses
and context.layers[value].metadata.get("os", None) not in self.oses
):
vollog.log(
constants.LOGLEVEL_V,
f"TypeError - Layer is not the required OS: {value}",
)
return {config_path: self}
if (self.architectures
and context.layers[value].metadata.get('architecture', None) not in self.architectures):
vollog.log(constants.LOGLEVEL_V, f"TypeError - Layer is not the required Architecture: {value}")
if (
self.architectures
and context.layers[value].metadata.get("architecture", None)
not in self.architectures
):
vollog.log(
constants.LOGLEVEL_V,
f"TypeError - Layer is not the required Architecture: {value}",
)
return {config_path: self}
return {}
if value is not None:
vollog.log(constants.LOGLEVEL_V,
f"TypeError - Translation Layer Requirement only accepts string labels: {repr(value)}")
vollog.log(
constants.LOGLEVEL_V,
f"TypeError - Translation Layer Requirement only accepts string labels: {repr(value)}",
)
return {config_path: self}
# TODO: check that the space in the context lives up to the requirements for arch/os etc
@@ -285,10 +361,15 @@ class TranslationLayerRequirement(interfaces.configuration.ConstructableRequirem
### NOTE: This validate method has side effects (the dependencies can change)!!!
self._validate_class(context, interfaces.configuration.parent_path(config_path))
vollog.log(constants.LOGLEVEL_V, f"IndexError - No configuration provided: {config_path}")
vollog.log(
constants.LOGLEVEL_V,
f"IndexError - No configuration provided: {config_path}",
)
return {config_path: self}
def construct(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def construct(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
"""Constructs the appropriate layer and adds it based on the class
parameter."""
config_path = interfaces.configuration.path_join(config_path, self.name)
@@ -303,8 +384,12 @@ class TranslationLayerRequirement(interfaces.configuration.ConstructableRequirem
args = {"context": context, "config_path": config_path, "name": name}
if any(
[subreq.unsatisfied(context, config_path) for subreq in self.requirements.values() if
not subreq.optional]):
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
):
return None
obj = self._construct_class(context, config_path, args)
@@ -314,42 +399,57 @@ class TranslationLayerRequirement(interfaces.configuration.ConstructableRequirem
# context.config[config_path] = obj.name
return None
def build_configuration(self, context: interfaces.context.ContextInterface, _: str,
value: Any) -> interfaces.configuration.HierarchicalDict:
def build_configuration(
self, context: interfaces.context.ContextInterface, _: str, value: Any
) -> interfaces.configuration.HierarchicalDict:
"""Builds the appropriate configuration for the specified
requirement."""
return context.layers[value].build_configuration()
class SymbolTableRequirement(interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface):
class SymbolTableRequirement(
interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface,
):
"""Class maintaining the limitations on what sort of symbol spaces are
acceptable."""
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Validate that the value is a valid within the symbol space of the
provided context."""
config_path = interfaces.configuration.path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
if not isinstance(value, str) and value is not None:
vollog.log(constants.LOGLEVEL_V,
f"TypeError - SymbolTableRequirement only accepts string labels: {repr(value)}")
vollog.log(
constants.LOGLEVEL_V,
f"TypeError - SymbolTableRequirement only accepts string labels: {repr(value)}",
)
return {config_path: self}
if value and value in context.symbol_space:
# This is an expected situation, so return rather than raise
return {}
elif value:
vollog.log(constants.LOGLEVEL_V, "IndexError - Value not present in the symbol space: {}".format(value
or ""))
vollog.log(
constants.LOGLEVEL_V,
"IndexError - Value not present in the symbol space: {}".format(
value or ""
),
)
### NOTE: This validate method has side effects (the dependencies can change)!!!
self._validate_class(context, interfaces.configuration.parent_path(config_path))
vollog.log(constants.LOGLEVEL_V, f"Symbol table requirement not yet fulfilled: {config_path}")
vollog.log(
constants.LOGLEVEL_V,
f"Symbol table requirement not yet fulfilled: {config_path}",
)
return {config_path: self}
def construct(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def construct(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
"""Constructs the symbol space within the context based on the
subrequirements."""
config_path = interfaces.configuration.path_join(config_path, self.name)
@@ -359,14 +459,23 @@ class SymbolTableRequirement(interfaces.configuration.ConstructableRequirementIn
args = {"context": context, "config_path": config_path, "name": name}
if any(
[subreq.unsatisfied(context, config_path) for subreq in self.requirements.values() if
not subreq.optional]):
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
):
return None
# Fill out the parameter for class creation
if not isinstance(self.requirements["class"], interfaces.configuration.ClassRequirement):
raise TypeError("Class requirement is not of type ClassRequirement: {}".format(
repr(self.requirements["class"])))
if not isinstance(
self.requirements["class"], interfaces.configuration.ClassRequirement
):
raise TypeError(
"Class requirement is not of type ClassRequirement: {}".format(
repr(self.requirements["class"])
)
)
cls = self.requirements["class"].cls
if cls is None:
return None
@@ -380,23 +489,27 @@ class SymbolTableRequirement(interfaces.configuration.ConstructableRequirementIn
context.symbol_space.append(obj)
return None
def build_configuration(self, context: interfaces.context.ContextInterface, _: str,
value: Any) -> interfaces.configuration.HierarchicalDict:
def build_configuration(
self, context: interfaces.context.ContextInterface, _: str, value: Any
) -> interfaces.configuration.HierarchicalDict:
"""Builds the appropriate configuration for the specified
requirement."""
return context.symbol_space[value].build_configuration()
class VersionRequirement(interfaces.configuration.RequirementInterface):
def __init__(self,
name: str,
description: str = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
version: Optional[Tuple[int, ...]] = None) -> None:
super().__init__(name = name, description = description, default = default, optional = optional)
def __init__(
self,
name: str,
description: str = None,
default: bool = False,
optional: bool = False,
component: Type[interfaces.configuration.VersionableInterface] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
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
@@ -404,17 +517,22 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
raise TypeError("Version cannot be None")
self._version = version
def unsatisfied(self, context: interfaces.context.ContextInterface,
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: interfaces.context.ContextInterface, config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
# Mypy doesn't appreciate our classproperty implementation, self._plugin.version has no type
config_path = interfaces.configuration.path_join(config_path, self.name)
if not self.matches_required(self._version, self._component.version):
return {config_path: self}
context.config[interfaces.configuration.path_join(config_path, self.name)] = True
context.config[
interfaces.configuration.path_join(config_path, self.name)
] = True
return {}
@classmethod
def matches_required(cls, required: Tuple[int, ...], version: Tuple[int, int, int]) -> bool:
def matches_required(
cls, required: Tuple[int, ...], version: Tuple[int, int, int]
) -> bool:
if len(required) > 0 and version[0] != required[0]:
return False
if len(required) > 1 and version[1] < required[1]:
@@ -423,60 +541,87 @@ class VersionRequirement(interfaces.configuration.RequirementInterface):
class PluginRequirement(VersionRequirement):
def __init__(self,
name: str,
description: str = None,
default: bool = False,
optional: bool = False,
plugin: Type[interfaces.plugins.PluginInterface] = None,
version: Optional[Tuple[int, ...]] = None) -> None:
super().__init__(name = name,
description = description,
default = default,
optional = optional,
component = plugin,
version = version)
def __init__(
self,
name: str,
description: str = None,
default: bool = False,
optional: bool = False,
plugin: Type[interfaces.plugins.PluginInterface] = None,
version: Optional[Tuple[int, ...]] = None,
) -> None:
super().__init__(
name=name,
description=description,
default=default,
optional=optional,
component=plugin,
version=version,
)
class ModuleRequirement(interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface):
def __init__(self, name: str, description: str = None, default: bool = False,
architectures: Optional[List[str]] = None, optional: bool = False):
super().__init__(name = name, description = description, default = default, optional = optional)
self.add_requirement(TranslationLayerRequirement(name = 'layer_name', architectures = architectures))
self.add_requirement(SymbolTableRequirement(name = 'symbol_table_name'))
class ModuleRequirement(
interfaces.configuration.ConstructableRequirementInterface,
interfaces.configuration.ConfigurableRequirementInterface,
):
def __init__(
self,
name: str,
description: str = None,
default: bool = False,
architectures: Optional[List[str]] = None,
optional: bool = False,
):
super().__init__(
name=name, description=description, default=default, optional=optional
)
self.add_requirement(
TranslationLayerRequirement(name="layer_name", architectures=architectures)
)
self.add_requirement(SymbolTableRequirement(name="symbol_table_name"))
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
IntRequirement(name = 'offset'),
IntRequirement(name="offset"),
]
def unsatisfied(self, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, interfaces.configuration.RequirementInterface]:
def unsatisfied(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, interfaces.configuration.RequirementInterface]:
"""Validate that the value is a valid module"""
config_path = interfaces.configuration.path_join(config_path, self.name)
value = self.config_value(context, config_path, None)
if isinstance(value, str):
if value not in context.modules:
vollog.log(constants.LOGLEVEL_V, f"IndexError - Module not found in context: {value}")
vollog.log(
constants.LOGLEVEL_V,
f"IndexError - Module not found in context: {value}",
)
return {config_path: self}
return {}
if value is not None:
vollog.log(constants.LOGLEVEL_V,
"TypeError - Module Requirement only accepts string labels: {}".format(repr(value)))
vollog.log(
constants.LOGLEVEL_V,
"TypeError - Module Requirement only accepts string labels: {}".format(
repr(value)
),
)
return {config_path: self}
result = {}
for subreq in self._requirements:
req_unsatisfied = self._requirements[subreq].unsatisfied(context, config_path)
req_unsatisfied = self._requirements[subreq].unsatisfied(
context, config_path
)
if req_unsatisfied:
result.update(req_unsatisfied)
if not result:
vollog.log(constants.LOGLEVEL_V, f"IndexError - No configuration provided: {config_path}")
vollog.log(
constants.LOGLEVEL_V,
f"IndexError - No configuration provided: {config_path}",
)
result = {config_path: self}
### NOTE: This validate method has side effects (the dependencies can change)!!!
@@ -485,7 +630,9 @@ class ModuleRequirement(interfaces.configuration.ConstructableRequirementInterfa
return result
def construct(self, context: interfaces.context.ContextInterface, config_path: str) -> None:
def construct(
self, context: interfaces.context.ContextInterface, config_path: str
) -> None:
"""Constructs the appropriate layer and adds it based on the class parameter."""
config_path = interfaces.configuration.path_join(config_path, self.name)
@@ -499,8 +646,12 @@ class ModuleRequirement(interfaces.configuration.ConstructableRequirementInterfa
args = {"context": context, "config_path": config_path, "name": name}
if any(
[subreq.unsatisfied(context, config_path) for subreq in self.requirements.values() if
not subreq.optional]):
[
subreq.unsatisfied(context, config_path)
for subreq in self.requirements.values()
if not subreq.optional
]
):
return None
obj = self._construct_class(context, config_path, args)
@@ -510,8 +661,9 @@ class ModuleRequirement(interfaces.configuration.ConstructableRequirementInterfa
# context.config[config_path] = obj.name
return None
def build_configuration(self, context: 'interfaces.context.ContextInterface', _: str,
value: Any) -> interfaces.configuration.HierarchicalDict:
def build_configuration(
self, context: "interfaces.context.ContextInterface", _: str, value: Any
) -> interfaces.configuration.HierarchicalDict:
"""Builds the appropriate configuration for the specified
requirement."""
return context.modules[value].build_configuration()
+48 -14
View File
@@ -9,6 +9,7 @@ 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
@@ -16,23 +17,27 @@ import volatility3.framework.constants.windows
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "plugins")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins"))
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "plugins")),
]
"""Default list of paths to load plugins from (volatility3/plugins and volatility3/framework/plugins)"""
SYMBOL_BASEPATHS = [
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "symbols")),
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "symbols"))
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "symbols")),
]
"""Default list of paths to load symbols from (volatility3/symbols and volatility3/framework/symbols)"""
ISF_EXTENSIONS = ['.json', '.json.xz', '.json.gz', '.json.bz2']
ISF_EXTENSIONS = [".json", ".json.xz", ".json.gz", ".json.bz2"]
"""List of accepted extensions for ISF files"""
if hasattr(sys, 'frozen') and sys.frozen:
if hasattr(sys, "frozen") and sys.frozen:
# Ensure we include the executable's directory as the base for plugins and symbols
PLUGINS_PATH = [os.path.abspath(os.path.join(os.path.dirname(sys.executable), 'plugins'))] + PLUGINS_PATH
SYMBOL_BASEPATHS = [os.path.abspath(os.path.join(os.path.dirname(sys.executable), 'symbols'))] + SYMBOL_BASEPATHS
PLUGINS_PATH = [
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "plugins"))
] + PLUGINS_PATH
SYMBOL_BASEPATHS = [
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "symbols"))
] + SYMBOL_BASEPATHS
BANG = "!"
"""Constant used to delimit table names from type names when referring to a symbol"""
@@ -40,15 +45,18 @@ BANG = "!"
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 4 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_PATCH = 1 # Number of changes that do not change the interface
VERSION_SUFFIX = ""
# TODO: At version 2.0.0, remove the symbol_shift feature
PACKAGE_VERSION = ".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]]) + VERSION_SUFFIX
PACKAGE_VERSION = (
".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]])
+ VERSION_SUFFIX
)
"""The canonical version of the volatility3 package"""
AUTOMAGIC_CONFIG_PATH = 'automagic'
AUTOMAGIC_CONFIG_PATH = "automagic"
"""The root section within the context configuration for automagic values"""
LOGLEVEL_V = 9
@@ -63,12 +71,14 @@ LOGLEVEL_VVVV = 6
CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3")
"""Default path to store cached data"""
SQLITE_CACHE_PERIOD = '-1 month'
SQLITE_CACHE_PERIOD = "-3 days"
"""SQLite time modifier for how long each item is valid in the cache for"""
if sys.platform == 'win32':
CACHE_PATH = os.path.realpath(os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3"))
os.makedirs(CACHE_PATH, exist_ok = True)
if sys.platform == "win32":
CACHE_PATH = os.path.realpath(
os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3")
)
os.makedirs(CACHE_PATH, exist_ok=True)
IDENTIFIERS_FILENAME = "identifier.cache"
"""Default location to record information about available identifiers"""
@@ -81,12 +91,13 @@ BUG_URL = "https://github.com/volatilityfoundation/volatility3/issues"
ProgressCallback = Optional[Callable[[float, str], None]]
"""Type information for ProgressCallback objects"""
OS_CATEGORIES = ['windows', 'mac', 'linux']
OS_CATEGORIES = ["windows", "mac", "linux"]
class Parallelism(enum.IntEnum):
"""An enumeration listing the different types of parallelism applied to
volatility."""
Off = 0
Threading = 1
Multiprocessing = 2
@@ -104,3 +115,26 @@ OFFLINE = False
REMOTE_ISF_URL = None # 'http://localhost:8000/banners.json'
"""Remote URL to query for a list of ISF addresses"""
###
# DEPRECATED VALUES
###
_deprecated_LINUX_BANNERS_FILENAME = os.path.join(CACHE_PATH, "linux_banners.cache")
"""This value is deprecated and is no longer used within volatility"""
_deprecated_MAC_BANNERS_PATH = os.path.join(CACHE_PATH, "mac_banners.cache")
"""This value is deprecated and is no longer used within volatility"""
_deprecated_IDENTIFIERS_PATH = os.path.join(CACHE_PATH, IDENTIFIERS_FILENAME)
"""This value is deprecated in favour of CACHE_PATH joined to IDENTIFIER_FILENAME"""
def __getattr__(name):
deprecated_tag = "_deprecated_"
if name in [
x[len(deprecated_tag) :] for x in globals() if x.startswith(deprecated_tag)
]:
warnings.warn(f"{name} is deprecated", FutureWarning)
return globals()[f"{deprecated_tag}{name}"]
return None
+151 -97
View File
@@ -87,12 +87,14 @@ class Context(interfaces.context.ContextInterface):
# ## Object Factory Functions
def object(self,
object_type: Union[str, interfaces.objects.Template],
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
**arguments) -> interfaces.objects.ObjectInterface:
def object(
self,
object_type: Union[str, interfaces.objects.Template],
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
**arguments,
) -> interfaces.objects.ObjectInterface:
"""Object factory, takes a context, symbol, offset and optional
layername.
@@ -122,18 +124,24 @@ class Context(interfaces.context.ContextInterface):
object_template = object_template.clone()
object_template.update_vol(**arguments)
return object_template(context = self,
object_info = interfaces.objects.ObjectInformation(layer_name = layer_name,
offset = offset,
native_layer_name = native_layer_name,
size = object_template.size))
return object_template(
context=self,
object_info=interfaces.objects.ObjectInformation(
layer_name=layer_name,
offset=offset,
native_layer_name=native_layer_name,
size=object_template.size,
),
)
def module(self,
module_name: str,
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
size: Optional[int] = None) -> interfaces.context.ModuleInterface:
def module(
self,
module_name: str,
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
size: Optional[int] = None,
) -> interfaces.context.ModuleInterface:
"""Constructs a new os-independent module.
Args:
@@ -144,17 +152,21 @@ class Context(interfaces.context.ContextInterface):
size: The size, in bytes, that the module occupies from offset location within the layer named layer_name
"""
if size:
return SizedModule.create(self,
module_name = module_name,
layer_name = layer_name,
offset = offset,
size = size,
native_layer_name = native_layer_name)
return Module.create(self,
module_name = module_name,
layer_name = layer_name,
offset = offset,
native_layer_name = native_layer_name)
return SizedModule.create(
self,
module_name=module_name,
layer_name=layer_name,
offset=offset,
size=size,
native_layer_name=native_layer_name,
)
return Module.create(
self,
module_name=module_name,
layer_name=layer_name,
offset=offset,
native_layer_name=native_layer_name,
)
def get_module_wrapper(method: str) -> Callable:
@@ -169,7 +181,13 @@ def get_module_wrapper(method: str) -> Callable:
raise ValueError(f"Cannot reference another module when calling {method}")
return getattr(self._context.symbol_space, method)(name)
for entry in ['__annotations__', '__doc__', '__module__', '__name__', '__qualname__']:
for entry in [
"__annotations__",
"__doc__",
"__module__",
"__name__",
"__qualname__",
]:
proxy_interface = getattr(interfaces.context.ModuleInterface, method)
if hasattr(proxy_interface, entry):
setattr(wrapper, entry, getattr(proxy_interface, entry))
@@ -178,26 +196,27 @@ def get_module_wrapper(method: str) -> Callable:
class Module(interfaces.context.ModuleInterface):
@classmethod
def create(cls,
context: interfaces.context.ContextInterface,
module_name: str,
layer_name: str,
offset: int,
**kwargs) -> 'Module':
def create(
cls,
context: interfaces.context.ContextInterface,
module_name: str,
layer_name: str,
offset: int,
**kwargs,
) -> "Module":
pathjoin = interfaces.configuration.path_join
# Check if config_path is None
free_module_name = context.modules.free_module_name(module_name)
config_path = kwargs.get('config_path', None)
config_path = kwargs.get("config_path", None)
if config_path is None:
config_path = pathjoin('temporary', 'modules', free_module_name)
config_path = pathjoin("temporary", "modules", free_module_name)
# Populate the configuration
context.config[pathjoin(config_path, 'layer_name')] = layer_name
context.config[pathjoin(config_path, 'offset')] = offset
context.config[pathjoin(config_path, "layer_name")] = layer_name
context.config[pathjoin(config_path, "offset")] = offset
# This is important, since the module_name may be changed in case it is already in use
if 'symbol_table_name' not in kwargs:
kwargs['symbol_table_name'] = module_name
if "symbol_table_name" not in kwargs:
kwargs["symbol_table_name"] = module_name
for arg in kwargs:
context.config[pathjoin(config_path, arg)] = kwargs.get(arg, None)
# Construct the object
@@ -207,12 +226,14 @@ class Module(interfaces.context.ModuleInterface):
# Add the module to the context modules collection
return return_val
def object(self,
object_type: str,
offset: int = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs) -> 'interfaces.objects.ObjectInterface':
def object(
self,
object_type: str,
offset: int = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object created using the symbol_table_name and layer_name
of the Module.
@@ -225,7 +246,9 @@ class Module(interfaces.context.ModuleInterface):
if constants.BANG not in object_type:
object_type = self.symbol_table_name + constants.BANG + object_type
else:
raise ValueError("Cannot reference another module when constructing an object")
raise ValueError(
"Cannot reference another module when constructing an object"
)
if offset is None:
raise TypeError("Offset must not be None for non-symbol objects")
@@ -234,19 +257,23 @@ class Module(interfaces.context.ModuleInterface):
offset += self._offset
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if 'layer_name' in kwargs:
del kwargs['layer_name']
return self._context.object(object_type = object_type,
layer_name = self._layer_name,
offset = offset,
native_layer_name = native_layer_name or self._native_layer_name,
**kwargs)
if "layer_name" in kwargs:
del kwargs["layer_name"]
return self._context.object(
object_type=object_type,
layer_name=self._layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
**kwargs,
)
def object_from_symbol(self,
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs) -> 'interfaces.objects.ObjectInterface':
def object_from_symbol(
self,
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object based on a specific symbol (containing type and
offset information) and the layer_name of the Module. This will throw
a ValueError if the symbol does not contain an associated type, or if
@@ -261,7 +288,9 @@ class Module(interfaces.context.ModuleInterface):
if constants.BANG not in symbol_name:
symbol_name = self.symbol_table_name + constants.BANG + symbol_name
else:
raise ValueError("Cannot reference another module when constructing an object")
raise ValueError(
"Cannot reference another module when constructing an object"
)
# Only set the offset if type is Symbol and we were given a name, not a template
symbol_val = self._context.symbol_space.get_symbol(symbol_name)
@@ -274,15 +303,17 @@ class Module(interfaces.context.ModuleInterface):
raise TypeError(f"Symbol {symbol_val.name} has no associated type")
# Ensure we don't use a layer_name other than the module's, why would anyone do that?
if 'layer_name' in kwargs:
del kwargs['layer_name']
if "layer_name" in kwargs:
del kwargs["layer_name"]
# Since type may be a template, we don't just call our own module method
return self._context.object(object_type = symbol_val.type,
layer_name = self._layer_name,
offset = offset,
native_layer_name = native_layer_name or self._native_layer_name,
**kwargs)
return self._context.object(
object_type=symbol_val.type,
layer_name=self._layer_name,
offset=offset,
native_layer_name=native_layer_name or self._native_layer_name,
**kwargs,
)
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
"""Returns the symbols within this module that live at the specified
@@ -290,28 +321,30 @@ class Module(interfaces.context.ModuleInterface):
if size < 0:
raise ValueError("Size must be strictly non-negative")
return list(
self._context.symbol_space.get_symbols_by_location(offset = offset - self._offset,
size = size,
table_name = self.symbol_table_name))
self._context.symbol_space.get_symbols_by_location(
offset=offset - self._offset,
size=size,
table_name=self.symbol_table_name,
)
)
@property
def symbols(self):
return self.context.symbol_space[self.symbol_table_name].symbols
get_symbol = get_module_wrapper('get_symbol')
get_type = get_module_wrapper('get_type')
get_enumeration = get_module_wrapper('get_enumeration')
has_symbol = get_module_wrapper('has_symbol')
has_type = get_module_wrapper('has_type')
has_enumeration = get_module_wrapper('has_enumeration')
get_symbol = get_module_wrapper("get_symbol")
get_type = get_module_wrapper("get_type")
get_enumeration = get_module_wrapper("get_enumeration")
has_symbol = get_module_wrapper("has_symbol")
has_type = get_module_wrapper("has_type")
has_enumeration = get_module_wrapper("has_enumeration")
class SizedModule(Module):
@property
def size(self) -> int:
"""Returns the size of the module (0 for unknown size)"""
size = self.config.get('size', 0)
size = self.config.get("size", 0)
return size or 0
@property # type: ignore # FIXME: mypy #5107
@@ -326,8 +359,12 @@ class SizedModule(Module):
layer = self._context.layers[self.layer_name]
if not isinstance(layer, interfaces.layers.TranslationLayerInterface):
raise TypeError("Hashing modules on non-TranslationLayers is not allowed")
return hashlib.md5(bytes(str(list(layer.mapping(self.offset, self.size, ignore_errors = True))),
'utf-8')).hexdigest()
return hashlib.md5(
bytes(
str(list(layer.mapping(self.offset, self.size, ignore_errors=True))),
"utf-8",
)
).hexdigest()
def get_symbols_by_absolute_location(self, offset: int, size: int = 0) -> List[str]:
"""Returns the symbols within this module that live at the specified
@@ -341,10 +378,12 @@ class ModuleCollection(interfaces.context.ModuleContainer):
"""Class to contain a collection of SizedModules and reason about their
contents."""
def __init__(self, modules: Optional[List[interfaces.context.ModuleInterface]] = None) -> None:
def __init__(
self, modules: Optional[List[interfaces.context.ModuleInterface]] = None
) -> None:
super().__init__(modules)
def deduplicate(self) -> 'ModuleCollection':
def deduplicate(self) -> "ModuleCollection":
"""Returns a new deduplicated ModuleCollection featuring no repeated
modules (based on data hash)
@@ -367,14 +406,17 @@ class ModuleCollection(interfaces.context.ModuleContainer):
return prefix + str(count)
@property
def modules(self) -> 'ModuleCollection':
def modules(self) -> "ModuleCollection":
"""A name indexed dictionary of modules using that name in this
collection."""
vollog.warning(
"This method has been deprecated in favour of the ModuleCollection acting as a dictionary itself")
"This method has been deprecated in favour of the ModuleCollection acting as a dictionary itself"
)
return self
def get_module_symbols_by_absolute_location(self, offset: int, size: int = 0) -> Iterable[Tuple[str, List[str]]]:
def get_module_symbols_by_absolute_location(
self, offset: int, size: int = 0
) -> Iterable[Tuple[str, List[str]]]:
"""Returns a tuple of (module_name, list_of_symbol_names) for each
module, where symbols live at the absolute offset in memory
provided."""
@@ -383,16 +425,28 @@ class ModuleCollection(interfaces.context.ModuleContainer):
for module_name in self._modules:
module = self._modules[module_name]
if isinstance(module, SizedModule):
if (offset <= module.offset + module.size) and (offset + size >= module.offset):
yield (module.name, module.get_symbols_by_absolute_location(offset, size))
if (offset <= module.offset + module.size) and (
offset + size >= module.offset
):
yield (
module.name,
module.get_symbols_by_absolute_location(offset, size),
)
class ConfigurableModule(Module, interfaces.configuration.ConfigurableInterface):
def __init__(self, context: interfaces.context.ContextInterface, config_path: str, name: str) -> None:
interfaces.configuration.ConfigurableInterface.__init__(self, context, config_path)
layer_name = self.config['layer_name']
offset = self.config['offset']
symbol_table_name = self.config['symbol_table_name']
interfaces.configuration.ConfigurableInterface.__init__(self, context, config_path)
Module.__init__(self, context, name, layer_name, offset, symbol_table_name, layer_name)
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
) -> None:
interfaces.configuration.ConfigurableInterface.__init__(
self, context, config_path
)
layer_name = self.config["layer_name"]
offset = self.config["offset"]
symbol_table_name = self.config["symbol_table_name"]
interfaces.configuration.ConfigurableInterface.__init__(
self, context, config_path
)
Module.__init__(
self, context, name, layer_name, offset, symbol_table_name, layer_name
)
+24 -8
View File
@@ -30,7 +30,9 @@ class PluginRequirementException(VolatilityException):
class SymbolError(VolatilityException):
"""Thrown when a symbol lookup has failed."""
def __init__(self, symbol_name: Optional[str], table_name: Optional[str], *args) -> None:
def __init__(
self, symbol_name: Optional[str], table_name: Optional[str], *args
) -> None:
super().__init__(*args)
self.symbol_name = symbol_name
self.table_name = table_name
@@ -63,7 +65,14 @@ class PagedInvalidAddressException(InvalidAddressException):
that are invalid
"""
def __init__(self, layer_name: str, invalid_address: int, invalid_bits: int, entry: int, *args) -> None:
def __init__(
self,
layer_name: str,
invalid_address: int,
invalid_bits: int,
entry: int,
*args,
) -> None:
super().__init__(layer_name, invalid_address, *args)
self.invalid_bits = invalid_bits
self.entry = entry
@@ -77,8 +86,15 @@ class SwappedInvalidAddressException(PagedInvalidAddressException):
the lookup that were invalid.
"""
def __init__(self, layer_name: str, invalid_address: int, invalid_bits: int, entry: int, swap_offset: int,
*args) -> None:
def __init__(
self,
layer_name: str,
invalid_address: int,
invalid_bits: int,
entry: int,
swap_offset: int,
*args,
) -> None:
super().__init__(layer_name, invalid_address, invalid_bits, entry, *args)
self.swap_offset = swap_offset
@@ -88,14 +104,14 @@ class SymbolSpaceError(VolatilityException):
class UnsatisfiedException(VolatilityException):
def __init__(self, unsatisfied: Dict[str, interfaces.configuration.RequirementInterface]) -> None:
def __init__(
self, unsatisfied: Dict[str, interfaces.configuration.RequirementInterface]
) -> None:
super().__init__()
self.unsatisfied = unsatisfied
class MissingModuleException(VolatilityException):
def __init__(self, module: str, *args) -> None:
super().__init__(*args)
self.module = module
@@ -109,4 +125,4 @@ class OfflineException(VolatilityException):
self._url = url
def __str__(self):
return f'Volatility 3 is offline: unable to access {self._url}'
return f"Volatility 3 is offline: unable to access {self._url}"
+10 -2
View File
@@ -12,5 +12,13 @@ components of volatility to write plugins.
# Import the submodules we want people to be able to use without importing them themselves
# 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
from volatility3.framework.interfaces import (
renderers,
configuration,
context,
layers,
objects,
plugins,
symbols,
automagic,
)
+54 -26
View File
@@ -17,7 +17,9 @@ from volatility3.framework.configuration import requirements
vollog = logging.getLogger(__name__)
class AutomagicInterface(interfaces.configuration.ConfigurableInterface, metaclass = ABCMeta):
class AutomagicInterface(
interfaces.configuration.ConfigurableInterface, metaclass=ABCMeta
):
"""Class that defines an automagic component that can help fulfill
`Requirements`
@@ -43,33 +45,52 @@ class AutomagicInterface(interfaces.configuration.ConfigurableInterface, metacla
exclusion_list = []
"""A list of plugin categories (typically operating systems) which the plugin will not operate on"""
def __init__(self, context: interfaces.context.ContextInterface, config_path: str, *args, **kwargs) -> None:
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
*args,
**kwargs
) -> None:
super().__init__(context, config_path)
for requirement in self.get_requirements():
if not isinstance(requirement, (interfaces.configuration.SimpleTypeRequirement,
requirements.ChoiceRequirement, requirements.ListRequirement,
requirements.VersionRequirement)):
if not isinstance(
requirement,
(
interfaces.configuration.SimpleTypeRequirement,
requirements.ChoiceRequirement,
requirements.ListRequirement,
requirements.VersionRequirement,
),
):
raise TypeError(
"Automagic requirements must be a SimpleTypeRequirement, ChoiceRequirement, ListRequirement or VersionRequirement")
"Automagic requirements must be a SimpleTypeRequirement, ChoiceRequirement, ListRequirement or VersionRequirement"
)
def __call__(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None) -> Optional[List[Any]]:
def __call__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement: interfaces.configuration.RequirementInterface,
progress_callback: constants.ProgressCallback = None,
) -> Optional[List[Any]]:
"""Runs the automagic over the configurable."""
return []
# TODO: requirement_type can be made UnionType[Type[T], Tuple[Type[T], ...]]
# once mypy properly supports Tuples in instance
def find_requirements(self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement_root: interfaces.configuration.RequirementInterface,
requirement_type: Union[Tuple[Type[interfaces.configuration.RequirementInterface], ...],
Type[interfaces.configuration.RequirementInterface]],
shortcut: bool = True) -> List[Tuple[str, interfaces.configuration.RequirementInterface]]:
def find_requirements(
self,
context: interfaces.context.ContextInterface,
config_path: str,
requirement_root: interfaces.configuration.RequirementInterface,
requirement_type: Union[
Tuple[Type[interfaces.configuration.RequirementInterface], ...],
Type[interfaces.configuration.RequirementInterface],
],
shortcut: bool = True,
) -> List[Tuple[str, interfaces.configuration.RequirementInterface]]:
"""Determines if there is actually an unfulfilled `Requirement`
waiting.
@@ -85,7 +106,9 @@ class AutomagicInterface(interfaces.configuration.ConfigurableInterface, metacla
Returns:
A list of tuples containing the config_path, sub_config_path and requirement identifying the unsatisfied `Requirements`
"""
sub_config_path = interfaces.configuration.path_join(config_path, requirement_root.name)
sub_config_path = interfaces.configuration.path_join(
config_path, requirement_root.name
)
results: List[Tuple[str, interfaces.configuration.RequirementInterface]] = []
recurse = not shortcut
if isinstance(requirement_root, requirement_type):
@@ -95,11 +118,13 @@ class AutomagicInterface(interfaces.configuration.ConfigurableInterface, metacla
recurse = True
if recurse:
for subreq in requirement_root.requirements.values():
results += self.find_requirements(context, sub_config_path, subreq, requirement_type, shortcut)
results += self.find_requirements(
context, sub_config_path, subreq, requirement_type, shortcut
)
return results
class StackerLayerInterface(metaclass = ABCMeta):
class StackerLayerInterface(metaclass=ABCMeta):
"""Class that takes a lower layer and attempts to build on it.
stack_order determines the order (from low to high) that stacking
@@ -113,10 +138,12 @@ class StackerLayerInterface(metaclass = ABCMeta):
"""The list operating systems/first-level plugin hierarchy that should exclude this stacker"""
@classmethod
def stack(self,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Method to determine whether this builder can operate on the named
layer. If so, modify the context appropriately.
@@ -135,4 +162,5 @@ class StackerLayerInterface(metaclass = ABCMeta):
@classmethod
def stacker_slow_warning(cls):
vollog.warning(
"Reads to this layer are slow, it's recommended to use the layerwriter plugin once to produce a raw file")
"Reads to this layer are slow, it's recommended to use the layerwriter plugin once to produce a raw file"
)
+141 -67
View File
@@ -23,7 +23,19 @@ import random
import string
import sys
from abc import ABCMeta, abstractmethod
from typing import Any, ClassVar, Dict, Generator, Iterator, List, Optional, Type, Union, Tuple, Set
from typing import (
Any,
ClassVar,
Dict,
Generator,
Iterator,
List,
Optional,
Type,
Union,
Tuple,
Set,
)
from volatility3 import classproperty, framework
from volatility3.framework import constants, interfaces
@@ -68,9 +80,11 @@ class HierarchicalDict(collections.abc.Mapping):
"""The core of configuration data, it is a mapping class that stores keys
within itself, and also stores lower hierarchies."""
def __init__(self,
initial_dict: Dict[str, 'SimpleTypeRequirement'] = None,
separator: str = CONFIG_SEPARATOR) -> None:
def __init__(
self,
initial_dict: Dict[str, "SimpleTypeRequirement"] = None,
separator: str = CONFIG_SEPARATOR,
) -> None:
"""
Args:
initial_dict: A dictionary to populate the HierarchicalDict with initially
@@ -80,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):
@@ -88,7 +102,8 @@ class HierarchicalDict(collections.abc.Mapping):
self[k] = v
elif initial_dict is not None:
raise TypeError(
f"Initial_dict must be a dictionary or JSON string containing a dictionary: {initial_dict}")
f"Initial_dict must be a dictionary or JSON string containing a dictionary: {initial_dict}"
)
def __eq__(self, other):
"""Define equality between HierarchicalDicts"""
@@ -109,7 +124,7 @@ class HierarchicalDict(collections.abc.Mapping):
"""Returns the first division of a key based on the dict separator, or
the full key if the separator is not present."""
if self.separator in key:
return key[:key.index(self.separator)]
return key[: key.index(self.separator)]
else:
return key
@@ -117,8 +132,8 @@ class HierarchicalDict(collections.abc.Mapping):
"""Returns all but the first division of a key based on the dict
separator, or None if the separator is not in the key."""
if self.separator in key:
return key[key.index(self.separator) + 1:]
return ''
return key[key.index(self.separator) + 1 :]
return ""
def __iter__(self) -> Iterator[Any]:
"""Returns an iterator object that supports the iterator protocol."""
@@ -156,7 +171,9 @@ class HierarchicalDict(collections.abc.Mapping):
def _setitem(self, key: str, value: Any, is_data: bool = True) -> None:
"""Set an item or appends a whole subtree at a key location."""
if self.separator in key:
subdict = self._subdict.get(self._key_head(key), HierarchicalDict(separator = self.separator))
subdict = self._subdict.get(
self._key_head(key), HierarchicalDict(separator=self.separator)
)
subdict._setitem(self._key_tail(key), value, is_data)
self._subdict[self._key_head(key)] = subdict
else:
@@ -166,7 +183,9 @@ class HierarchicalDict(collections.abc.Mapping):
if not isinstance(value, HierarchicalDict):
raise TypeError(
"HierarchicalDicts can only store HierarchicalDicts within their structure: {}".format(
type(value)))
type(value)
)
)
self._subdict[key] = value
def _sanitize_value(self, value: Any) -> ConfigSimpleType:
@@ -185,7 +204,9 @@ class HierarchicalDict(collections.abc.Mapping):
for element in value:
element_value = self._sanitize_value(element)
if isinstance(element_value, list):
raise TypeError("Configuration list types cannot contain list types")
raise TypeError(
"Configuration list types cannot contain list types"
)
if element_value is not None:
new_list.append(element_value)
return new_list
@@ -220,7 +241,7 @@ class HierarchicalDict(collections.abc.Mapping):
"""Returns the length of all items."""
return len(self._data) + sum([len(subdict) for subdict in self._subdict])
def branch(self, key: str) -> 'HierarchicalDict':
def branch(self, key: str) -> "HierarchicalDict":
"""Returns the HierarchicalDict housed under the key.
This differs from the data property, in that it is directed by the `key`, and all layers under that key are
@@ -241,10 +262,12 @@ class HierarchicalDict(collections.abc.Mapping):
else:
return self._subdict[key]
except KeyError:
self._setitem(key = key, value = HierarchicalDict(separator = self.separator), is_data = False)
self._setitem(
key=key, value=HierarchicalDict(separator=self.separator), is_data=False
)
return HierarchicalDict()
def splice(self, key: str, value: 'HierarchicalDict') -> None:
def splice(self, key: str, value: "HierarchicalDict") -> None:
"""Splices an existing HierarchicalDictionary under a specific key.
This can be thought of as an inverse of :func:`branch`, although
@@ -255,7 +278,9 @@ class HierarchicalDict(collections.abc.Mapping):
raise TypeError("Splice requires a string key and HierarchicalDict value")
self._setitem(key, value, False)
def merge(self, key: str, value: 'HierarchicalDict', overwrite: bool = False) -> None:
def merge(
self, key: str, value: "HierarchicalDict", overwrite: bool = False
) -> None:
"""Acts similarly to splice, but maintains previous values.
If overwrite is true, then entries in the new value are used over those that exist within key already
@@ -274,7 +299,7 @@ class HierarchicalDict(collections.abc.Mapping):
else:
self[key + self._separator + item] = value[item]
def clone(self) -> 'HierarchicalDict':
def clone(self) -> "HierarchicalDict":
"""Duplicates the configuration, allowing changes without affecting the
original.
@@ -285,10 +310,12 @@ class HierarchicalDict(collections.abc.Mapping):
def __str__(self) -> str:
"""Turns the Hierarchical dict into a string representation."""
return json.dumps(dict([(key, self[key]) for key in sorted(self.generator())]), indent = 2)
return json.dumps(
dict([(key, self[key]) for key in sorted(self.generator())]), indent=2
)
class RequirementInterface(metaclass = ABCMeta):
class RequirementInterface(metaclass=ABCMeta):
"""Class that defines a requirement.
A requirement is a means for plugins and other framework components to request specific configuration data.
@@ -300,11 +327,13 @@ class RequirementInterface(metaclass = ABCMeta):
as :class:`TranslationLayerRequirement`, :class:`SymbolTableRequirement` and :class:`ClassRequirement`
"""
def __init__(self,
name: str,
description: str = None,
default: ConfigSimpleType = None,
optional: bool = False) -> None:
def __init__(
self,
name: str,
description: str = None,
default: ConfigSimpleType = None,
optional: bool = False,
) -> None:
"""
Args:
@@ -315,7 +344,9 @@ class RequirementInterface(metaclass = ABCMeta):
"""
super().__init__()
if CONFIG_SEPARATOR in name:
raise ValueError(f"Name cannot contain the config-hierarchy divider ({CONFIG_SEPARATOR})")
raise ValueError(
f"Name cannot contain the config-hierarchy divider ({CONFIG_SEPARATOR})"
)
self._name = name
self._description = description or ""
self._default = default
@@ -363,10 +394,12 @@ class RequirementInterface(metaclass = ABCMeta):
"""Sets the optional value for a requirement."""
self._optional = bool(value)
def config_value(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
default: ConfigSimpleType = None) -> ConfigSimpleType:
def config_value(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
default: ConfigSimpleType = None,
) -> ConfigSimpleType:
"""Returns the value for this Requirement from its config path.
Args:
@@ -378,12 +411,12 @@ class RequirementInterface(metaclass = ABCMeta):
# Child operations
@property
def requirements(self) -> Dict[str, 'RequirementInterface']:
def requirements(self) -> Dict[str, "RequirementInterface"]:
"""Returns a dictionary of all the child requirements, indexed by
name."""
return self._requirements.copy()
def add_requirement(self, requirement: 'RequirementInterface') -> None:
def add_requirement(self, requirement: "RequirementInterface") -> None:
"""Adds a child to the list of requirements.
Args:
@@ -391,7 +424,7 @@ class RequirementInterface(metaclass = ABCMeta):
"""
self._requirements[requirement.name] = requirement
def remove_requirement(self, requirement: 'RequirementInterface') -> None:
def remove_requirement(self, requirement: "RequirementInterface") -> None:
"""Removes a child from the list of requirements.
Args:
@@ -399,8 +432,9 @@ class RequirementInterface(metaclass = ABCMeta):
"""
del self._requirements[requirement.name]
def unsatisfied_children(self, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, 'RequirementInterface']:
def unsatisfied_children(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, "RequirementInterface"]:
"""Method that will validate all child requirements.
Args:
@@ -413,14 +447,17 @@ class RequirementInterface(metaclass = ABCMeta):
result = {}
for requirement in self.requirements.values():
if not requirement.optional:
subresult = requirement.unsatisfied(context, path_join(config_path, self._name))
subresult = requirement.unsatisfied(
context, path_join(config_path, self._name)
)
result.update(subresult)
return result
# Validation routines
@abstractmethod
def unsatisfied(self, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, 'RequirementInterface']:
def unsatisfied(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, "RequirementInterface"]:
"""Method to validate the value stored at config_path for the
configuration object against a context.
@@ -438,6 +475,7 @@ class RequirementInterface(metaclass = ABCMeta):
class SimpleTypeRequirement(RequirementInterface):
"""Class to represent a single simple type (such as a boolean, a string, an
integer or a series of bytes)"""
instance_type: ClassVar[Type] = bool
def add_requirement(self, requirement: RequirementInterface):
@@ -450,8 +488,9 @@ class SimpleTypeRequirement(RequirementInterface):
children."""
raise TypeError("Instance Requirements cannot have subrequirements")
def unsatisfied(self, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, RequirementInterface]:
def unsatisfied(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, RequirementInterface]:
"""Validates the instance requirement based upon its
`instance_type`."""
config_path = path_join(config_path, self.name)
@@ -460,8 +499,10 @@ 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)))
"TypeError - {} requirements only accept {} type: {}".format(
self.name, self.instance_type.__name__, repr(value)
),
)
return {config_path: self}
return {}
@@ -489,8 +530,9 @@ class ClassRequirement(RequirementInterface):
class name."""
return self._cls
def unsatisfied(self, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, RequirementInterface]:
def unsatisfied(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, RequirementInterface]:
"""Checks to see if a class can be recovered."""
config_path = path_join(config_path, self.name)
@@ -499,8 +541,8 @@ class ClassRequirement(RequirementInterface):
if value is not None and isinstance(value, str):
if "." in value:
# TODO: consider importing the prefix
module = sys.modules.get(value[:value.rindex(".")], None)
class_name = value[value.rindex(".") + 1:]
module = sys.modules.get(value[: value.rindex(".")], None)
class_name = value[value.rindex(".") + 1 :]
if hasattr(module, class_name):
self._cls = getattr(module, class_name)
else:
@@ -528,7 +570,9 @@ class ConstructableRequirementInterface(RequirementInterface):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.add_requirement(ClassRequirement("class", "Class of the constructable requirement"))
self.add_requirement(
ClassRequirement("class", "Class of the constructable requirement")
)
self._current_class_requirements: Set[Any] = set()
def __eq__(self, other):
@@ -537,7 +581,9 @@ class ConstructableRequirementInterface(RequirementInterface):
return super().__eq__(other)
@abstractmethod
def construct(self, context: 'interfaces.context.ContextInterface', config_path: str) -> None:
def construct(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> None:
"""Method for constructing within the context any required elements
from subrequirements.
@@ -546,7 +592,9 @@ class ConstructableRequirementInterface(RequirementInterface):
config_path: The configuration path for the specific instance of this constructable
"""
def _validate_class(self, context: 'interfaces.context.ContextInterface', config_path: str) -> None:
def _validate_class(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> None:
"""Method to check if the class Requirement is valid and if so populate
the other requirements (but no need to validate, since we're invalid
already)
@@ -555,9 +603,11 @@ class ConstructableRequirementInterface(RequirementInterface):
context: The context object containing the configuration data for the constructable
config_path: The configuration path for the specific instance of this constructable
"""
class_req = self.requirements['class']
class_req = self.requirements["class"]
subreq_config_path = path_join(config_path, self.name)
if not class_req.unsatisfied(context, subreq_config_path) and isinstance(class_req, ClassRequirement):
if not class_req.unsatisfied(context, subreq_config_path) and isinstance(
class_req, ClassRequirement
):
# We have a class, and since it's validated we can construct our requirements from it
if issubclass(class_req.cls, ConfigurableInterface):
# In case the class has changed, clear out the old requirements
@@ -569,10 +619,12 @@ class ConstructableRequirementInterface(RequirementInterface):
self._current_class_requirements.add(requirement.name)
self.add_requirement(requirement)
def _construct_class(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
requirement_dict: Dict[str, object] = None) -> Optional['interfaces.objects.ObjectInterface']:
def _construct_class(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
requirement_dict: Dict[str, object] = None,
) -> Optional["interfaces.objects.ObjectInterface"]:
"""Constructs the class, handing args and the subrequirements as
parameters to __init__"""
if self.requirements["class"].unsatisfied(context, config_path):
@@ -605,16 +657,22 @@ class ConstructableRequirementInterface(RequirementInterface):
class ConfigurableRequirementInterface(RequirementInterface):
"""Simple Abstract class to provide build_required_config."""
def build_configuration(self, context: 'interfaces.context.ContextInterface', config_path: str,
value: Any) -> HierarchicalDict:
def build_configuration(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
value: Any,
) -> HierarchicalDict:
"""Proxies to a ConfigurableInterface if necessary."""
class ConfigurableInterface(metaclass = ABCMeta):
class ConfigurableInterface(metaclass=ABCMeta):
"""Class to allow objects to have requirements and read configuration data
from the context config tree."""
def __init__(self, context: 'interfaces.context.ContextInterface', config_path: str) -> None:
def __init__(
self, context: "interfaces.context.ContextInterface", config_path: str
) -> None:
"""Basic initializer that allows configurables to access their own
config settings."""
super().__init__()
@@ -623,7 +681,7 @@ class ConfigurableInterface(metaclass = ABCMeta):
self._config_cache: Optional[HierarchicalDict] = None
@property
def context(self) -> 'interfaces.context.ContextInterface':
def context(self) -> "interfaces.context.ContextInterface":
"""The context object that this configurable belongs to/configuration
is stored in."""
return self._context
@@ -660,11 +718,16 @@ class ConfigurableInterface(metaclass = ABCMeta):
for req in self.get_requirements():
value = self.config.get(req.name, None)
# Do not include the name of constructed classes
if value is not None and not isinstance(req, ConstructableRequirementInterface):
if value is not None and not isinstance(
req, ConstructableRequirementInterface
):
result[req.name] = value
if isinstance(req, ConfigurableRequirementInterface):
if value is not None:
result.splice(req.name, req.build_configuration(self.context, self.config_path, value))
result.splice(
req.name,
req.build_configuration(self.context, self.config_path, value),
)
return result
@classmethod
@@ -674,8 +737,9 @@ class ConfigurableInterface(metaclass = ABCMeta):
return []
@classmethod
def unsatisfied(cls, context: 'interfaces.context.ContextInterface',
config_path: str) -> Dict[str, RequirementInterface]:
def unsatisfied(
cls, context: "interfaces.context.ContextInterface", config_path: str
) -> Dict[str, RequirementInterface]:
"""Returns a list of the names of all unsatisfied requirements.
Since a satisfied set of requirements will return [], it can be used in tests as follows:
@@ -694,7 +758,12 @@ class ConfigurableInterface(metaclass = ABCMeta):
return result
@classmethod
def make_subconfig(cls, context: 'interfaces.context.ContextInterface', base_config_path: str, **kwargs) -> str:
def make_subconfig(
cls,
context: "interfaces.context.ContextInterface",
base_config_path: str,
**kwargs,
) -> str:
"""Convenience function to allow constructing a new randomly generated
sub-configuration path, containing each element from kwargs.
@@ -706,8 +775,10 @@ class ConfigurableInterface(metaclass = ABCMeta):
Returns:
str: The newly generated full configuration path
"""
random_config_dict = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8))
random_config_dict = "".join(
random.SystemRandom().choice(string.ascii_uppercase + string.digits)
for _ in range(8)
)
new_config_path = path_join(base_config_path, random_config_dict)
# TODO: Check that the new_config_path is empty, although it's not critical if it's not since the values are merged in
@@ -716,7 +787,9 @@ class ConfigurableInterface(metaclass = ABCMeta):
# constructor anyway, however, to prevent bad types getting into the config tree we just verify that v is a simple type
for k, v in kwargs.items():
if not isinstance(v, (int, str, bool, float, bytes)):
raise TypeError("Config values passed to make_subconfig can only be simple types")
raise TypeError(
"Config values passed to make_subconfig can only be simple types"
)
context.config[path_join(new_config_path, k)] = v
return new_config_path
@@ -729,6 +802,7 @@ class VersionableInterface:
All version number should use semantic versioning
"""
_version: Tuple[int, int, int] = (0, 0, 0)
_required_framework_version: Tuple[int, int, int] = (0, 0, 0)
+64 -49
View File
@@ -19,7 +19,7 @@ from typing import Optional, Union, Dict, List, Iterable
from volatility3.framework import interfaces, exceptions
class ContextInterface(metaclass = ABCMeta):
class ContextInterface(metaclass=ABCMeta):
"""All context-like objects must adhere to the following interface.
This interface is present to avoid import dependency cycles.
@@ -32,12 +32,12 @@ class ContextInterface(metaclass = ABCMeta):
@property
@abstractmethod
def config(self) -> 'interfaces.configuration.HierarchicalDict':
def config(self) -> "interfaces.configuration.HierarchicalDict":
"""Returns the configuration object for this context."""
@property
@abstractmethod
def symbol_space(self) -> 'interfaces.symbols.SymbolSpaceInterface':
def symbol_space(self) -> "interfaces.symbols.SymbolSpaceInterface":
"""Returns the symbol_space for the context.
This object must support the :class:`~volatility3.framework.interfaces.symbols.SymbolSpaceInterface`
@@ -47,11 +47,11 @@ class ContextInterface(metaclass = ABCMeta):
@property
@abstractmethod
def modules(self) -> 'ModuleContainer':
def modules(self) -> "ModuleContainer":
"""Returns the memory object for the context."""
raise NotImplementedError("ModuleContainer has not been implemented.")
def add_module(self, module: 'interfaces.context.ModuleInterface'):
def add_module(self, module: "interfaces.context.ModuleInterface"):
"""Adds a named module to the context.
Args:
@@ -65,11 +65,11 @@ class ContextInterface(metaclass = ABCMeta):
@property
@abstractmethod
def layers(self) -> 'interfaces.layers.LayerContainer':
def layers(self) -> "interfaces.layers.LayerContainer":
"""Returns the memory object for the context."""
raise NotImplementedError("LayerContainer has not been implemented.")
def add_layer(self, layer: 'interfaces.layers.DataLayerInterface'):
def add_layer(self, layer: "interfaces.layers.DataLayerInterface"):
"""Adds a named translation layer to the context memory.
Args:
@@ -80,12 +80,14 @@ class ContextInterface(metaclass = ABCMeta):
# ## Object Factory Functions
@abstractmethod
def object(self,
object_type: Union[str, 'interfaces.objects.Template'],
layer_name: str,
offset: int,
native_layer_name: str = None,
**arguments):
def object(
self,
object_type: Union[str, "interfaces.objects.Template"],
layer_name: str,
offset: int,
native_layer_name: str = None,
**arguments,
):
"""Object factory, takes a context, symbol, offset and optional
layer_name.
@@ -102,7 +104,7 @@ class ContextInterface(metaclass = ABCMeta):
A fully constructed object
"""
def clone(self) -> 'ContextInterface':
def clone(self) -> "ContextInterface":
"""Produce a clone of the context (and configuration), allowing
modifications to be made without affecting any mutable objects in the
original.
@@ -112,12 +114,14 @@ class ContextInterface(metaclass = ABCMeta):
"""
return copy.deepcopy(self)
def module(self,
module_name: str,
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
size: Optional[int] = None) -> 'ModuleInterface':
def module(
self,
module_name: str,
layer_name: str,
offset: int,
native_layer_name: Optional[str] = None,
size: Optional[int] = None,
) -> "ModuleInterface":
"""Create a module object.
A module object is associated with a symbol table, and acts like a context, but offsets locations by a known value
@@ -142,10 +146,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
This object is OS-independent.
"""
def __init__(self,
context: ContextInterface,
config_path: str,
name: str) -> None:
def __init__(self, context: ContextInterface, config_path: str, name: str) -> None:
"""Constructs a new os-independent module.
Args:
@@ -158,35 +159,43 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
@property
def _layer_name(self) -> str:
return self.config['layer_name']
return self.config["layer_name"]
@property
def _offset(self) -> int:
return self.config['offset']
return self.config["offset"]
@property
def _native_layer_name(self) -> str:
return self.config.get('native_layer_name', self._layer_name)
return self.config.get("native_layer_name", self._layer_name)
@property
def _symbol_table_name(self) -> str:
return self.config.get('symbol_table_name', self._module_name)
return self.config.get("symbol_table_name", self._module_name)
def build_configuration(self) -> 'interfaces.configuration.HierarchicalDict':
def build_configuration(self) -> "interfaces.configuration.HierarchicalDict":
"""Builds the configuration dictionary for this specific Module"""
config = super().build_configuration()
config['offset'] = self.config['offset']
subconfigs = {'symbol_table_name': self.context.symbol_space[self.symbol_table_name].build_configuration(),
'layer_name': self.context.layers[self.layer_name].build_configuration()}
config["offset"] = self.config["offset"]
subconfigs = {
"symbol_table_name": self.context.symbol_space[
self.symbol_table_name
].build_configuration(),
"layer_name": self.context.layers[self.layer_name].build_configuration(),
}
if self.layer_name != self._native_layer_name:
subconfigs['native_layer_name'] = self.context.layers[self._native_layer_name].build_configuration()
subconfigs["native_layer_name"] = self.context.layers[
self._native_layer_name
].build_configuration()
for subconfig in subconfigs:
for req in subconfigs[subconfig]:
config[interfaces.configuration.path_join(subconfig, req)] = subconfigs[subconfig][req]
config[interfaces.configuration.path_join(subconfig, req)] = subconfigs[
subconfig
][req]
return config
@@ -217,12 +226,14 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
return self._symbol_table_name
@abstractmethod
def object(self,
object_type: str,
offset: int = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs) -> 'interfaces.objects.ObjectInterface':
def object(
self,
object_type: str,
offset: int = None,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object created using the symbol_table_name and layer_name
of the Module.
@@ -237,11 +248,13 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
"""
@abstractmethod
def object_from_symbol(self,
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs) -> 'interfaces.objects.ObjectInterface':
def object_from_symbol(
self,
symbol_name: str,
native_layer_name: Optional[str] = None,
absolute: bool = False,
**kwargs,
) -> "interfaces.objects.ObjectInterface":
"""Returns an object created using the symbol_table_name and layer_name
of the Module.
@@ -259,13 +272,13 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
symbol = self.get_symbol(name)
return self.offset + symbol.address
def get_type(self, name: str) -> 'interfaces.objects.Template':
def get_type(self, name: str) -> "interfaces.objects.Template":
"""Returns a type from the module's symbol table."""
def get_symbol(self, name: str) -> 'interfaces.symbols.SymbolInterface':
def get_symbol(self, name: str) -> "interfaces.symbols.SymbolInterface":
"""Returns a symbol object from the module's symbol table."""
def get_enumeration(self, name: str) -> 'interfaces.objects.Template':
def get_enumeration(self, name: str) -> "interfaces.objects.Template":
"""Returns an enumeration from the module's symbol table."""
def has_type(self, name: str) -> bool:
@@ -306,7 +319,9 @@ class ModuleContainer(collections.abc.Mapping):
module: the module to add to the list of modules (based on module.name)
"""
if module.name in self._modules:
raise exceptions.VolatilityException(f"Module already exists: {module.name}")
raise exceptions.VolatilityException(
f"Module already exists: {module.name}"
)
self._modules[module.name] = module
def __delitem__(self, name: str) -> None:
+169 -70
View File
@@ -22,11 +22,13 @@ from volatility3.framework import constants, exceptions, interfaces
vollog = logging.getLogger(__name__)
ProgressValue = Union['DummyProgress', multiprocessing.managers.ValueProxy]
ProgressValue = Union["DummyProgress", multiprocessing.managers.ValueProxy]
IteratorValue = Tuple[List[Tuple[str, int, int]], int]
class ScannerInterface(interfaces.configuration.VersionableInterface, metaclass = ABCMeta):
class ScannerInterface(
interfaces.configuration.VersionableInterface, metaclass=ABCMeta
):
"""Class for layer scanners that return locations of particular values from
within the data.
@@ -52,6 +54,7 @@ class ScannerInterface(interfaces.configuration.VersionableInterface, metaclass
in either their own class or the context. This will allow the scanner to be run
in parallel against multiple blocks.
"""
thread_safe = False
_required_framework_version = (2, 0, 0)
@@ -64,11 +67,11 @@ class ScannerInterface(interfaces.configuration.VersionableInterface, metaclass
self._layer_name: Optional[str] = None
@property
def context(self) -> Optional['interfaces.context.ContextInterface']:
def context(self) -> Optional["interfaces.context.ContextInterface"]:
return self._context
@context.setter
def context(self, ctx: 'interfaces.context.ContextInterface') -> None:
def context(self, ctx: "interfaces.context.ContextInterface") -> None:
"""Stores the context locally in case the scanner needs to access the
layer."""
self._context = ctx
@@ -94,20 +97,24 @@ class ScannerInterface(interfaces.configuration.VersionableInterface, metaclass
"""
class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metaclass = ABCMeta):
class DataLayerInterface(
interfaces.configuration.ConfigurableInterface, metaclass=ABCMeta
):
"""A Layer that directly holds data (and does not translate it).
This is effectively a leaf node in a layer tree. It directly
accesses a data source and exposes it within volatility.
"""
_direct_metadata: Mapping = {'architecture': 'Unknown', 'os': 'Unknown'}
_direct_metadata: Mapping = {"architecture": "Unknown", "os": "Unknown"}
def __init__(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
def __init__(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(context, config_path)
self._name = name
self._metadata = metadata or {}
@@ -199,11 +206,13 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
# ## General scanning methods
def scan(self,
context: interfaces.context.ContextInterface,
scanner: ScannerInterface,
progress_callback: constants.ProgressCallback = None,
sections: Iterable[Tuple[int, int]] = None) -> Iterable[Any]:
def scan(
self,
context: interfaces.context.ContextInterface,
scanner: ScannerInterface,
progress_callback: constants.ProgressCallback = None,
sections: Iterable[Tuple[int, int]] = None,
) -> Iterable[Any]:
"""Scans a Translation layer by chunk.
Note: this will skip missing/unmappable chunks of memory
@@ -224,7 +233,9 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
scanner.layer_name = self.name
if sections is None:
sections = [(self.minimum_address, self.maximum_address - self.minimum_address)]
sections = [
(self.minimum_address, self.maximum_address - self.minimum_address)
]
sections = list(self._coalesce_sections(sections))
@@ -232,13 +243,18 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
progress: ProgressValue = DummyProgress()
scan_iterator = functools.partial(self._scan_iterator, scanner, sections)
scan_metric = self._scan_metric(scanner, sections)
if not scanner.thread_safe or constants.PARALLELISM == constants.Parallelism.Off:
if (
not scanner.thread_safe
or constants.PARALLELISM == constants.Parallelism.Off
):
progress = DummyProgress()
scan_chunk = functools.partial(self._scan_chunk, scanner, progress)
for value in scan_iterator():
if progress_callback:
progress_callback(scan_metric(progress.value),
f"Scanning {self.name} using {scanner.__class__.__name__}")
progress_callback(
scan_metric(progress.value),
f"Scanning {self.name} using {scanner.__class__.__name__}",
)
yield from scan_chunk(value)
else:
progress = multiprocessing.Manager().Value("Q", 0)
@@ -252,8 +268,10 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
while not result.ready():
if progress_callback:
# Run the progress_callback
progress_callback(scan_metric(progress.value),
f"Scanning {self.name} using {scanner.__class__.__name__}")
progress_callback(
scan_metric(progress.value),
f"Scanning {self.name} using {scanner.__class__.__name__}",
)
# Ensures we don't burn CPU cycles going round in a ready waiting loop
# without delaying the user too long between progress updates/results
result.wait(0.1)
@@ -262,10 +280,16 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
except Exception as e:
# We don't care the kind of exception, so catch and report on everything, yielding nothing further
vollog.debug(f"Scan Failure: {str(e)}")
vollog.log(constants.LOGLEVEL_VVV,
"\n".join(traceback.TracebackException.from_exception(e).format(chain = True)))
vollog.log(
constants.LOGLEVEL_VVV,
"\n".join(
traceback.TracebackException.from_exception(e).format(chain=True)
),
)
def _coalesce_sections(self, sections: Iterable[Tuple[int, int]]) -> Iterable[Tuple[int, int]]:
def _coalesce_sections(
self, sections: Iterable[Tuple[int, int]]
) -> Iterable[Tuple[int, int]]:
"""Take a list of (start, length) sections and coalesce any adjacent
sections."""
result: List[Tuple[int, int]] = []
@@ -283,7 +307,10 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
if first_start + first_length < self.minimum_address:
result = result[1:]
elif first_start < self.minimum_address:
result[0] = (self.minimum_address, (first_start + first_length) - self.minimum_address)
result[0] = (
self.minimum_address,
(first_start + first_length) - self.minimum_address,
)
while result and result[-1] > (self.maximum_address, 0):
last_start, last_length = result[-1]
if last_start > self.maximum_address:
@@ -292,8 +319,9 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
result[1] = (last_start, self.maximum_address - last_start)
return result
def _scan_iterator(self, scanner: 'ScannerInterface', sections: Iterable[Tuple[int,
int]]) -> Iterable[IteratorValue]:
def _scan_iterator(
self, scanner: "ScannerInterface", sections: Iterable[Tuple[int, int]]
) -> Iterable[IteratorValue]:
"""Iterator that indicates which blocks in the layer are to be read by
for the scanning.
@@ -303,7 +331,12 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
assumed to have no holes
"""
for section_start, section_length in sections:
offset, mapped_offset, length, layer_name = section_start, section_start, section_length, self.name
offset, mapped_offset, length, layer_name = (
section_start,
section_start,
section_length,
self.name,
)
while length > 0:
chunk_size = min(length, scanner.chunk_size + scanner.overlap)
yield [(layer_name, mapped_offset, chunk_size)], offset + chunk_size
@@ -315,16 +348,23 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
offset += chunk_size
# We ignore the type due to the iterator_value, actually it only needs to match the output from _scan_iterator
def _scan_chunk(self, scanner: 'ScannerInterface', progress: 'ProgressValue',
iterator_value: IteratorValue) -> List[Any]:
def _scan_chunk(
self,
scanner: "ScannerInterface",
progress: "ProgressValue",
iterator_value: IteratorValue,
) -> List[Any]:
data_to_scan, chunk_end = iterator_value
data = b''
data = b""
for layer_name, address, chunk_size in data_to_scan:
try:
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))
vollog.debug(
"Invalid address in layer {} found scanning {} at address {:x}".format(
layer_name, self.name, address
)
)
if len(data) > scanner.chunk_size + scanner.overlap:
vollog.debug(f"Scan chunk too large: {hex(len(data))}")
@@ -332,7 +372,9 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
progress.value = chunk_end
return list(scanner(data, chunk_end - len(data)))
def _scan_metric(self, _scanner: 'ScannerInterface', sections: List[Tuple[int, int]]) -> Callable[[int], float]:
def _scan_metric(
self, _scanner: "ScannerInterface", sections: List[Tuple[int, int]]
) -> Callable[[int], float]:
if not sections:
raise ValueError("Sections have no size, nothing to scan")
@@ -357,11 +399,15 @@ class DataLayerInterface(interfaces.configuration.ConfigurableInterface, metacla
@property
def metadata(self) -> Mapping:
"""Returns a ReadOnly copy of the metadata published by this layer."""
maps = [self.context.layers[layer_name].metadata for layer_name in self.dependencies]
return interfaces.objects.ReadOnlyMapping(collections.ChainMap(self._metadata, self._direct_metadata, *maps))
maps = [
self.context.layers[layer_name].metadata for layer_name in self.dependencies
]
return interfaces.objects.ReadOnlyMapping(
collections.ChainMap(self._metadata, self._direct_metadata, *maps)
)
class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
class TranslationLayerInterface(DataLayerInterface, metaclass=ABCMeta):
"""Provides a layer that translates or transforms another layer or layers.
Translation layers always depend on another layer (typically
@@ -370,10 +416,9 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
"""
@abstractmethod
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
"""Returns a sorted iterable of (offset, sublength, mapped_offset, mapped_length, layer)
mappings.
@@ -390,7 +435,9 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
"""Returns a list of layer names that this layer translates onto."""
return []
def _decode_data(self, data: bytes, mapped_offset: int, offset: int, output_length: int) -> bytes:
def _decode_data(
self, data: bytes, mapped_offset: int, offset: int, output_length: int
) -> bytes:
"""Decodes any necessary data. Note, additional data may need to be read from the lower layer, such as lookup
tables or similar. The data provided to this layer is purely that data which encompasses the requested data
range.
@@ -405,7 +452,9 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
The data to be read from the underlying layer."""
return data
def _encode_data(self, layer_name: str, mapped_offset: int, offset: int, value: bytes) -> bytes:
def _encode_data(
self, layer_name: str, mapped_offset: int, offset: int, value: bytes
) -> bytes:
"""Encodes any necessary data.
Args:
@@ -420,28 +469,41 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
# ## Read/Write functions for mapped pages
@functools.lru_cache(maxsize = 512)
@functools.lru_cache(maxsize=512)
def read(self, offset: int, length: int, pad: bool = False) -> bytes:
"""Reads an offset for length bytes and returns 'bytes' (not 'str') of
length size."""
current_offset = offset
output: bytes = b''
for (layer_offset, sublength, mapped_offset, mapped_length, layer) in self.mapping(offset,
length,
ignore_errors = pad):
output: bytes = b""
for (
layer_offset,
sublength,
mapped_offset,
mapped_length,
layer,
) in self.mapping(offset, length, ignore_errors=pad):
if not pad and layer_offset > current_offset:
raise exceptions.InvalidAddressException(
self.name, current_offset, f"Layer {self.name} cannot map offset: {current_offset}")
self.name,
current_offset,
f"Layer {self.name} cannot map offset: {current_offset}",
)
elif layer_offset > current_offset:
output += b"\x00" * (layer_offset - current_offset)
current_offset = layer_offset
# The layer_offset can be less than the current_offset in non-linearly mapped layers
# it does not suggest an overlap, but that the data is in an encoded block
if mapped_length > 0:
unprocessed_data = self._context.layers.read(layer, mapped_offset, mapped_length, pad)
processed_data = self._decode_data(unprocessed_data, mapped_offset, layer_offset, sublength)
unprocessed_data = self._context.layers.read(
layer, mapped_offset, mapped_length, pad
)
processed_data = self._decode_data(
unprocessed_data, mapped_offset, layer_offset, sublength
)
if len(processed_data) != sublength:
raise ValueError("ProcessedData length does not match expected length of chunk")
raise ValueError(
"ProcessedData length does not match expected length of chunk"
)
output += processed_data
current_offset += sublength
return output + (b"\x00" * (length - len(output)))
@@ -451,21 +513,36 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
underlying mapping."""
current_offset = offset
length = len(value)
for (layer_offset, sublength, mapped_offset, mapped_length, layer) in self.mapping(offset, length):
for (
layer_offset,
sublength,
mapped_offset,
mapped_length,
layer,
) in self.mapping(offset, length):
if layer_offset > current_offset:
raise exceptions.InvalidAddressException(
self.name, current_offset, f"Layer {self.name} cannot map offset: {current_offset}")
self.name,
current_offset,
f"Layer {self.name} cannot map offset: {current_offset}",
)
value_chunk = value[layer_offset - offset:layer_offset - offset + sublength]
new_data = self._encode_data(layer, mapped_offset, layer_offset, value_chunk)
value_chunk = value[
layer_offset - offset : layer_offset - offset + sublength
]
new_data = self._encode_data(
layer, mapped_offset, layer_offset, value_chunk
)
self._context.layers.write(layer, mapped_offset, new_data)
current_offset += len(new_data)
def _scan_iterator(self,
scanner: 'ScannerInterface',
sections: Iterable[Tuple[int, int]],
linear: bool = False) -> Iterable[IteratorValue]:
def _scan_iterator(
self,
scanner: "ScannerInterface",
sections: Iterable[Tuple[int, int]],
linear: bool = False,
) -> Iterable[IteratorValue]:
"""Iterator that indicates which blocks in the layer are to be read by
for the scanning.
@@ -483,7 +560,9 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
# For each section, find out which bits of its exists and where they map to
# This is faster than cutting the entire space into scan_chunk sized blocks and then
# finding out what exists (particularly if most of the space isn't mapped)
for mapped in self.mapping(section_start, section_length, ignore_errors = True):
for mapped in self.mapping(
section_start, section_length, ignore_errors=True
):
offset, sublength, mapped_offset, mapped_length, layer_name = mapped
# Setup the variables for this block
@@ -506,7 +585,10 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
# Halfway through a chunk, finish the chunk, then take more
if chunk_position != chunk_start:
chunk_size = min(chunk_position - chunk_start, scanner.chunk_size + scanner.overlap)
chunk_size = min(
chunk_position - chunk_start,
scanner.chunk_size + scanner.overlap,
)
output += [(return_name, chunk_position + conversion, chunk_size)]
chunk_start = chunk_position + chunk_size
chunk_position = chunk_start
@@ -519,8 +601,12 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
chunk_position = chunk_start
# Take from chunk_position as far as the block can go,
# or as much left of a scanner chunk as we can
chunk_size = min(block_end - chunk_position,
scanner.chunk_size + scanner.overlap - (chunk_position - chunk_start))
chunk_size = min(
block_end - chunk_position,
scanner.chunk_size
+ scanner.overlap
- (chunk_position - chunk_start),
)
output += [(return_name, chunk_position + conversion, chunk_size)]
chunk_start = chunk_position + chunk_size
chunk_position = chunk_start
@@ -568,12 +654,20 @@ class LayerContainer(collections.abc.Mapping):
layer: the layer to add to the list of layers (based on layer.name)
"""
if layer.name in self._layers:
raise exceptions.LayerException(layer.name, f"Layer already exists: {layer.name}")
raise exceptions.LayerException(
layer.name, f"Layer already exists: {layer.name}"
)
if isinstance(layer, TranslationLayerInterface):
missing_list = [sublayer for sublayer in layer.dependencies if sublayer not in self._layers]
missing_list = [
sublayer
for sublayer in layer.dependencies
if sublayer not in self._layers
]
if missing_list:
raise exceptions.LayerException(
layer.name, f"Layer {layer.name} has unmet dependencies: {', '.join(missing_list)}")
layer.name,
f"Layer {layer.name} has unmet dependencies: {', '.join(missing_list)}",
)
self._layers[layer.name] = layer
def del_layer(self, name: str) -> None:
@@ -585,11 +679,16 @@ class LayerContainer(collections.abc.Mapping):
name: The name of the layer to delete
"""
for layer in self._layers:
depend_list = [superlayer for superlayer in self._layers if name in self._layers[layer].dependencies]
depend_list = [
superlayer
for superlayer in self._layers
if name in self._layers[layer].dependencies
]
if depend_list:
raise exceptions.LayerException(
self._layers[layer].name,
f"Layer {self._layers[layer].name} is depended upon: {', '.join(depend_list)}")
f"Layer {self._layers[layer].name} is depended upon: {', '.join(depend_list)}",
)
self._layers[name].destroy()
del self._layers[name]
+86 -51
View File
@@ -28,11 +28,13 @@ class ReadOnlyMapping(collections.abc.Mapping):
def __getattr__(self, attr: str) -> Any:
"""Returns the item as an attribute."""
if attr == '_dict':
if attr == "_dict":
return super().__getattribute__(attr)
if attr in self._dict:
return self._dict[attr]
raise AttributeError(f"Object has no attribute: {self.__class__.__name__}.{attr}")
raise AttributeError(
f"Object has no attribute: {self.__class__.__name__}.{attr}"
)
def __getitem__(self, name: str) -> Any:
"""Returns the item requested."""
@@ -61,13 +63,15 @@ class ObjectInformation(ReadOnlyMapping):
in a single place. These values are based on the :class:`ReadOnlyMapping` class, to prevent their modification.
"""
def __init__(self,
layer_name: str,
offset: int,
member_name: Optional[str] = None,
parent: Optional['ObjectInterface'] = None,
native_layer_name: Optional[str] = None,
size: Optional[int] = None):
def __init__(
self,
layer_name: str,
offset: int,
member_name: Optional[str] = None,
parent: Optional["ObjectInterface"] = None,
native_layer_name: Optional[str] = None,
size: Optional[int] = None,
):
"""Constructs a container for basic information about an object.
Args:
@@ -78,22 +82,29 @@ class ObjectInformation(ReadOnlyMapping):
native_layer_name: If this object references other objects (such as a pointer), what layer those objects live in
size: The size that the whole structure consumes in bytes
"""
super().__init__({
'layer_name': layer_name,
'offset': offset,
'member_name': member_name,
'parent': parent,
'native_layer_name': native_layer_name or layer_name,
'size': size
})
super().__init__(
{
"layer_name": layer_name,
"offset": offset,
"member_name": member_name,
"parent": parent,
"native_layer_name": native_layer_name or layer_name,
"size": size,
}
)
class ObjectInterface(metaclass = abc.ABCMeta):
class ObjectInterface(metaclass=abc.ABCMeta):
"""A base object required to be the ancestor of every object used in
volatility."""
def __init__(self, context: 'interfaces.context.ContextInterface', type_name: str, object_info: 'ObjectInformation',
**kwargs) -> None:
def __init__(
self,
context: "interfaces.context.ContextInterface",
type_name: str,
object_info: "ObjectInformation",
**kwargs,
) -> None:
"""Constructs an Object adhering to the ObjectInterface.
Args:
@@ -116,7 +127,7 @@ class ObjectInterface(metaclass = abc.ABCMeta):
mask = context.layers[object_info.layer_name].address_mask
normalized_offset = object_info.offset & mask
vol_info_dict = {'type_name': type_name, 'offset': normalized_offset}
vol_info_dict = {"type_name": type_name, "offset": normalized_offset}
self._vol = collections.ChainMap({}, vol_info_dict, object_info, kwargs)
self._context = context
@@ -143,13 +154,17 @@ class ObjectInterface(metaclass = abc.ABCMeta):
KeyError: If the table_name is not valid within the object's context
"""
if constants.BANG not in self.vol.type_name:
raise ValueError(f"Unable to determine table for symbol: {self.vol.type_name}")
table_name = self.vol.type_name[:self.vol.type_name.index(constants.BANG)]
raise ValueError(
f"Unable to determine table for symbol: {self.vol.type_name}"
)
table_name = self.vol.type_name[: self.vol.type_name.index(constants.BANG)]
if table_name not in self._context.symbol_space:
raise KeyError(f"Symbol table not found in context's symbol_space for symbol: {self.vol.type_name}")
raise KeyError(
f"Symbol table not found in context's symbol_space for symbol: {self.vol.type_name}"
)
return table_name
def cast(self, new_type_name: str, **additional) -> 'ObjectInterface':
def cast(self, new_type_name: str, **additional) -> "ObjectInterface":
"""Returns a new object at the offset and from the layer that the
current object inhabits.
@@ -163,13 +178,15 @@ class ObjectInterface(metaclass = abc.ABCMeta):
object_template = self._context.symbol_space.get_type(new_type_name)
object_template = object_template.clone()
object_template.update_vol(**additional)
object_info = ObjectInformation(layer_name = self.vol.layer_name,
offset = self.vol.offset,
member_name = self.vol.member_name,
parent = self.vol.parent,
native_layer_name = self.vol.native_layer_name,
size = object_template.size)
return object_template(context = self._context, object_info = object_info)
object_info = ObjectInformation(
layer_name=self.vol.layer_name,
offset=self.vol.offset,
member_name=self.vol.member_name,
parent=self.vol.parent,
native_layer_name=self.vol.native_layer_name,
size=object_template.size,
)
return object_template(context=self._context, object_info=object_info)
def has_member(self, member_name: str) -> bool:
"""Returns whether the object would contain a member called
@@ -201,7 +218,7 @@ class ObjectInterface(metaclass = abc.ABCMeta):
"""
return all([self.has_valid_member(member_name) for member_name in member_names])
class VolTemplateProxy(metaclass = abc.ABCMeta):
class VolTemplateProxy(metaclass=abc.ABCMeta):
"""A container for proxied methods that the ObjectTemplate of this
object will call. This is primarily to keep methods together for easy
organization/management, there is no significant need for it to be a
@@ -214,41 +231,52 @@ class ObjectInterface(metaclass = abc.ABCMeta):
to control how their templates respond without needing to write
new templates for each and every potential object type.
"""
_methods: List[str] = []
@classmethod
@abc.abstractmethod
def size(cls, template: 'Template') -> int:
def size(cls, template: "Template") -> int:
"""Returns the size of the template object."""
@classmethod
@abc.abstractmethod
def children(cls, template: 'Template') -> List['Template']:
def children(cls, template: "Template") -> List["Template"]:
"""Returns the children of the template."""
return []
@classmethod
@abc.abstractmethod
def replace_child(cls, template: 'Template', old_child: 'Template', new_child: 'Template') -> None:
def replace_child(
cls, template: "Template", old_child: "Template", new_child: "Template"
) -> None:
"""Substitutes the old_child for the new_child."""
raise KeyError(f"Template does not contain any children to replace: {template.vol.type_name}")
raise KeyError(
f"Template does not contain any children to replace: {template.vol.type_name}"
)
@classmethod
@abc.abstractmethod
def relative_child_offset(cls, template: 'Template', child: str) -> int:
def relative_child_offset(cls, template: "Template", child: str) -> int:
"""Returns the relative offset from the head of the parent data to
the child member."""
raise KeyError(f"Template does not contain any children: {template.vol.type_name}")
raise KeyError(
f"Template does not contain any children: {template.vol.type_name}"
)
@classmethod
@abc.abstractmethod
def child_template(cls, template: 'Template', child: str) -> 'interfaces.objects.Template':
def child_template(
cls, template: "Template", child: str
) -> "interfaces.objects.Template":
"""Returns the template of the child member from the parent."""
raise KeyError(f"Template does not contain any children: {template.vol.type_name}")
raise KeyError(
f"Template does not contain any children: {template.vol.type_name}"
)
@classmethod
@abc.abstractmethod
def has_member(cls, template: 'Template', member_name: str) -> bool:
def has_member(cls, template: "Template", member_name: str) -> bool:
"""Returns whether the object would contain a member called
member_name."""
return False
@@ -282,7 +310,9 @@ class Template:
# Allow the updating of template arguments whilst still in template form
super().__init__()
empty_dict: Dict[str, Any] = {}
self._vol = collections.ChainMap(empty_dict, arguments, {'type_name': type_name})
self._vol = collections.ChainMap(
empty_dict, arguments, {"type_name": type_name}
)
@property
def vol(self) -> ReadOnlyMapping:
@@ -292,7 +322,7 @@ class Template:
return ReadOnlyMapping(self._vol)
@property
def children(self) -> List['Template']:
def children(self) -> List["Template"]:
"""The children of this template (such as member types, sub-types and
base-types where they are relevant).
@@ -311,11 +341,11 @@ class Template:
offset."""
@abc.abstractmethod
def child_template(self, child: str) -> 'interfaces.objects.Template':
def child_template(self, child: str) -> "interfaces.objects.Template":
"""Returns the `child` member template from its parent."""
@abc.abstractmethod
def replace_child(self, old_child: 'Template', new_child: 'Template') -> None:
def replace_child(self, old_child: "Template", new_child: "Template") -> None:
"""Replaces `old_child` with `new_child` in the list of children."""
@abc.abstractmethod
@@ -323,7 +353,7 @@ class Template:
"""Returns whether the object would contain a member called
`member_name`"""
def clone(self) -> 'Template':
def clone(self) -> "Template":
"""Returns a copy of the original Template as constructed (without
`update_vol` additions having been made)"""
clone = self.__class__(**self._vol.parents.new_child())
@@ -337,11 +367,16 @@ class Template:
def __getattr__(self, attr: str) -> Any:
"""Exposes any other values stored in ._vol as attributes (for example,
enumeration choices)"""
if attr != '_vol':
if attr != "_vol":
if attr in self._vol:
return self._vol[attr]
raise AttributeError(f"{self.__class__.__name__} object has no attribute {attr}")
raise AttributeError(
f"{self.__class__.__name__} object has no attribute {attr}"
)
def __call__(self, context: 'interfaces.context.ContextInterface',
object_info: ObjectInformation) -> ObjectInterface:
def __call__(
self,
context: "interfaces.context.ContextInterface",
object_info: ObjectInformation,
) -> ObjectInterface:
"""Constructs the object."""
+17 -9
View File
@@ -64,7 +64,9 @@ class FileHandlerInterface(io.RawIOBase):
if exc_type is None and exc_value is None and traceback is None:
self.close()
else:
vollog.warning(f"File {self._preferred_filename} could not be written: {str(exc_value)}")
vollog.warning(
f"File {self._preferred_filename} could not be written: {str(exc_value)}"
)
self.close()
@@ -82,9 +84,11 @@ class FileHandlerInterface(io.RawIOBase):
# The plugin runs and produces a TreeGrid output
class PluginInterface(interfaces.configuration.ConfigurableInterface,
interfaces.configuration.VersionableInterface,
metaclass = ABCMeta):
class PluginInterface(
interfaces.configuration.ConfigurableInterface,
interfaces.configuration.VersionableInterface,
metaclass=ABCMeta,
):
"""Class that defines the basic interface that all Plugins must maintain.
The constructor must only take a `context` and `config_path`, so
@@ -97,10 +101,12 @@ class PluginInterface(interfaces.configuration.ConfigurableInterface,
_required_framework_version: Tuple[int, int, int] = (0, 0, 0)
"""The _version variable is a quick way for plugins to define their current interface, it should follow SemVer rules"""
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
progress_callback: constants.ProgressCallback = None) -> None:
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
progress_callback: constants.ProgressCallback = None,
) -> None:
"""
Args:
@@ -114,7 +120,9 @@ class PluginInterface(interfaces.configuration.ConfigurableInterface,
# the validation doesn't need to be repeated over and over again by externals
if self.unsatisfied(context, config_path):
vollog.warning("Plugin failed validation")
raise exceptions.PluginRequirementException("The plugin configuration failed to validate")
raise exceptions.PluginRequirementException(
"The plugin configuration failed to validate"
)
# Populate any optional defaults
for requirement in self.get_requirements():
if requirement.name not in self.config:
+56 -24
View File
@@ -12,14 +12,26 @@ suitable output.
import datetime
from abc import abstractmethod, ABCMeta
from collections import abc
from typing import Any, Callable, ClassVar, Generator, List, NamedTuple, Optional, TypeVar, Type, Tuple, Union
from typing import (
Any,
Callable,
ClassVar,
Generator,
List,
NamedTuple,
Optional,
TypeVar,
Type,
Tuple,
Union,
)
Column = NamedTuple('Column', [('name', str), ('type', Any)])
Column = NamedTuple("Column", [("name", str), ("type", Any)])
RenderOption = Any
class Renderer(metaclass = ABCMeta):
class Renderer(metaclass=ABCMeta):
"""Class that defines the interface that all output renderers must
support."""
@@ -32,12 +44,12 @@ class Renderer(metaclass = ABCMeta):
"""Returns a list of rendering options."""
@abstractmethod
def render(self, grid: 'TreeGrid') -> None:
def render(self, grid: "TreeGrid") -> None:
"""Takes a grid object and renders it based on the object's
preferences."""
class ColumnSortKey(metaclass = ABCMeta):
class ColumnSortKey(metaclass=ABCMeta):
ascending: bool = True
@abstractmethod
@@ -46,14 +58,13 @@ class ColumnSortKey(metaclass = ABCMeta):
function."""
class TreeNode(abc.Sequence, metaclass = ABCMeta):
class TreeNode(abc.Sequence, metaclass=ABCMeta):
def __init__(self, path, treegrid, parent, values):
"""Initializes the TreeNode."""
@property
@abstractmethod
def values(self) -> List['BaseTypes']:
def values(self) -> List["BaseTypes"]:
"""Returns the list of values from the particular node, based on column
index."""
@@ -69,7 +80,7 @@ class TreeNode(abc.Sequence, metaclass = ABCMeta):
@property
@abstractmethod
def parent(self) -> Optional['TreeNode']:
def parent(self) -> Optional["TreeNode"]:
"""Returns the parent node of this node or None."""
@property
@@ -94,9 +105,12 @@ class BaseAbsentValue(object):
class Disassembly(object):
"""A class to indicate that the bytes provided should be disassembled
(based on the architecture)"""
possible_architectures = ['intel', 'intel64', 'arm', 'arm64']
def __init__(self, data: bytes, offset: int = 0, architecture: str = 'intel64') -> None:
possible_architectures = ["intel", "intel64", "arm", "arm64"]
def __init__(
self, data: bytes, offset: int = 0, architecture: str = "intel64"
) -> None:
self.data = data
self.architecture = None
if architecture in self.possible_architectures:
@@ -110,13 +124,20 @@ class Disassembly(object):
# contain the types that the validator will accept (which would not include the base)
_Type = TypeVar("_Type")
BaseTypes = Union[Type[int], Type[str], Type[float], Type[bytes], Type[datetime.datetime], Type[BaseAbsentValue],
Type[Disassembly]]
BaseTypes = Union[
Type[int],
Type[str],
Type[float],
Type[bytes],
Type[datetime.datetime],
Type[BaseAbsentValue],
Type[Disassembly],
]
ColumnsType = List[Tuple[str, BaseTypes]]
VisitorSignature = Callable[[TreeNode, _Type], _Type]
class TreeGrid(object, metaclass = ABCMeta):
class TreeGrid(object, 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.
@@ -129,7 +150,14 @@ class TreeGrid(object, metaclass = ABCMeta):
and to create cycles.
"""
base_types: ClassVar[Tuple] = (int, str, float, bytes, datetime.datetime, Disassembly)
base_types: ClassVar[Tuple] = (
int,
str,
float,
bytes,
datetime.datetime,
Disassembly,
)
def __init__(self, columns: ColumnsType, generator: Generator) -> None:
"""Constructs a TreeGrid object using a specific set of columns.
@@ -149,10 +177,12 @@ class TreeGrid(object, metaclass = ABCMeta):
"""Method used to sanitize column names for TreeNodes."""
@abstractmethod
def populate(self,
function: VisitorSignature = None,
initial_accumulator: Any = None,
fail_on_errors: bool = True) -> Optional[Exception]:
def populate(
self,
function: VisitorSignature = None,
initial_accumulator: Any = None,
fail_on_errors: bool = True,
) -> Optional[Exception]:
"""Populates the tree by consuming the TreeGrid's construction
generator Func is called on every node, so can be used to create output
on demand.
@@ -196,11 +226,13 @@ class TreeGrid(object, metaclass = ABCMeta):
return node.path_depth
@abstractmethod
def visit(self,
node: Optional[TreeNode],
function: VisitorSignature,
initial_accumulator: _Type,
sort_key: ColumnSortKey = None) -> None:
def visit(
self,
node: Optional[TreeNode],
function: VisitorSignature,
initial_accumulator: _Type,
sort_key: ColumnSortKey = None,
) -> None:
"""Visits all the nodes in a tree, calling function on each one.
function should have the signature function(node, accumulator) and return new_accumulator
+79 -41
View File
@@ -16,11 +16,13 @@ from volatility3.framework.interfaces.configuration import RequirementInterface
class SymbolInterface:
"""Contains information about a named location in a program's memory."""
def __init__(self,
name: str,
address: int,
type: Optional[objects.Template] = None,
constant_data: Optional[bytes] = None) -> None:
def __init__(
self,
name: str,
address: int,
type: Optional[objects.Template] = None,
constant_data: Optional[bytes] = None,
) -> None:
"""
Args:
@@ -31,7 +33,9 @@ class SymbolInterface:
"""
self._name = name
if constants.BANG in self._name:
raise ValueError(f"Symbol names cannot contain the symbol differentiator ({constants.BANG})")
raise ValueError(
f"Symbol names cannot contain the symbol differentiator ({constants.BANG})"
)
# Scope can be added at a later date
self._location = None
@@ -50,7 +54,7 @@ class SymbolInterface:
# Objects and ObjectTemplates should *always* get a type_name when they're constructed, so allow the IndexError
if self.type is None:
return None
return self.type.vol['type_name']
return self.type.vol["type_name"]
@property
def type(self) -> Optional[objects.Template]:
@@ -78,11 +82,13 @@ class BaseSymbolTableInterface:
Note: table_mapping is a rarely used feature (since symbol tables are typically self-contained)
"""
def __init__(self,
name: str,
native_types: 'NativeTableInterface',
table_mapping: Optional[Dict[str, str]] = None,
class_types: Optional[Mapping[str, Type[objects.ObjectInterface]]] = None) -> None:
def __init__(
self,
name: str,
native_types: "NativeTableInterface",
table_mapping: Optional[Dict[str, str]] = None,
class_types: Optional[Mapping[str, Type[objects.ObjectInterface]]] = None,
) -> None:
"""
Args:
@@ -110,44 +116,54 @@ class BaseSymbolTableInterface:
If the symbol isn't found, it raises a SymbolError exception
"""
raise NotImplementedError("Abstract property get_symbol not implemented by subclass.")
raise NotImplementedError(
"Abstract property get_symbol not implemented by subclass."
)
@property
def symbols(self) -> Iterable[str]:
"""Returns an iterator of the Symbol names."""
raise NotImplementedError("Abstract property symbols not implemented by subclass.")
raise NotImplementedError(
"Abstract property symbols not implemented by subclass."
)
# ## Required Type functions
@property
def types(self) -> Iterable[str]:
"""Returns an iterator of the Symbol type names."""
raise NotImplementedError("Abstract property types not implemented by subclass.")
raise NotImplementedError(
"Abstract property types not implemented by subclass."
)
def get_type(self, name: str) -> objects.Template:
"""Resolves a symbol name into an object template.
If the symbol isn't found it raises a SymbolError exception
"""
raise NotImplementedError("Abstract method get_type not implemented by subclass.")
raise NotImplementedError(
"Abstract method get_type not implemented by subclass."
)
# ## Required Symbol enumeration functions
@property
def enumerations(self) -> Iterable[Any]:
"""Returns an iterator of the Enumeration names."""
raise NotImplementedError("Abstract property enumerations not implemented by subclass.")
raise NotImplementedError(
"Abstract property enumerations not implemented by subclass."
)
# ## Native Type Handler
@property
def natives(self) -> 'NativeTableInterface':
def natives(self) -> "NativeTableInterface":
"""Returns None or a NativeTable for handling space specific native
types."""
return self._native_types
@natives.setter
def natives(self, value: 'NativeTableInterface') -> None:
def natives(self, value: "NativeTableInterface") -> None:
"""Checks the natives value and then applies it internally.
WARNING: This allows changing the underlying size of all the other types referenced in the SymbolTable
@@ -167,7 +183,9 @@ class BaseSymbolTableInterface:
"""
raise NotImplementedError("Abstract method set_type_class not implemented yet.")
def optional_set_type_class(self, name: str, clazz: Type[objects.ObjectInterface]) -> bool:
def optional_set_type_class(
self, name: str, clazz: Type[objects.ObjectInterface]
) -> bool:
"""Calls the set_type_class function but does not throw an exception.
Returns whether setting the type class was successful.
Args:
@@ -176,7 +194,7 @@ class BaseSymbolTableInterface:
"""
try:
self.set_type_class(name, clazz)
return True
except ValueError:
return False
@@ -206,8 +224,10 @@ class BaseSymbolTableInterface:
# This allows for searching with and without the table name (in case multiple tables contain
# the same symbol name and we've not specifically been told which one)
symbol = self.get_symbol(symbol_name)
if symbol.type_name is not None and (symbol.type_name == type_name or
(symbol.type_name.endswith(constants.BANG + type_name))):
if symbol.type_name is not None and (
symbol.type_name == type_name
or (symbol.type_name.endswith(constants.BANG + type_name))
):
yield symbol.name
def get_symbols_by_location(self, offset: int, size: int = 0) -> Iterable[str]:
@@ -216,11 +236,15 @@ class BaseSymbolTableInterface:
if size < 0:
raise ValueError("Size must be strictly non-negative")
if not self._sort_symbols:
self._sort_symbols = sorted([(self.get_symbol(sn).address, sn) for sn in self.symbols])
self._sort_symbols = sorted(
[(self.get_symbol(sn).address, sn) for sn in self.symbols]
)
sort_symbols = self._sort_symbols
result = bisect.bisect_left(sort_symbols, (offset, ""))
while result < len(sort_symbols) and \
(sort_symbols[result][0] >= offset and sort_symbols[result][0] <= offset + size):
while result < len(sort_symbols) and (
sort_symbols[result][0] >= offset
and sort_symbols[result][0] <= offset + size
):
yield sort_symbols[result][1]
result += 1
@@ -247,7 +271,9 @@ class SymbolSpaceInterface(collections.abc.Mapping):
"""Returns all symbols based on the type of the symbol."""
@abstractmethod
def get_symbols_by_location(self, offset: int, size: int = 0, table_name: Optional[str] = None) -> Iterable[str]:
def get_symbols_by_location(
self, offset: int, size: int = 0, table_name: Optional[str] = None
) -> Iterable[str]:
"""Returns all symbols that exist at a specific relative address."""
@abstractmethod
@@ -281,17 +307,21 @@ class SymbolSpaceInterface(collections.abc.Mapping):
"""Adds a symbol_list to the end of the space."""
class SymbolTableInterface(BaseSymbolTableInterface, configuration.ConfigurableInterface, ABC):
class SymbolTableInterface(
BaseSymbolTableInterface, configuration.ConfigurableInterface, ABC
):
"""Handles a table of symbols."""
# FIXME: native_types and table_mapping aren't recorded in the configuration
def __init__(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
name: str,
native_types: 'NativeTableInterface',
table_mapping: Optional[Dict[str, str]] = None,
class_types: Optional[Mapping[str, Type[objects.ObjectInterface]]] = None) -> None:
def __init__(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
name: str,
native_types: "NativeTableInterface",
table_mapping: Optional[Dict[str, str]] = None,
class_types: Optional[Mapping[str, Type[objects.ObjectInterface]]] = None,
) -> None:
"""Instantiates an SymbolTable based on an IntermediateSymbolFormat JSON file. This is validated against the
appropriate schema.
@@ -305,9 +335,11 @@ class SymbolTableInterface(BaseSymbolTableInterface, configuration.ConfigurableI
class_types: A dictionary of type names and classes that override StructType when they are instantiated
"""
configuration.ConfigurableInterface.__init__(self, context, config_path)
BaseSymbolTableInterface.__init__(self, name, native_types, table_mapping, class_types = class_types)
BaseSymbolTableInterface.__init__(
self, name, native_types, table_mapping, class_types=class_types
)
def build_configuration(self) -> 'configuration.HierarchicalDict':
def build_configuration(self) -> "configuration.HierarchicalDict":
config = super().build_configuration()
# Symbol Tables are constructable, and therefore require a class configuration variable
@@ -317,9 +349,13 @@ class SymbolTableInterface(BaseSymbolTableInterface, configuration.ConfigurableI
@classmethod
def get_requirements(cls) -> List[RequirementInterface]:
return super().get_requirements() + [
requirements.IntRequirement(name = 'symbol_mask', description = 'Address mask for symbols', optional = True,
default = 0),
]
requirements.IntRequirement(
name="symbol_mask",
description="Address mask for symbols",
optional=True,
default=0,
),
]
class NativeTableInterface(BaseSymbolTableInterface):
@@ -333,7 +369,9 @@ class NativeTableInterface(BaseSymbolTableInterface):
return []
def get_enumeration(self, name: str) -> objects.Template:
raise exceptions.SymbolError(name, self.name, "NativeTables never hold enumerations")
raise exceptions.SymbolError(
name, self.name, "NativeTables never hold enumerations"
)
@property
def enumerations(self) -> Iterable[str]:
+75 -30
View File
@@ -37,13 +37,19 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer):
@classmethod
def _check_header(cls, layer: interfaces.layers.DataLayerInterface):
header_structure = "<II"
magic, version = struct.unpack(header_structure,
layer.read(layer.minimum_address, struct.calcsize(header_structure)))
if magic not in [0x4c4d5641] or version != 2:
magic, version = struct.unpack(
header_structure,
layer.read(layer.minimum_address, struct.calcsize(header_structure)),
)
if magic not in [0x4C4D5641] or version != 2:
raise exceptions.LayerException("File not completely in AVML format")
if not HAS_SNAPPY:
vollog.warning('AVML file detected, but snappy python library not installed')
raise exceptions.LayerException("AVML format dependencies not satisfied (snappy)")
vollog.warning(
"AVML file detected, but snappy python library not installed"
)
raise exceptions.LayerException(
"AVML format dependencies not satisfied (snappy)"
)
def _load_segments(self) -> None:
base_layer = self.context.layers[self._base_layer]
@@ -52,24 +58,38 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer):
avml_header_structure = "<IIQQQ"
avml_header_size = struct.calcsize(avml_header_structure)
avml_header_data = base_layer.read(offset, avml_header_size)
magic, version, start, end, padding = struct.unpack(avml_header_structure, avml_header_data)
magic, version, start, end, padding = struct.unpack(
avml_header_structure, avml_header_data
)
if magic not in [0x4c4d5641] or version != 2:
if magic not in [0x4C4D5641] or version != 2:
raise exceptions.LayerException("File not completely in AVML format")
chunk_data = base_layer.read(offset + avml_header_size,
min(end - start,
base_layer.maximum_address - (offset + avml_header_size)))
chunk_data = base_layer.read(
offset + avml_header_size,
min(
end - start,
base_layer.maximum_address - (offset + avml_header_size),
),
)
segments, consumed = self._read_snappy_frames(chunk_data, end - start)
# The returned segments are accurate the chunk_data that was passed in, but needs shifting
for (thing, mapped_offset, size, mapped_size, compressed) in segments:
self._segments.append((thing + start, offset + mapped_offset + avml_header_size, size, mapped_size))
self._segments.append(
(
thing + start,
offset + mapped_offset + avml_header_size,
size,
mapped_size,
)
)
self._compressed[offset + mapped_offset + avml_header_size] = compressed
# TODO: Check whatever the remaining 8 bytes are
offset += avml_header_size + consumed + 8
def _read_snappy_frames(self, data: bytes, expected_length: int) -> Tuple[
List[Tuple[int, int, int, int, bool]], int]:
def _read_snappy_frames(
self, data: bytes, expected_length: int
) -> Tuple[List[Tuple[int, int, int, int, bool]], int]:
"""
Reads a framed-format snappy stream
@@ -84,41 +104,62 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer):
decompressed_len = 0
offset = 0
crc_len = 4
frame_header_struct = '<L'
frame_header_struct = "<L"
frame_header_len = struct.calcsize(frame_header_struct)
while decompressed_len <= expected_length:
if offset + frame_header_len < len(data):
frame_header = data[offset:offset + frame_header_len]
frame_header_val = struct.unpack('<L', frame_header)[0]
frame_type, frame_size = frame_header_val & 0xff, frame_header_val >> 8
if frame_type == 0xff:
if data[offset + frame_header_len:offset + frame_header_len + frame_size] != b'sNaPpY':
frame_header = data[offset : offset + frame_header_len]
frame_header_val = struct.unpack("<L", frame_header)[0]
frame_type, frame_size = frame_header_val & 0xFF, frame_header_val >> 8
if frame_type == 0xFF:
if (
data[
offset
+ frame_header_len : offset
+ frame_header_len
+ frame_size
]
!= b"sNaPpY"
):
raise ValueError(f"Snappy header missing at offset: {offset}")
elif frame_type in [0x00, 0x01]:
# CRC + (Un)compressed data
mapped_start = offset + frame_header_len
# frame_crc = data[mapped_start: mapped_start + crc_len]
frame_data = data[mapped_start + crc_len: mapped_start + frame_size]
frame_data = data[
mapped_start + crc_len : mapped_start + frame_size
]
if frame_type == 0x00:
# Compressed data
frame_data = snappy.decompress(frame_data)
# TODO: Verify CRC
segments.append((decompressed_len, mapped_start + crc_len, len(frame_data), frame_size - crc_len,
frame_type == 0x00))
segments.append(
(
decompressed_len,
mapped_start + crc_len,
len(frame_data),
frame_size - crc_len,
frame_type == 0x00,
)
)
decompressed_len += len(frame_data)
elif frame_type in range(0x2, 0x80):
# Unskippable
raise exceptions.LayerException(f"Unskippable chunk of type {frame_type} found: {offset}")
raise exceptions.LayerException(
f"Unskippable chunk of type {frame_type} found: {offset}"
)
offset += frame_header_len + frame_size
return segments, offset
def _decode_data(self, data: bytes, mapped_offset: int, offset: int, output_length: int) -> bytes:
def _decode_data(
self, data: bytes, mapped_offset: int, offset: int, output_length: int
) -> bytes:
start_offset, _, _, _ = self._find_segment(offset)
if self._compressed[mapped_offset]:
decoded_data = snappy.decompress(data)
else:
decoded_data = data
decoded_data = decoded_data[offset - start_offset:]
decoded_data = decoded_data[offset - start_offset :]
decoded_data = decoded_data[:output_length]
return decoded_data
@@ -127,14 +168,18 @@ class AVMLStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 10
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
try:
AVMLLayer._check_header(context.layers[layer_name])
except exceptions.LayerException:
return None
new_name = context.layers.free_layer_name("AVMLLayer")
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = layer_name
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return AVMLLayer(context, new_name, new_name)
+108 -47
View File
@@ -27,16 +27,18 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
provides = {"type": "physical"}
SIGNATURE = 0x45474150
VALIDDUMP = 0x504d5544
VALIDDUMP = 0x504D5544
crashdump_json = 'crash'
crashdump_json = "crash"
supported_dumptypes = [0x01, 0x05] # we need 0x5 for 32-bit bitmaps
dump_header_name = '_DUMP_HEADER'
dump_header_name = "_DUMP_HEADER"
_magic_struct = struct.Struct('<II')
_magic_struct = struct.Struct("<II")
headerpages = 1
def __init__(self, context: interfaces.context.ContextInterface, config_path: str, name: str) -> None:
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
) -> None:
# Construct these so we can use self.config
self._context = context
@@ -46,15 +48,18 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
self._base_layer = self.config["base_layer"]
# Create a custom SymbolSpace
self._crash_table_name = intermed.IntermediateSymbolTable.create(context, self._config_path, 'windows',
self.crashdump_json)
self._crash_table_name = intermed.IntermediateSymbolTable.create(
context, self._config_path, "windows", self.crashdump_json
)
# the _SUMMARY_DUMP is shared between 32- and 64-bit
self._crash_common_table_name = intermed.IntermediateSymbolTable.create(context,
self._config_path,
'windows',
'crash_common',
class_types = crash.class_types)
self._crash_common_table_name = intermed.IntermediateSymbolTable.create(
context,
self._config_path,
"windows",
"crash_common",
class_types=crash.class_types,
)
# Check Header
hdr_layer = self._context.layers[self._base_layer]
@@ -71,21 +76,30 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
# Verify that it is a supported format
if header.DumpType not in self.supported_dumptypes:
vollog.log(constants.LOGLEVEL_VVVV, f"unsupported dump format 0x{header.DumpType:x}")
raise WindowsCrashDumpFormatException(name, f"unsupported dump format 0x{header.DumpType:x}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"unsupported dump format 0x{header.DumpType:x}",
)
raise WindowsCrashDumpFormatException(
name, f"unsupported dump format 0x{header.DumpType:x}"
)
# Then call the super, which will call load_segments (which needs the base_layer before it'll work)
super().__init__(context, config_path, name)
def get_header(self) -> interfaces.objects.ObjectInterface:
return self.context.object(self._crash_table_name + constants.BANG + self.dump_header_name,
offset = 0,
layer_name = self._base_layer)
return self.context.object(
self._crash_table_name + constants.BANG + self.dump_header_name,
offset=0,
layer_name=self._base_layer,
)
def get_summary_header(self) -> interfaces.objects.ObjectInterface:
return self.context.object(self._crash_common_table_name + constants.BANG + "_SUMMARY_DUMP",
offset = 0x1000 * self.headerpages,
layer_name = self._base_layer)
return self.context.object(
self._crash_common_table_name + constants.BANG + "_SUMMARY_DUMP",
offset=0x1000 * self.headerpages,
layer_name=self._base_layer,
)
def _load_segments(self) -> None:
"""Loads up the segments from the meta_layer."""
@@ -93,15 +107,25 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
segments = []
if self.dump_type == 0x1:
header = self.context.object(self._crash_table_name + constants.BANG + self.dump_header_name,
offset = 0,
layer_name = self._base_layer)
header = self.context.object(
self._crash_table_name + constants.BANG + self.dump_header_name,
offset=0,
layer_name=self._base_layer,
)
offset = self.headerpages
header.PhysicalMemoryBlockBuffer.Run.count = header.PhysicalMemoryBlockBuffer.NumberOfRuns
header.PhysicalMemoryBlockBuffer.Run.count = (
header.PhysicalMemoryBlockBuffer.NumberOfRuns
)
for run in header.PhysicalMemoryBlockBuffer.Run:
segments.append(
(run.BasePage * 0x1000, offset * 0x1000, run.PageCount * 0x1000, run.PageCount * 0x1000))
(
run.BasePage * 0x1000,
offset * 0x1000,
run.PageCount * 0x1000,
run.PageCount * 0x1000,
)
)
offset += run.PageCount
elif self.dump_type == 0x05:
@@ -118,7 +142,14 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
if first_bit is not None:
last_bit = ((outer_index - 1) * 32) + 31
segment_length = (last_bit - first_bit + 1) * 0x1000
segments.append((first_bit * 0x1000, first_offset, segment_length, segment_length))
segments.append(
(
first_bit * 0x1000,
first_offset,
segment_length,
segment_length,
)
)
first_bit = None
elif buffer_long[outer_index] == 0xFFFFFFFF:
if first_bit is None:
@@ -135,48 +166,74 @@ class WindowsCrashDump32Layer(segmented.SegmentedLayer):
offset = offset + 0x1000
else:
if first_bit is not None:
segment_length = ((bit_addr - 1) - first_bit + 1) * 0x1000
segments.append((first_bit * 0x1000, first_offset, segment_length, segment_length))
segment_length = (
(bit_addr - 1) - first_bit + 1
) * 0x1000
segments.append(
(
first_bit * 0x1000,
first_offset,
segment_length,
segment_length,
)
)
first_bit = None
last_bit_seen = (outer_index * 32) + 31
if first_bit is not None:
segment_length = (last_bit_seen - first_bit + 1) * 0x1000
segments.append((first_bit * 0x1000, first_offset, segment_length, segment_length))
segments.append(
(first_bit * 0x1000, first_offset, segment_length, segment_length)
)
else:
vollog.log(constants.LOGLEVEL_VVVV, f"unsupported dump format 0x{self.dump_type:x}")
raise WindowsCrashDumpFormatException(self.name, f"unsupported dump format 0x{self.dump_type:x}")
vollog.log(
constants.LOGLEVEL_VVVV, f"unsupported dump format 0x{self.dump_type:x}"
)
raise WindowsCrashDumpFormatException(
self.name, f"unsupported dump format 0x{self.dump_type:x}"
)
if len(segments) == 0:
raise WindowsCrashDumpFormatException(self.name, f"No Crash segments defined in {self._base_layer}")
raise WindowsCrashDumpFormatException(
self.name, f"No Crash segments defined in {self._base_layer}"
)
else:
# report the segments for debugging. this is valuable for dev/troubleshooting but
# not important enough for a dedicated plugin.
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))
"Segment {}: Position {:#x} Offset {:#x} Length {:#x}".format(
idx, start_position, mapped_offset, length
),
)
self._segments = segments
@classmethod
def check_header(cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0) -> Tuple[int, int]:
def check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0
) -> Tuple[int, int]:
# Verify the Window's crash dump file magic
try:
header_data = base_layer.read(offset, cls._magic_struct.size)
except exceptions.InvalidAddressException:
raise WindowsCrashDumpFormatException(base_layer.name,
f"Crashdump header not found at offset {offset}")
raise WindowsCrashDumpFormatException(
base_layer.name, f"Crashdump header not found at offset {offset}"
)
(signature, validdump) = cls._magic_struct.unpack(header_data)
if signature != cls.SIGNATURE:
raise WindowsCrashDumpFormatException(
base_layer.name, f"Bad signature 0x{signature:x} at file offset 0x{offset:x}")
base_layer.name,
f"Bad signature 0x{signature:x} at file offset 0x{offset:x}",
)
if validdump != cls.VALIDDUMP:
raise WindowsCrashDumpFormatException(base_layer.name,
f"Invalid dump 0x{validdump:x} at file offset 0x{offset:x}")
raise WindowsCrashDumpFormatException(
base_layer.name,
f"Invalid dump 0x{validdump:x} at file offset 0x{offset:x}",
)
return signature, validdump
@@ -188,8 +245,8 @@ class WindowsCrashDump64Layer(WindowsCrashDump32Layer):
"""
VALIDDUMP = 0x34365544
crashdump_json = 'crash64'
dump_header_name = '_DUMP_HEADER64'
crashdump_json = "crash64"
dump_header_name = "_DUMP_HEADER64"
supported_dumptypes = [0x1, 0x05]
headerpages = 2
@@ -198,14 +255,18 @@ class WindowsCrashDumpStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 11
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
for layer in [WindowsCrashDump32Layer, WindowsCrashDump64Layer]:
with contextlib.suppress(WindowsCrashDumpFormatException):
layer.check_header(context.layers[layer_name])
new_name = context.layers.free_layer_name(layer.__name__)
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = layer_name
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return layer(context, new_name, new_name)
return None
+59 -23
View File
@@ -18,50 +18,82 @@ class ElfFormatException(exceptions.LayerException):
class Elf64Layer(segmented.SegmentedLayer):
"""A layer that supports the Elf64 format as documented at: http://ftp.openwatcom.org/devel/docs/elf-64-gen.pdf"""
_header_struct = struct.Struct("<IBBB")
MAGIC = 0x464c457f # "\x7fELF"
MAGIC = 0x464C457F # "\x7fELF"
ELF_CLASS = 2
def __init__(self, context: interfaces.context.ContextInterface, config_path: str, name: str) -> None:
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
) -> None:
# Create a custom SymbolSpace
self._elf_table_name = intermed.IntermediateSymbolTable.create(context, config_path, 'linux', 'elf')
self._elf_table_name = intermed.IntermediateSymbolTable.create(
context, config_path, "linux", "elf"
)
super().__init__(context, config_path, name)
def _load_segments(self) -> None:
"""Load the segments from based on the PT_LOAD segments of the Elf64 format"""
ehdr = self.context.object(self._elf_table_name + constants.BANG + "Elf64_Ehdr",
layer_name = self._base_layer,
offset = 0)
ehdr = self.context.object(
self._elf_table_name + constants.BANG + "Elf64_Ehdr",
layer_name=self._base_layer,
offset=0,
)
segments = []
for pindex in range(ehdr.e_phnum):
phdr = self.context.object(self._elf_table_name + constants.BANG + "Elf64_Phdr",
layer_name = self._base_layer,
offset = ehdr.e_phoff + (pindex * ehdr.e_phentsize))
phdr = self.context.object(
self._elf_table_name + constants.BANG + "Elf64_Phdr",
layer_name=self._base_layer,
offset=ehdr.e_phoff + (pindex * ehdr.e_phentsize),
)
# We only want PT_TYPES with valid sizes
if phdr.p_type.lookup() == "PT_LOAD" and phdr.p_filesz == phdr.p_memsz and phdr.p_filesz > 0:
if (
phdr.p_type.lookup() == "PT_LOAD"
and phdr.p_filesz == phdr.p_memsz
and phdr.p_filesz > 0
):
# Cast these to ints to ensure the offsets don't need reconstructing
segments.append((int(phdr.p_paddr), int(phdr.p_offset), int(phdr.p_memsz), int(phdr.p_memsz)))
segments.append(
(
int(phdr.p_paddr),
int(phdr.p_offset),
int(phdr.p_memsz),
int(phdr.p_memsz),
)
)
if len(segments) == 0:
raise ElfFormatException(self.name, f"No ELF segments defined in {self._base_layer}")
raise ElfFormatException(
self.name, f"No ELF segments defined in {self._base_layer}"
)
self._segments = segments
@classmethod
def _check_header(cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0) -> bool:
def _check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0
) -> bool:
try:
header_data = base_layer.read(offset, cls._header_struct.size)
except exceptions.InvalidAddressException:
raise ElfFormatException(base_layer.name,
f"Offset 0x{offset:0x} does not exist within the base layer")
(magic, elf_class, elf_data_encoding, elf_version) = cls._header_struct.unpack(header_data)
raise ElfFormatException(
base_layer.name,
f"Offset 0x{offset:0x} does not exist within the base layer",
)
(magic, elf_class, elf_data_encoding, elf_version) = cls._header_struct.unpack(
header_data
)
if magic != cls.MAGIC:
raise ElfFormatException(base_layer.name, f"Bad magic 0x{magic:x} at file offset 0x{offset:x}")
raise ElfFormatException(
base_layer.name, f"Bad magic 0x{magic:x} at file offset 0x{offset:x}"
)
if elf_class != cls.ELF_CLASS:
raise ElfFormatException(base_layer.name, f"ELF class is not 64-bit (2): {elf_class:d}")
raise ElfFormatException(
base_layer.name, f"ELF class is not 64-bit (2): {elf_class:d}"
)
# Virtualbox uses an ELF version of 0, which isn't to specification, but is ok to deal with
return True
@@ -70,10 +102,12 @@ class Elf64Stacker(interfaces.automagic.StackerLayerInterface):
stack_order = 10
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
try:
if not Elf64Layer._check_header(context.layers[layer_name]):
return None
@@ -81,6 +115,8 @@ class Elf64Stacker(interfaces.automagic.StackerLayerInterface):
vollog.log(constants.LOGLEVEL_VVVV, f"Exception: {excp}")
return None
new_name = context.layers.free_layer_name("Elf64Layer")
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = layer_name
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return Elf64Layer(context, new_name, new_name)
+161 -74
View File
@@ -28,28 +28,39 @@ class Intel(linear.LinearlyMappedLayer):
# NOTE: _maxphyaddr is MAXPHYADDR as defined in the Intel specs *NOT* the maximum physical address
_maxphyaddr = 32
_maxvirtaddr = _maxphyaddr
_structure = [('page directory', 10, False), ('page table', 10, True)]
_direct_metadata = collections.ChainMap({'architecture': 'Intel32'}, {'mapped': True},
interfaces.layers.TranslationLayerInterface._direct_metadata)
_structure = [("page directory", 10, False), ("page table", 10, True)]
_direct_metadata = collections.ChainMap(
{"architecture": "Intel32"},
{"mapped": True},
interfaces.layers.TranslationLayerInterface._direct_metadata,
)
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
self._base_layer = self.config["memory_layer"]
self._swap_layers: List[str] = []
self._page_map_offset = self.config["page_map_offset"]
# Assign constants
self._initial_position = min(self._maxvirtaddr, self._bits_per_register) - 1
self._initial_entry = self._mask(self._page_map_offset, self._initial_position, 0) | 0x1
self._initial_entry = (
self._mask(self._page_map_offset, self._initial_position, 0) | 0x1
)
self._entry_size = struct.calcsize(self._entry_format)
self._entry_number = self.page_size // self._entry_size
# 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 = int(
math.ceil(math.log2(struct.calcsize(self._entry_format)))
)
@classproperty
@functools.lru_cache()
@@ -86,7 +97,7 @@ class Intel(linear.LinearlyMappedLayer):
"""Returns the bits of a value between highbit and lowbit inclusive."""
high_mask = (1 << (high_bit + 1)) - 1
low_mask = (1 << low_bit) - 1
mask = (high_mask ^ low_mask)
mask = high_mask ^ low_mask
# print(high_bit, low_bit, bin(mask), bin(value))
return value & mask
@@ -106,9 +117,16 @@ class Intel(linear.LinearlyMappedLayer):
# Now we're done
if not self._page_is_valid(entry):
raise exceptions.PagedInvalidAddressException(self.name, offset, position + 1, 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)
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
position + 1,
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
)
return page, 1 << (position + 1), self._base_layer
@@ -124,20 +142,30 @@ class Intel(linear.LinearlyMappedLayer):
entry = self._initial_entry
if self.minimum_address > offset > self.maximum_address:
raise exceptions.PagedInvalidAddressException(self.name, offset, position + 1, entry,
"Entry outside virtual address range: " + hex(entry))
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
position + 1,
entry,
"Entry outside virtual address range: " + hex(entry),
)
# Run through the offset in various chunks
for (name, size, large_page) in self._structure:
# Check we're valid
if not self._page_is_valid(entry):
raise exceptions.PagedInvalidAddressException(self.name, offset, position + 1, entry,
"Page Fault at entry " + hex(entry) + " in table " + name)
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
position + 1,
entry,
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Check if we're a large page
if large_page and (entry & (1 << 7)):
# Mask off the PAT bit
if entry & (1 << 12):
entry -= (1 << 12)
entry -= 1 << 12
# 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
@@ -147,33 +175,51 @@ class Intel(linear.LinearlyMappedLayer):
index = self._mask(offset, start, position + 1) >> (position + 1)
# Grab the base address of the table we'll be getting the next entry from
base_address = self._mask(entry, self._maxphyaddr - 1, size + self._index_shift)
base_address = self._mask(
entry, self._maxphyaddr - 1, size + self._index_shift
)
table = self._get_valid_table(base_address)
if table is None:
raise exceptions.PagedInvalidAddressException(self.name, offset, position + 1, entry,
"Page Fault at entry " + hex(entry) + " in table " + name)
raise exceptions.PagedInvalidAddressException(
self.name,
offset,
position + 1,
entry,
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Read the data for the next entry
entry_data = table[(index << self._index_shift):(index << self._index_shift) + self._entry_size]
entry_data = table[
(index << self._index_shift) : (index << self._index_shift)
+ self._entry_size
]
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))
constants.LOGLEVEL_VVVV,
"Entry {} at index {} gives data {} as {}".format(
hex(entry),
hex(index),
hex(struct.unpack(self._entry_format, entry_data)[0]),
name,
),
)
# Read out the new entry from memory
entry, = struct.unpack(self._entry_format, entry_data)
(entry,) = struct.unpack(self._entry_format, entry_data)
return entry, position
@functools.lru_cache(1025)
def _get_valid_table(self, base_address: int) -> Optional[bytes]:
"""Extracts the table, validates it and returns it if it's valid."""
table = self._context.layers.read(self._base_layer, base_address, self.page_size)
table = self._context.layers.read(
self._base_layer, base_address, self.page_size
)
# If the table is entirely duplicates, then mark the whole table as bad
if (table == table[:self._entry_size] * self._entry_number):
if table == table[: self._entry_size] * self._entry_number:
return None
return table
@@ -182,27 +228,36 @@ class Intel(linear.LinearlyMappedLayer):
address."""
try:
# TODO: Consider reimplementing this, since calls to mapping can call is_valid
return all([
self._context.layers[layer].is_valid(mapped_offset)
for _, _, mapped_offset, _, layer in self.mapping(offset, length)
])
return all(
[
self._context.layers[layer].is_valid(mapped_offset)
for _, _, mapped_offset, _, layer in self.mapping(offset, length)
]
)
except exceptions.InvalidAddressException:
return False
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
"""Returns a sorted iterable of (offset, sublength, mapped_offset, mapped_length, layer)
mappings.
This allows translation layers to provide maps of contiguous
regions in one layer
"""
stashed_offset = stashed_mapped_offset = stashed_size = stashed_mapped_size = stashed_map_layer = None
for offset, size, mapped_offset, mapped_size, map_layer in self._mapping(offset, length, ignore_errors):
if stashed_offset is None or (stashed_offset + stashed_size != offset) or (
stashed_mapped_offset + stashed_mapped_size != mapped_offset) or (stashed_map_layer != map_layer):
stashed_offset = (
stashed_mapped_offset
) = stashed_size = stashed_mapped_size = stashed_map_layer = None
for offset, size, mapped_offset, mapped_size, map_layer in self._mapping(
offset, length, ignore_errors
):
if (
stashed_offset is None
or (stashed_offset + stashed_size != offset)
or (stashed_mapped_offset + stashed_mapped_size != mapped_offset)
or (stashed_map_layer != map_layer)
):
# The block isn't contiguous
if stashed_offset is not None:
yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer
@@ -217,14 +272,18 @@ class Intel(linear.LinearlyMappedLayer):
stashed_size += size
stashed_mapped_size += mapped_size
# Yield whatever's left
if (stashed_offset is not None and stashed_mapped_offset is not None and stashed_size is not None
and stashed_mapped_size is not None and stashed_map_layer is not None):
if (
stashed_offset is not None
and stashed_mapped_offset is not None
and stashed_size is not None
and stashed_mapped_size is not None
and stashed_map_layer is not None
):
yield stashed_offset, stashed_size, stashed_mapped_offset, stashed_mapped_size, stashed_map_layer
def _mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def _mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
"""Returns a sorted iterable of (offset, sublength, mapped_offset, mapped_length, layer)
mappings.
@@ -235,7 +294,9 @@ class Intel(linear.LinearlyMappedLayer):
try:
mapped_offset, _, layer_name = self._translate(offset)
if not self._context.layers[layer_name].is_valid(mapped_offset):
raise exceptions.InvalidAddressException(layer_name = layer_name, invalid_address = mapped_offset)
raise exceptions.InvalidAddressException(
layer_name=layer_name, invalid_address=mapped_offset
)
except exceptions.InvalidAddressException:
if not ignore_errors:
raise
@@ -246,9 +307,16 @@ class Intel(linear.LinearlyMappedLayer):
try:
chunk_offset, page_size, layer_name = self._translate(offset)
chunk_size = min(page_size - (chunk_offset % page_size), length)
if not self._context.layers[layer_name].is_valid(chunk_offset, chunk_size):
raise exceptions.InvalidAddressException(layer_name = layer_name, invalid_address = chunk_offset)
except (exceptions.PagedInvalidAddressException, exceptions.InvalidAddressException) as excp:
if not self._context.layers[layer_name].is_valid(
chunk_offset, chunk_size
):
raise exceptions.InvalidAddressException(
layer_name=layer_name, invalid_address=chunk_offset
)
except (
exceptions.PagedInvalidAddressException,
exceptions.InvalidAddressException,
) as excp:
if not ignore_errors:
raise
# We can jump more if we know where the page fault failed
@@ -256,7 +324,7 @@ class Intel(linear.LinearlyMappedLayer):
mask = (1 << excp.invalid_bits) - 1
else:
mask = (1 << self._page_size_in_bits) - 1
length_diff = (mask + 1 - (offset & mask))
length_diff = mask + 1 - (offset & mask)
length -= length_diff
offset += length_diff
else:
@@ -273,11 +341,13 @@ class Intel(linear.LinearlyMappedLayer):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.TranslationLayerRequirement(name = 'memory_layer', optional = False),
requirements.LayerListRequirement(name = 'swap_layers', optional = True),
requirements.IntRequirement(name = 'page_map_offset', optional = False),
requirements.IntRequirement(name = 'kernel_virtual_offset', optional = True),
requirements.StringRequirement(name = 'kernel_banner', optional = True)
requirements.TranslationLayerRequirement(
name="memory_layer", optional=False
),
requirements.LayerListRequirement(name="swap_layers", optional=True),
requirements.IntRequirement(name="page_map_offset", optional=False),
requirements.IntRequirement(name="kernel_virtual_offset", optional=True),
requirements.StringRequirement(name="kernel_banner", optional=True),
]
@@ -289,25 +359,34 @@ class IntelPAE(Intel):
_bits_per_register = 32
_maxphyaddr = 40
_maxvirtaddr = 32
_structure = [('page directory pointer', 2, False), ('page directory', 9, True), ('page table', 9, True)]
_direct_metadata = collections.ChainMap({'pae': True}, Intel._direct_metadata)
_structure = [
("page directory pointer", 2, False),
("page directory", 9, True),
("page table", 9, True),
]
_direct_metadata = collections.ChainMap({"pae": True}, Intel._direct_metadata)
class Intel32e(Intel):
"""Class for handling 64-bit (32-bit extensions) for Intel
architectures."""
_direct_metadata = collections.ChainMap({'architecture': 'Intel64'}, Intel._direct_metadata)
_direct_metadata = collections.ChainMap(
{"architecture": "Intel64"}, Intel._direct_metadata
)
_entry_format = "<Q"
_bits_per_register = 64
_maxphyaddr = 52
_maxvirtaddr = 48
_structure = [('page map layer 4', 9, False), ('page directory pointer', 9, True), ('page directory', 9, True),
('page table', 9, True)]
_structure = [
("page map layer 4", 9, False),
("page directory pointer", 9, True),
("page directory", 9, True),
("page table", 9, True),
]
class WindowsMixin(Intel):
@staticmethod
def _page_is_valid(entry: int) -> bool:
"""Returns whether a particular page is valid based on its entry.
@@ -321,7 +400,9 @@ class WindowsMixin(Intel):
"""
return bool((entry & 1) or ((entry & 1 << 11) and not entry & 1 << 10))
def _translate_swap(self, layer: Intel, offset: int, bit_offset: int) -> Tuple[int, int, str]:
def _translate_swap(
self, layer: Intel, offset: int, bit_offset: int
) -> Tuple[int, int, str]:
try:
return super()._translate(offset)
except exceptions.PagedInvalidAddressException as excp:
@@ -331,19 +412,27 @@ class WindowsMixin(Intel):
unknown_bit = bool(entry & (1 << 7))
n = (entry >> 1) & 0xF
vbit = bool(entry & 1)
if (not tbit and not pbit and not vbit and unknown_bit) and ((entry >> bit_offset) != 0):
if (not tbit and not pbit and not vbit and unknown_bit) and (
(entry >> bit_offset) != 0
):
swap_offset = entry >> bit_offset << excp.invalid_bits
if layer.config.get('swap_layers', False):
if layer.config.get("swap_layers", False):
swap_layer_name = layer.config.get(
interfaces.configuration.path_join('swap_layers', 'swap_layers' + str(n)), None)
interfaces.configuration.path_join(
"swap_layers", "swap_layers" + str(n)
),
None,
)
if swap_layer_name:
return swap_offset, 1 << excp.invalid_bits, swap_layer_name
raise exceptions.SwappedInvalidAddressException(layer_name = excp.layer_name,
invalid_address = excp.invalid_address,
invalid_bits = excp.invalid_bits,
entry = excp.entry,
swap_offset = swap_offset)
raise exceptions.SwappedInvalidAddressException(
layer_name=excp.layer_name,
invalid_address=excp.invalid_address,
invalid_bits=excp.invalid_bits,
entry=excp.entry,
swap_offset=swap_offset,
)
raise
@@ -351,13 +440,11 @@ class WindowsMixin(Intel):
class WindowsIntel(WindowsMixin, Intel):
def _translate(self, offset):
return self._translate_swap(self, offset, self._page_size_in_bits)
class WindowsIntelPAE(WindowsMixin, IntelPAE):
def _translate(self, offset: int) -> Tuple[int, int, str]:
return self._translate_swap(self, offset, self._bits_per_register)
+18 -12
View File
@@ -9,6 +9,7 @@ from typing import Optional, Any, List
try:
import leechcorepyc
HAS_LEECHCORE = True
except ImportError:
HAS_LEECHCORE = False
@@ -66,7 +67,7 @@ if HAS_LEECHCORE:
"""
return bool(self._handle)
def seek(self, offset, whence = io.SEEK_SET):
def seek(self, offset, whence=io.SEEK_SET):
if whence == io.SEEK_SET:
self._cursor = offset
elif whence == io.SEEK_CUR:
@@ -91,9 +92,14 @@ if HAS_LEECHCORE:
output = []
for entry in self.handle.memmap:
if entry['base'] + entry['size'] <= chunk_start or entry['base'] >= chunk_start + chunk_size:
if (
entry["base"] + entry["size"] <= chunk_start
or entry["base"] >= chunk_start + chunk_size
):
continue
output += [(max(entry['base'], chunk_start), min(entry['size'], chunk_size))]
output += [
(max(entry["base"], chunk_start), min(entry["size"], chunk_size))
]
chunk_start = output[-1][0] + output[-1][1]
chunk_size = max(0, size - chunk_start)
@@ -114,14 +120,16 @@ if HAS_LEECHCORE:
if len(data) > size:
data = data[:size]
else:
data = data + b'\x00' * (size - len(data))
data = data + b"\x00" * (size - len(data))
self._cursor += len(data)
if not len(data):
raise exceptions.InvalidAddressException('LeechCore layer read failure', self._cursor + len(data))
raise exceptions.InvalidAddressException(
"LeechCore layer read failure", self._cursor + len(data)
)
return data
def readline(self, __size: Optional[int] = ...) -> bytes:
data = b''
data = b""
while __size > self._chunk_size or __size < 0:
data += self.read(self._chunk_size)
index = data.find(b"\n")
@@ -159,20 +167,18 @@ if HAS_LEECHCORE:
def closed(self):
return self._handle
class LeechCoreHandler(resources.VolatilityHandler):
"""Handler for the invented `leechcore` scheme. This is an unofficial scheme and not registered with IANA
"""
"""Handler for the invented `leechcore` scheme. This is an unofficial scheme and not registered with IANA"""
@classmethod
def non_cached_schemes(cls) -> List[str]:
"""We need to turn caching *off* for a live filesystem"""
return ['leechcore']
return ["leechcore"]
@staticmethod
def default_open(req: urllib.request.Request) -> Optional[Any]:
"""Handles the request if it's the leechcore scheme."""
if req.type == 'leechcore':
device_uri = '://'.join(req.full_url.split('://')[1:])
if req.type == "leechcore":
device_uri = "://".join(req.full_url.split("://")[1:])
return LeechCoreFile(device_uri)
return None
+37 -17
View File
@@ -20,14 +20,16 @@ class LimeLayer(segmented.SegmentedLayer):
are large holes in the physical layer
"""
MAGIC = 0x4c694d45
MAGIC = 0x4C694D45
VERSION = 1
# Magic[4], Version[4], Start[8], End[8], Reserved[8]
# XXX move this to a custom SymbolSpace?
_header_struct = struct.Struct('<IIQQQ')
_header_struct = struct.Struct("<IIQQQ")
def __init__(self, context: interfaces.context.ContextInterface, config_path: str, name: str) -> None:
def __init__(
self, context: interfaces.context.ContextInterface, config_path: str, name: str
) -> None:
super().__init__(context, config_path, name)
# The base class loads the segments on initialization, but otherwise this must to get the right min/max addresses
@@ -45,31 +47,45 @@ class LimeLayer(segmented.SegmentedLayer):
if start < maxaddr or end < start:
raise LimeFormatException(
self.name, f"Bad start/end 0x{start:x}/0x{end:x} at file offset 0x{offset:x}")
self.name,
f"Bad start/end 0x{start:x}/0x{end:x} at file offset 0x{offset:x}",
)
segment_length = end - start + 1
segments.append((start, offset + header_size, segment_length, segment_length))
segments.append(
(start, offset + header_size, segment_length, segment_length)
)
maxaddr = end
offset = offset + header_size + segment_length
if len(segments) == 0:
raise LimeFormatException(self.name, f"No LiME segments defined in {self._base_layer}")
raise LimeFormatException(
self.name, f"No LiME segments defined in {self._base_layer}"
)
self._segments = segments
@classmethod
def _check_header(cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0) -> Tuple[int, int]:
def _check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, offset: int = 0
) -> Tuple[int, int]:
try:
header_data = base_layer.read(offset, cls._header_struct.size)
except exceptions.InvalidAddressException:
raise LimeFormatException(base_layer.name,
f"Offset 0x{offset:0x} does not exist within the base layer")
raise LimeFormatException(
base_layer.name,
f"Offset 0x{offset:0x} does not exist within the base layer",
)
(magic, version, start, end, reserved) = cls._header_struct.unpack(header_data)
if magic != cls.MAGIC:
raise LimeFormatException(base_layer.name, f"Bad magic 0x{magic:x} at file offset 0x{offset:x}")
raise LimeFormatException(
base_layer.name, f"Bad magic 0x{magic:x} at file offset 0x{offset:x}"
)
if version != cls.VERSION:
raise LimeFormatException(base_layer.name,
f"Unexpected version {version:d} at file offset 0x{offset:x}")
raise LimeFormatException(
base_layer.name,
f"Unexpected version {version:d} at file offset 0x{offset:x}",
)
return start, end
@@ -77,14 +93,18 @@ class LimeStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 10
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
try:
LimeLayer._check_header(context.layers[layer_name])
except LimeFormatException:
return None
new_name = context.layers.free_layer_name("LimeLayer")
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = layer_name
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
return LimeLayer(context, new_name, new_name)
+36 -16
View File
@@ -14,41 +14,54 @@ class LinearlyMappedLayer(interfaces.layers.TranslationLayerInterface):
### Translation layer convenience function
def translate(self, offset: int, ignore_errors: bool = False) -> Tuple[Optional[int], Optional[str]]:
def translate(
self, offset: int, ignore_errors: bool = False
) -> Tuple[Optional[int], Optional[str]]:
mapping = list(self.mapping(offset, 0, ignore_errors))
if len(mapping) == 1:
original_offset, _, mapped_offset, _, layer = mapping[0]
if original_offset != offset:
raise exceptions.LayerException(self.name,
f"Layer {self.name} claims to map linearly but does not")
raise exceptions.LayerException(
self.name, f"Layer {self.name} claims to map linearly but does not"
)
else:
if ignore_errors:
# We should only hit this if we ignored errors, but check anyway
return None, None
raise exceptions.InvalidAddressException(self.name, offset,
f"Cannot translate {offset} in layer {self.name}")
raise exceptions.InvalidAddressException(
self.name, offset, f"Cannot translate {offset} in layer {self.name}"
)
return mapped_offset, layer
# ## Read/Write functions for mapped pages
# Redefine read here for speed reasons (so we don't call a processing method
@functools.lru_cache(maxsize = 512)
@functools.lru_cache(maxsize=512)
def read(self, offset: int, length: int, pad: bool = False) -> bytes:
"""Reads an offset for length bytes and returns 'bytes' (not 'str') of
length size."""
current_offset = offset
output: List[bytes] = []
for (offset, _, mapped_offset, mapped_length, layer) in self.mapping(offset, length, ignore_errors = pad):
for (offset, _, mapped_offset, mapped_length, layer) in self.mapping(
offset, length, ignore_errors=pad
):
if not pad and offset > current_offset:
raise exceptions.InvalidAddressException(
self.name, current_offset, f"Layer {self.name} cannot map offset: {current_offset}")
self.name,
current_offset,
f"Layer {self.name} cannot map offset: {current_offset}",
)
elif offset > current_offset:
output += [b"\x00" * (offset - current_offset)]
current_offset = offset
elif offset < current_offset:
raise exceptions.LayerException(self.name, "Mapping returned an overlapping element")
raise exceptions.LayerException(
self.name, "Mapping returned an overlapping element"
)
if mapped_length > 0:
output += [self._context.layers.read(layer, mapped_offset, mapped_length, pad)]
output += [
self._context.layers.read(layer, mapped_offset, mapped_length, pad)
]
current_offset += mapped_length
recovered_data = b"".join(output)
return recovered_data + b"\x00" * (length - len(recovered_data))
@@ -61,15 +74,22 @@ class LinearlyMappedLayer(interfaces.layers.TranslationLayerInterface):
for (offset, _, mapped_offset, length, layer) in self.mapping(offset, length):
if offset > current_offset:
raise exceptions.InvalidAddressException(
self.name, current_offset, f"Layer {self.name} cannot map offset: {current_offset}")
self.name,
current_offset,
f"Layer {self.name} cannot map offset: {current_offset}",
)
elif offset < current_offset:
raise exceptions.LayerException(self.name, "Mapping returned an overlapping element")
raise exceptions.LayerException(
self.name, "Mapping returned an overlapping element"
)
self._context.layers.write(layer, mapped_offset, value[:length])
value = value[length:]
current_offset += length
def _scan_iterator(self,
scanner: 'interfaces.layers.ScannerInterface',
sections: Iterable[Tuple[int, int]],
linear: bool = True) -> Iterable[interfaces.layers.IteratorValue]:
def _scan_iterator(
self,
scanner: "interfaces.layers.ScannerInterface",
sections: Iterable[Tuple[int, int]],
linear: bool = True,
) -> Iterable[interfaces.layers.IteratorValue]:
return super()._scan_iterator(scanner, sections, linear)
+107 -68
View File
@@ -21,15 +21,19 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
"BIG_MSF_HDR": "Microsoft C/C++ MSF 7.00\r\n\x1a\x44\x53",
}
def __init__(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
def __init__(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(context, config_path, name, metadata)
self._base_layer = self.config["base_layer"]
self._pdb_symbol_table = intermed.IntermediateSymbolTable.create(context, self._config_path, 'windows', 'pdb')
self._pdb_symbol_table = intermed.IntermediateSymbolTable.create(
context, self._config_path, "windows", "pdb"
)
response = self._check_header()
if response is None:
raise PDBFormatException(name, "Could not find a suitable header")
@@ -46,56 +50,79 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
return
# Recover the root table, by recovering the root table index table...
module = self.context.module(self.pdb_symbol_table, self._base_layer, offset = 0)
module = self.context.module(self.pdb_symbol_table, self._base_layer, offset=0)
entry_size = module.get_type("unsigned long").size
root_table_num_pages = math.ceil(self._header.StreamInfo.StreamInfoSize / self._header.PageSize)
root_index_size = math.ceil((root_table_num_pages * entry_size) / self._header.PageSize)
root_index = module.object(object_type = "array",
offset = self._header.vol.size,
count = root_index_size,
subtype = module.get_type("unsigned long"))
root_index_layer_name = self.create_stream_from_pages("root_index", self._header.StreamInfo.StreamInfoSize,
[x for x in root_index])
root_table_num_pages = math.ceil(
self._header.StreamInfo.StreamInfoSize / self._header.PageSize
)
root_index_size = math.ceil(
(root_table_num_pages * entry_size) / self._header.PageSize
)
root_index = module.object(
object_type="array",
offset=self._header.vol.size,
count=root_index_size,
subtype=module.get_type("unsigned long"),
)
root_index_layer_name = self.create_stream_from_pages(
"root_index",
self._header.StreamInfo.StreamInfoSize,
[x for x in root_index],
)
module = self.context.module(self.pdb_symbol_table, root_index_layer_name, offset = 0)
root_pages = module.object(object_type = "array",
offset = 0,
count = root_table_num_pages,
subtype = module.get_type("unsigned long"))
root_layer_name = self.create_stream_from_pages("root", self._header.StreamInfo.StreamInfoSize,
[x for x in root_pages])
module = self.context.module(
self.pdb_symbol_table, root_index_layer_name, offset=0
)
root_pages = module.object(
object_type="array",
offset=0,
count=root_table_num_pages,
subtype=module.get_type("unsigned long"),
)
root_layer_name = self.create_stream_from_pages(
"root", self._header.StreamInfo.StreamInfoSize, [x for x in root_pages]
)
module = self.context.module(self.pdb_symbol_table, root_layer_name, offset = 0)
num_streams = module.object(object_type = "unsigned long", offset = 0)
stream_sizes = module.object(object_type = "array",
offset = entry_size,
count = num_streams,
subtype = module.get_type("unsigned long"))
module = self.context.module(self.pdb_symbol_table, root_layer_name, offset=0)
num_streams = module.object(object_type="unsigned long", offset=0)
stream_sizes = module.object(
object_type="array",
offset=entry_size,
count=num_streams,
subtype=module.get_type("unsigned long"),
)
current_offset = (num_streams + 1) * entry_size
for stream in range(num_streams):
list_size = math.ceil(stream_sizes[stream] / self.page_size)
if list_size == 0 or stream_sizes[stream] == 0xffffffff:
if list_size == 0 or stream_sizes[stream] == 0xFFFFFFFF:
self._streams[stream] = None
else:
stream_page_list = module.object(object_type = "array",
offset = current_offset,
count = list_size,
subtype = module.get_type("unsigned long"))
current_offset += (list_size * entry_size)
self._streams[stream] = self.create_stream_from_pages("stream" + str(stream), stream_sizes[stream],
[x for x in stream_page_list])
stream_page_list = module.object(
object_type="array",
offset=current_offset,
count=list_size,
subtype=module.get_type("unsigned long"),
)
current_offset += list_size * entry_size
self._streams[stream] = self.create_stream_from_pages(
"stream" + str(stream),
stream_sizes[stream],
[x for x in stream_page_list],
)
def create_stream_from_pages(self, stream_name: str, maximum_size: int, pages: List[int]) -> str:
def create_stream_from_pages(
self, stream_name: str, maximum_size: int, pages: List[int]
) -> str:
# Construct a root layer based on a number of pages
layer_name = self.name + "_" + stream_name
path_join = interfaces.configuration.path_join
config_path = path_join(self.config_path, stream_name)
self.context.config[path_join(config_path, 'base_layer')] = self.name
self.context.config[path_join(config_path, 'pages')] = pages
self.context.config[path_join(config_path, 'maximum_size')] = maximum_size
self.context.config[path_join(config_path, "base_layer")] = self.name
self.context.config[path_join(config_path, "pages")] = pages
self.context.config[path_join(config_path, "maximum_size")] = maximum_size
layer = PdbMSFStream(self.context, config_path, layer_name)
self.context.layers.add_layer(layer)
return layer_name
@@ -107,7 +134,10 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
header_type = self.pdb_symbol_table + constants.BANG + header
current_header = self.context.object(header_type, self._base_layer, 0)
if utility.array_to_string(current_header.Magic) == self._headers[header]:
if not (current_header.PageSize < 0x100 or current_header.PageSize > (128 * 0x10000)):
if not (
current_header.PageSize < 0x100
or current_header.PageSize > (128 * 0x10000)
):
return header, current_header
return None
@@ -123,7 +153,9 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [requirements.TranslationLayerRequirement(name = 'base_layer', optional = False)]
return [
requirements.TranslationLayerRequirement(name="base_layer", optional=False)
]
@property
def maximum_address(self) -> int:
@@ -136,13 +168,12 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
def is_valid(self, offset: int, length: int = 1) -> bool:
return self.context.layers[self._base_layer].is_valid(offset, length)
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
yield offset, length, offset, length, self._base_layer
def get_stream(self, index) -> Optional['PdbMSFStream']:
def get_stream(self, index) -> Optional["PdbMSFStream"]:
self.read_streams()
if index not in self._streams:
raise PDBFormatException(self.name, "Stream not present")
@@ -154,12 +185,13 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
class PdbMSFStream(linear.LinearlyMappedLayer):
def __init__(self,
context: 'interfaces.context.ContextInterface',
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
def __init__(
self,
context: "interfaces.context.ContextInterface",
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(context, config_path, name, metadata)
self._base_layer = self.config["base_layer"]
self._pages = self.config.get("pages", None)
@@ -180,28 +212,31 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ListRequirement(name = 'pages', element_type = int, min_elements = 1),
requirements.TranslationLayerRequirement(name = 'base_layer'),
requirements.IntRequirement(name = 'maximum_size')
requirements.ListRequirement(
name="pages", element_type=int, min_elements=1
),
requirements.TranslationLayerRequirement(name="base_layer"),
requirements.IntRequirement(name="maximum_size"),
]
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
returned = 0
page_size = self._pdb_layer.page_size
while length > 0:
page = math.floor((offset + returned) / page_size)
page_position = ((offset + returned) % page_size)
page_position = (offset + returned) % page_size
chunk_size = min(page_size - page_position, length)
if page >= self._pages_len:
if not ignore_errors:
raise exceptions.InvalidAddressException(layer_name = self.name,
invalid_address = offset + returned)
raise exceptions.InvalidAddressException(
layer_name=self.name, invalid_address=offset + returned
)
else:
yield offset + returned, chunk_size, (self._pages[page] *
page_size) + page_position, chunk_size, self._base_layer
yield offset + returned, chunk_size, (
self._pages[page] * page_size
) + page_position, chunk_size, self._base_layer
returned += chunk_size
length -= chunk_size
@@ -218,13 +253,17 @@ class PdbMSFStream(linear.LinearlyMappedLayer):
@property
def maximum_address(self) -> int:
return self.config.get('maximum_size', len(self._pages) * self._pdb_layer.page_size)
return self.config.get(
"maximum_size", len(self._pages) * self._pdb_layer.page_size
)
@property
def _pdb_layer(self) -> PdbMultiStreamFormat:
if self._base_layer not in self._context.layers:
raise PDBFormatException(self._base_layer,
f"No PdbMultiStreamFormat layer found: {self._base_layer}")
raise PDBFormatException(
self._base_layer,
f"No PdbMultiStreamFormat layer found: {self._base_layer}",
)
result = self._context.layers[self._base_layer]
if isinstance(result, PdbMultiStreamFormat):
return result
+55 -32
View File
@@ -16,13 +16,17 @@ class BufferDataLayer(interfaces.layers.DataLayerInterface):
"""A DataLayer class backed by a buffer in memory, designed for testing and
swift data access."""
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
buffer: bytes,
metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
buffer: bytes,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
self._buffer = buffer
@property
@@ -37,8 +41,10 @@ class BufferDataLayer(interfaces.layers.DataLayerInterface):
def is_valid(self, offset: int, length: int = 1) -> bool:
"""Returns whether the offset is valid or not."""
return bool(self.minimum_address <= offset <= self.maximum_address
and self.minimum_address <= offset + length - 1 <= self.maximum_address)
return bool(
self.minimum_address <= offset <= self.maximum_address
and self.minimum_address <= offset + length - 1 <= self.maximum_address
)
def read(self, address: int, length: int, pad: bool = False) -> bytes:
"""Reads the data from the buffer."""
@@ -46,26 +52,30 @@ class BufferDataLayer(interfaces.layers.DataLayerInterface):
invalid_address = address
if self.minimum_address < address <= self.maximum_address:
invalid_address = self.maximum_address + 1
raise exceptions.InvalidAddressException(self.name, invalid_address,
"Offset outside of the buffer boundaries")
return self._buffer[address:address + length]
raise exceptions.InvalidAddressException(
self.name, invalid_address, "Offset outside of the buffer boundaries"
)
return self._buffer[address : address + length]
def write(self, address: int, data: bytes):
"""Writes the data from to the buffer."""
self._buffer = self._buffer[:address] + data + self._buffer[address + len(data):]
self._buffer = (
self._buffer[:address] + data + self._buffer[address + len(data) :]
)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
# No real requirements (only the buffer). Need to figure out if there's a better way of representing this
return [
requirements.BytesRequirement(name = 'buffer',
description = "The direct bytes to interact with",
optional = False)
requirements.BytesRequirement(
name="buffer",
description="The direct bytes to interact with",
optional=False,
)
]
class DummyLock:
def __enter__(self) -> None:
pass
@@ -76,12 +86,16 @@ class DummyLock:
class FileLayer(interfaces.layers.DataLayerInterface):
"""a DataLayer backed by a file on the filesystem."""
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
self._write_warning = False
self._location = self.config["location"]
@@ -133,8 +147,10 @@ class FileLayer(interfaces.layers.DataLayerInterface):
"""Returns whether the offset is valid or not."""
if length <= 0:
raise ValueError("Length must be positive")
return bool(self.minimum_address <= offset <= self.maximum_address
and self.minimum_address <= offset + length - 1 <= self.maximum_address)
return bool(
self.minimum_address <= offset <= self.maximum_address
and self.minimum_address <= offset + length - 1 <= self.maximum_address
)
def read(self, offset: int, length: int, pad: bool = False) -> bytes:
"""Reads from the file at offset for length."""
@@ -142,8 +158,9 @@ class FileLayer(interfaces.layers.DataLayerInterface):
invalid_address = offset
if self.minimum_address < offset <= self.maximum_address:
invalid_address = self.maximum_address + 1
raise exceptions.InvalidAddressException(self.name, invalid_address,
"Offset outside of the buffer boundaries")
raise exceptions.InvalidAddressException(
self.name, invalid_address, "Offset outside of the buffer boundaries"
)
# TODO: implement locking for multi-threading
with self._lock:
@@ -152,10 +169,13 @@ class FileLayer(interfaces.layers.DataLayerInterface):
if len(data) < length:
if pad:
data += (b"\x00" * (length - len(data)))
data += b"\x00" * (length - len(data))
else:
raise exceptions.InvalidAddressException(
self.name, offset + len(data), "Could not read sufficient bytes from the " + self.name + " file")
self.name,
offset + len(data),
"Could not read sufficient bytes from the " + self.name + " file",
)
return data
def write(self, offset: int, data: bytes) -> None:
@@ -172,8 +192,11 @@ class FileLayer(interfaces.layers.DataLayerInterface):
invalid_address = offset
if self.minimum_address < offset <= self.maximum_address:
invalid_address = self.maximum_address + 1
raise exceptions.InvalidAddressException(self.name, invalid_address,
"Data segment outside of the " + self.name + " file boundaries")
raise exceptions.InvalidAddressException(
self.name,
invalid_address,
"Data segment outside of the " + self.name + " file boundaries",
)
with self._lock:
self._file.seek(offset)
self._file.write(data)
@@ -196,4 +219,4 @@ class FileLayer(interfaces.layers.DataLayerInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [requirements.StringRequirement(name = 'location', optional = False)]
return [requirements.StringRequirement(name="location", optional=False)]
+247 -128
View File
@@ -26,7 +26,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
QEVM_SUBSECTION = 0x05
QEVM_VMDESCRIPTION = 0x06
QEVM_CONFIGURATION = 0x07
QEVM_SECTION_FOOTER = 0x7e
QEVM_SECTION_FOOTER = 0x7E
HASH_PTE_SIZE_64 = 16
SEGMENT_FLAG_COMPRESS = 0x02
@@ -56,57 +56,86 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
distro_re = r"(\w+[\d{1,2}\.]*)"
pci_hole_table = {re.compile(r"^pc-i440fx-([23456789]|\d\d+)\.\d$"): (0xe0000000, 0xc0000000, 0x100000000),
re.compile(r"^pc-i440fx-[01]\.\d$"): (0xe0000000, 0xe0000000, 0x100000000),
re.compile(r"^pc-q35-\d\.\d$"): (0xb0000000, 0x80000000, 0x100000000),
re.compile(r"^microvm$"): (0xc0000000, 0xc0000000, 0x100000000),
re.compile(r"^xen$"): (0xf0000000, 0xf0000000, 0x100000000),
re.compile(r"^pc-i440fx-" + distro_re + r"$"): (0xe0000000, 0xc0000000, 0x100000000),
re.compile(r"^pc-q35-" + distro_re + r"$"): (0xb0000000, 0x80000000, 0x100000000),
}
pci_hole_table = {
re.compile(r"^pc-i440fx-([23456789]|\d\d+)\.\d$"): (
0xE0000000,
0xC0000000,
0x100000000,
),
re.compile(r"^pc-i440fx-[01]\.\d$"): (0xE0000000, 0xE0000000, 0x100000000),
re.compile(r"^pc-q35-\d\.\d$"): (0xB0000000, 0x80000000, 0x100000000),
re.compile(r"^microvm$"): (0xC0000000, 0xC0000000, 0x100000000),
re.compile(r"^xen$"): (0xF0000000, 0xF0000000, 0x100000000),
re.compile(r"^pc-i440fx-" + distro_re + r"$"): (
0xE0000000,
0xC0000000,
0x100000000,
),
re.compile(r"^pc-q35-" + distro_re + r"$"): (
0xB0000000,
0x80000000,
0x100000000,
),
}
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
self._qemu_table_name = intermed.IntermediateSymbolTable.create(context, config_path, 'generic', 'qemu')
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
self._qemu_table_name = intermed.IntermediateSymbolTable.create(
context, config_path, "generic", "qemu"
)
self._configuration = None
self._architecture = None
self._compressed: Set[int] = set()
self._current_segment_name = b''
self._current_segment_name = b""
self._pci_hole_start = 0
self._pci_hole_end = 0
self._pci_hole_minimum = 0
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
@classmethod
def _check_header(cls, base_layer: interfaces.layers.DataLayerInterface, name: str = ''):
def _check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, name: str = ""
):
header = base_layer.read(0, 8)
if header[:4] != b'\x51\x45\x56\x4D':
raise exceptions.LayerException(name, 'No QEMU magic bytes')
if header[4:] != b'\x00\x00\x00\x03':
raise exceptions.LayerException(name, 'Unsupported QEMU version found')
if header[:4] != b"\x51\x45\x56\x4D":
raise exceptions.LayerException(name, "No QEMU magic bytes")
if header[4:] != b"\x00\x00\x00\x03":
raise exceptions.LayerException(name, "Unsupported QEMU version found")
vollog.debug("QEVM header found")
def _read_configuration(self, base_layer: interfaces.layers.DataLayerInterface, name: str) -> Any:
def _read_configuration(
self, base_layer: interfaces.layers.DataLayerInterface, name: str
) -> Any:
"""Reads the JSON configuration from the end of the file"""
chunk_size = 4096
data = b''
for i in range(base_layer.maximum_address, base_layer.minimum_address, -chunk_size):
data = b""
for i in range(
base_layer.maximum_address, base_layer.minimum_address, -chunk_size
):
if i != base_layer.maximum_address:
data = (base_layer.read(i, chunk_size) + data).rstrip(b'\x00')
if b'\x00' in data:
last_null_byte = data.rfind(b'\x00')
start_of_json = data.find(b'{', last_null_byte)
data = (base_layer.read(i, chunk_size) + data).rstrip(b"\x00")
if b"\x00" in data:
last_null_byte = data.rfind(b"\x00")
start_of_json = data.find(b"{", last_null_byte)
if start_of_json >= 0:
data = data[start_of_json:]
return json.loads(data)
# No JSON configuration found at the end of the file, return empty dict
return dict()
raise exceptions.LayerException(name, "Invalid JSON configuration at the end of the file")
raise exceptions.LayerException(
name, "Invalid JSON configuration at the end of the file"
)
def _get_ram_segments(self, index: int, page_size: int) -> Tuple[List[Tuple[int, int, int, int]], int]:
def _get_ram_segments(
self, index: int, page_size: int
) -> Tuple[List[Tuple[int, int, int, int]], int]:
"""Recovers the new index and any sections of memory from a ram section"""
done = None
segments = []
@@ -116,7 +145,7 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
while not done:
# Use struct.unpack here for performance improvements
addr = struct.unpack('>Q', base_layer.read(index, 8))[0]
addr = struct.unpack(">Q", base_layer.read(index, 8))[0]
# Flags are stored in the n least significant bits, where n equals the bit-length of pagesize
flags = addr & (page_size - 1)
@@ -129,43 +158,59 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
addr += self._pci_hole_end - self._pci_hole_start
if flags & self.SEGMENT_FLAG_MEM_SIZE:
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
offset = index,
layer_name = self._base_layer)
namelen = self._context.object(
self._qemu_table_name + constants.BANG + "unsigned char",
offset=index,
layer_name=self._base_layer,
)
while namelen != 0:
total_size = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned long long',
offset = index + 1 + namelen,
layer_name = self._base_layer)
total_size = self._context.object(
self._qemu_table_name + constants.BANG + "unsigned long long",
offset=index + 1 + namelen,
layer_name=self._base_layer,
)
size_array[base_layer.read(index + 1, namelen)] = total_size
index += 1 + namelen + 8
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
offset = index,
layer_name = self._base_layer)
highest_possible_maximum = max([x[0] for x in self.pci_hole_table.values()]) + 1
if size_array.get(b'pc.ram', highest_possible_maximum) < self._pci_hole_minimum:
namelen = self._context.object(
self._qemu_table_name + constants.BANG + "unsigned char",
offset=index,
layer_name=self._base_layer,
)
highest_possible_maximum = (
max([x[0] for x in self.pci_hole_table.values()]) + 1
)
if (
size_array.get(b"pc.ram", highest_possible_maximum)
< self._pci_hole_minimum
):
# Turns off the pci_hole if it's not supposed to be there
vollog.debug(
f"QEVM turning off PCI hole due to small image size: 0x{size_array.get(b'pc.ram'):x} < 0x{self._pci_hole_minimum:x}")
f"QEVM turning off PCI hole due to small image size: 0x{size_array.get(b'pc.ram'):x} < 0x{self._pci_hole_minimum:x}"
)
self._pci_hole_start, self._pci_hole_end = 0, 0
if flags & (self.SEGMENT_FLAG_COMPRESS | self.SEGMENT_FLAG_PAGE):
if not (flags & self.SEGMENT_FLAG_CONTINUE):
namelen = self._context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
offset = index,
layer_name = self._base_layer)
namelen = self._context.object(
self._qemu_table_name + constants.BANG + "unsigned char",
offset=index,
layer_name=self._base_layer,
)
self._current_segment_name = base_layer.read(index + 1, namelen)
index += 1 + namelen
if flags & self.SEGMENT_FLAG_COMPRESS:
if self._current_segment_name == b'pc.ram':
if self._current_segment_name == b"pc.ram":
segments.append((addr, index, page_size, 1))
self._compressed.add(addr)
index += 1
else:
if self._current_segment_name == b'pc.ram':
if self._current_segment_name == b"pc.ram":
segments.append((addr, index, page_size, page_size))
index += page_size
if flags & self.SEGMENT_FLAG_XBZRLE:
raise exceptions.LayerException(self.name, "XBZRLE compression not supported")
raise exceptions.LayerException(
self.name, "XBZRLE compression not supported"
)
if flags & self.SEGMENT_FLAG_EOS:
done = True
return segments, index
@@ -187,88 +232,136 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
if not self._architecture:
self._architecture = self._fallback_determine_architecture()
if self._architecture is None:
vollog.log(constants.LOGLEVEL_VV, f"QEVM architecture could not be determined")
vollog.log(
constants.LOGLEVEL_VV,
f"QEVM architecture could not be determined",
)
# Once all segments have been read, determine the PCI hole if any
for regex in self.pci_hole_table:
if regex.match(self._architecture):
self._pci_hole_minimum, self._pci_hole_start, self._pci_hole_end = self.pci_hole_table[regex]
vollog.log(constants.LOGLEVEL_VVVV, f"QEVM architecture detected as: {self._architecture}")
(
self._pci_hole_minimum,
self._pci_hole_start,
self._pci_hole_end,
) = self.pci_hole_table[regex]
vollog.log(
constants.LOGLEVEL_VVVV,
f"QEVM architecture detected as: {self._architecture}",
)
break
else:
vollog.log(constants.LOGLEVEL_VVVV, f"QEVM unknown architecture found: {self._architecture}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"QEVM unknown architecture found: {self._architecture}",
)
arch_detected = True
section_byte = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
offset = index,
layer_name = self._base_layer)
section_byte = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned char",
offset=index,
layer_name=self._base_layer,
)
index += 1
if section_byte == self.QEVM_CONFIGURATION:
section_len = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
self._architecture = self.context.object(self._qemu_table_name + constants.BANG + 'string',
offset = index + 4, layer_name = self._base_layer,
max_length = section_len)
section_len = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
self._architecture = self.context.object(
self._qemu_table_name + constants.BANG + "string",
offset=index + 4,
layer_name=self._base_layer,
max_length=section_len,
)
index += 4 + section_len
elif section_byte == self.QEVM_SECTION_START or section_byte == self.QEVM_SECTION_FULL:
section_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
elif (
section_byte == self.QEVM_SECTION_START
or section_byte == self.QEVM_SECTION_FULL
):
section_id = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
current_section_id = section_id
index += 4
name_len = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned char',
offset = index,
layer_name = self._base_layer)
name_len = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned char",
offset=index,
layer_name=self._base_layer,
)
index += 1
name = self.context.object(self._qemu_table_name + constants.BANG + 'string',
offset = index,
layer_name = self._base_layer,
max_length = name_len)
name = self.context.object(
self._qemu_table_name + constants.BANG + "string",
offset=index,
layer_name=self._base_layer,
max_length=name_len,
)
index += name_len
# instance_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
# offset = index,
# layer_name = self._base_layer)
index += 4
version_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
version_id = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
index += 4
# Store section info for handling QEVM_SECTION_PARTs later on
section_info[current_section_id] = {'name': name, 'version_id': version_id}
section_info[current_section_id] = {
"name": name,
"version_id": version_id,
}
# Read additional data
index = self.extract_data(index, name, version_id)
elif section_byte == self.QEVM_SECTION_PART or section_byte == self.QEVM_SECTION_END:
section_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
elif (
section_byte == self.QEVM_SECTION_PART
or section_byte == self.QEVM_SECTION_END
):
section_id = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
current_section_id = section_id
index += 4
# Read additional data
index = self.extract_data(index, section_info[current_section_id]['name'],
section_info[current_section_id]['version_id'])
index = self.extract_data(
index,
section_info[current_section_id]["name"],
section_info[current_section_id]["version_id"],
)
elif section_byte == self.QEVM_SECTION_FOOTER:
section_id = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
section_id = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
index += 4
if section_id != current_section_id:
raise exceptions.LayerException(
self._name, f'QEMU section footer mismatch: {current_section_id} and {section_id}')
self._name,
f"QEMU section footer mismatch: {current_section_id} and {section_id}",
)
elif section_byte == self.QEVM_EOF:
pass
else:
raise exceptions.LayerException(self._name, f'QEMU unknown section encountered: {section_byte}')
raise exceptions.LayerException(
self._name, f"QEMU unknown section encountered: {section_byte}"
)
def _fallback_determine_architecture(self) -> str:
architecture_pattern = rb'pc-(i440fx|q35)-(\d{1,2}\.\d{1,2}|\w+[\d{1,2}\.]*)'
architecture_pattern = rb"pc-(i440fx|q35)-(\d{1,2}\.\d{1,2}|\w+[\d{1,2}\.]*)"
default_suffix = "-2.0"
base_layer = self.context.layers[self._base_layer]
vollog.log(constants.LOGLEVEL_VVVV, "QEVM fallback architecture detection used")
res = scanners.RegExScanner(architecture_pattern)
for offset in base_layer.scan(context = self.context, scanner = res):
for offset in base_layer.scan(context=self.context, scanner=res):
line = base_layer.read(offset, 64)
regex_results = re.search(architecture_pattern, line)
architecture = regex_results.group().decode()
@@ -276,80 +369,102 @@ class QemuSuspendLayer(segmented.NonLinearlySegmentedLayer):
# If that does not work, look in configuration JSON for devices specific to a certain architecture
architecture = None
for device in self._configuration.get('devices', []):
device_name = device.get('vmsd_name', '').lower()
if 'i440fx' in device_name or 'piix' in device_name:
architecture = 'pc-i440fx' + default_suffix
for device in self._configuration.get("devices", []):
device_name = device.get("vmsd_name", "").lower()
if "i440fx" in device_name or "piix" in device_name:
architecture = "pc-i440fx" + default_suffix
break
elif 'ich9' in device_name:
architecture = 'pc-q35' + default_suffix
elif "ich9" in device_name:
architecture = "pc-q35" + default_suffix
break
if architecture:
vollog.log(constants.LOGLEVEL_VVV, f'Architecture version unknown, default used: {default_suffix}')
vollog.log(
constants.LOGLEVEL_VVV,
f"Architecture version unknown, default used: {default_suffix}",
)
return architecture
# Still haven't found architecture, switch to fallback-method
architecture_pattern = rb'Standard PC \((i440FX|Q35)'
architecture_pattern = rb"Standard PC \((i440FX|Q35)"
res = scanners.RegExScanner(architecture_pattern)
for offset in base_layer.scan(context = self.context, scanner = res):
for offset in base_layer.scan(context=self.context, scanner=res):
line = base_layer.read(offset, 64)
regex_results = re.search(architecture_pattern, line)
architecture = "pc-" + regex_results.groups()[0].decode().lower() + default_suffix
vollog.log(constants.LOGLEVEL_VVV, f'Architecture version unknown, default used: {default_suffix}')
architecture = (
"pc-" + regex_results.groups()[0].decode().lower() + default_suffix
)
vollog.log(
constants.LOGLEVEL_VVV,
f"Architecture version unknown, default used: {default_suffix}",
)
return architecture
vollog.warning("Could not determine QEMU target architecture!")
return None
def extract_data(self, index, name, version_id):
if name == 'ram':
if name == "ram":
if version_id != 4:
raise exceptions.LayerException(f"QEMU unknown RAM version_id {version_id}")
new_segments, index = self._get_ram_segments(index, self._configuration.get('page_size', 4096))
raise exceptions.LayerException(
f"QEMU unknown RAM version_id {version_id}"
)
new_segments, index = self._get_ram_segments(
index, self._configuration.get("page_size", 4096)
)
self._segments += new_segments
elif name == 'spapr/htab':
elif name == "spapr/htab":
if version_id != 1:
raise exceptions.LayerException(f"QEMU unknown HTAB version_id {version_id}")
header = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long',
offset = index,
layer_name = self._base_layer)
raise exceptions.LayerException(
f"QEMU unknown HTAB version_id {version_id}"
)
header = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long",
offset=index,
layer_name=self._base_layer,
)
index += 4
if header == 0:
htab_index = -1
htab_n_valid = 0
htab_n_invalid = 0
while htab_index != 0 and htab_n_valid != 0 and htab_n_invalid != 0:
htab = self.context.object(self._qemu_table_name + constants.BANG + 'htab',
offset = index,
layer_name = self._base_layer)
htab = self.context.object(
self._qemu_table_name + constants.BANG + "htab",
offset=index,
layer_name=self._base_layer,
)
htab_index, htab_n_valid, htab_n_invalid = htab
index += 8 + (htab_n_valid * self.HASH_PTE_SIZE_64)
elif name == 'dirty-bitmap':
elif name == "dirty-bitmap":
index += 1
elif name == 'pbs-state':
section_len = self.context.object(self._qemu_table_name + constants.BANG + 'unsigned long long',
offset = index,
layer_name = self._base_layer)
elif name == "pbs-state":
section_len = self.context.object(
self._qemu_table_name + constants.BANG + "unsigned long long",
offset=index,
layer_name=self._base_layer,
)
index += 8 + section_len
return index
def _decode_data(self, data: bytes, mapped_offset: int, offset: int, output_length: int) -> bytes:
def _decode_data(
self, data: bytes, mapped_offset: int, offset: int, output_length: int
) -> bytes:
"""Takes the full segment from the base_layer that the data occurs in, checks whether it's compressed
(by locating it in the segment list and verifying if that address is compressed), then reading/expanding the
data, and finally cutting it to the right size. Offset may be the address requested rather than the location
of the starting data. It is the responsibility of the layer to turn the provided data chunk into the right
portion of data necessary.
"""
page_size = self._configuration.get('page_size', 4096)
page_size = self._configuration.get("page_size", 4096)
# start_offset equals the highest multiple of pagesize <= offset
# (We assume that page_size is a power of 2)
start_offset = offset ^ (offset & (page_size - 1))
if start_offset in self._compressed:
data = (data * page_size)
result = data[offset - start_offset:output_length + offset - start_offset]
data = data * page_size
result = data[offset - start_offset : output_length + offset - start_offset]
return result
@functools.lru_cache(maxsize = 512)
@functools.lru_cache(maxsize=512)
def read(self, offset: int, length: int, pad: bool = False) -> bytes:
return super().read(offset, length, pad)
@@ -358,16 +473,20 @@ class QemuStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 10
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
try:
QemuSuspendLayer._check_header(context.layers[layer_name])
except exceptions.LayerException:
return None
new_name = context.layers.free_layer_name("QemuSuspendLayer")
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = layer_name
context.config[
interfaces.configuration.path_join(new_name, "base_layer")
] = layer_name
layer = QemuSuspendLayer(context, new_name, new_name)
cls.stacker_slow_warning()
return layer
+130 -70
View File
@@ -7,7 +7,10 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from volatility3.framework import constants, exceptions, interfaces, objects
from volatility3.framework.configuration import requirements
from volatility3.framework.configuration.requirements import IntRequirement, TranslationLayerRequirement
from volatility3.framework.configuration.requirements import (
IntRequirement,
TranslationLayerRequirement,
)
from volatility3.framework.exceptions import InvalidAddressException
from volatility3.framework.layers import linear
from volatility3.framework.symbols import intermed
@@ -25,35 +28,49 @@ class RegistryInvalidIndex(exceptions.LayerException):
class RegistryHive(linear.LinearlyMappedLayer):
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
self._base_layer = self.config["base_layer"]
self._hive_offset = self.config["hive_offset"]
self._table_name = self.config["nt_symbols"]
self._page_size = 1 << 12
self._reg_table_name = intermed.IntermediateSymbolTable.create(context, self._config_path, 'windows',
'registry')
self._reg_table_name = intermed.IntermediateSymbolTable.create(
context, self._config_path, "windows", "registry"
)
cmhive = self.context.object(self._table_name + constants.BANG + "_CMHIVE", self._base_layer, self._hive_offset)
cmhive = self.context.object(
self._table_name + constants.BANG + "_CMHIVE",
self._base_layer,
self._hive_offset,
)
self._cmhive_name = cmhive.get_name()
self.hive = cmhive.Hive
# TODO: Check the checksum
if self.hive.Signature != 0xbee0bee0:
if self.hive.Signature != 0xBEE0BEE0:
raise RegistryFormatException(
self.name, f"Registry hive at {self._hive_offset} does not have a valid signature")
self.name,
f"Registry hive at {self._hive_offset} does not have a valid signature",
)
# Win10 17063 introduced the Registry process to map most hives. Check
# if it exists and update RegistryHive._base_layer
for proc in pslist.PsList.list_processes(self.context, self.config['base_layer'], self.config['nt_symbols']):
proc_name = proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count, errors = 'replace')
for proc in pslist.PsList.list_processes(
self.context, self.config["base_layer"], self.config["nt_symbols"]
):
proc_name = proc.ImageFileName.cast(
"string", max_length=proc.ImageFileName.vol.count, errors="replace"
)
if proc_name == "Registry" and proc.InheritedFromUniqueProcessId == 4:
proc_layer_name = proc.add_process_layer()
self._base_layer = proc_layer_name
@@ -66,16 +83,23 @@ class RegistryHive(linear.LinearlyMappedLayer):
self._hive_maxaddr_non_volatile = self.hive.Storage[0].Length
self._hive_maxaddr_volatile = self.hive.Storage[1].Length
self._maxaddr = 0x80000000 | self._hive_maxaddr_volatile
vollog.log(constants.LOGLEVEL_VVVV, f"Setting hive {self.name} max address to {hex(self._maxaddr)}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"Setting hive {self.name} max address to {hex(self._maxaddr)}",
)
except exceptions.InvalidAddressException:
self._hive_maxaddr_non_volatile = 0x7fffffff
self._hive_maxaddr_volatile = 0x7fffffff
self._hive_maxaddr_non_volatile = 0x7FFFFFFF
self._hive_maxaddr_volatile = 0x7FFFFFFF
self._maxaddr = 0x80000000 | self._hive_maxaddr_volatile
vollog.log(constants.LOGLEVEL_VVVV,
f"Exception when setting hive {self.name} max address, using {hex(self._maxaddr)}")
vollog.log(
constants.LOGLEVEL_VVVV,
f"Exception when setting hive {self.name} max address, using {hex(self._maxaddr)}",
)
def _get_hive_maxaddr(self, volatile):
return self._hive_maxaddr_volatile if volatile else self._hive_maxaddr_non_volatile
return (
self._hive_maxaddr_volatile if volatile else self._hive_maxaddr_non_volatile
)
def get_name(self) -> str:
return self._cmhive_name or "[NONAME]"
@@ -93,42 +117,54 @@ class RegistryHive(linear.LinearlyMappedLayer):
def root_cell_offset(self) -> int:
"""Returns the offset for the root cell in this hive."""
with contextlib.suppress(InvalidAddressException):
if self._base_block.Signature.cast("string", max_length = 4, encoding = "latin-1") == 'regf':
if (
self._base_block.Signature.cast(
"string", max_length=4, encoding="latin-1"
)
== "regf"
):
return self._base_block.RootCell
return 0x20
def get_cell(self, cell_offset: int) -> 'objects.StructType':
def get_cell(self, cell_offset: int) -> "objects.StructType":
"""Returns the appropriate Cell value for a cell offset."""
# This would be an _HCELL containing CELL_DATA, but to save time we skip the size of the HCELL
cell = self._context.object(object_type = self._table_name + constants.BANG + "_CELL_DATA",
offset = cell_offset + 4,
layer_name = self.name)
cell = self._context.object(
object_type=self._table_name + constants.BANG + "_CELL_DATA",
offset=cell_offset + 4,
layer_name=self.name,
)
return cell
def get_node(self, cell_offset: int) -> 'objects.StructType':
def get_node(self, cell_offset: int) -> "objects.StructType":
"""Returns the appropriate Node, interpreted from the Cell based on its
Signature."""
cell = self.get_cell(cell_offset)
signature = cell.cast('string', max_length = 2, encoding = 'latin-1')
if signature == 'nk':
signature = cell.cast("string", max_length=2, encoding="latin-1")
if signature == "nk":
return cell.u.KeyNode
elif signature == 'sk':
elif signature == "sk":
return cell.u.KeySecurity
elif signature == 'vk':
elif signature == "vk":
return cell.u.KeyValue
elif signature == 'db':
elif signature == "db":
# Big Data
return cell.u.ValueData
elif signature == 'lf' or signature == 'lh' or signature == 'ri':
elif signature == "lf" or signature == "lh" or signature == "ri":
# Fast Leaf, Hash Leaf, Index Root
return cell.u.KeyIndex
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))
vollog.debug(
"Unknown Signature {} (0x{:x}) at offset {}".format(
signature, cell.u.KeyNode.Signature, cell_offset
)
)
return cell
def get_key(self, key: str, return_list: bool = False) -> Union[List[objects.StructType], objects.StructType]:
def get_key(
self, key: str, return_list: bool = False
) -> Union[List[objects.StructType], objects.StructType]:
"""Gets a specific registry key by key path.
return_list specifies whether the return result will be a single
@@ -138,7 +174,7 @@ class RegistryHive(linear.LinearlyMappedLayer):
node_key = [self.get_node(self.root_cell_offset)]
if key.endswith("\\"):
key = key[:-1]
key_array = key.split('\\')
key_array = key.split("\\")
found_key: List[str] = []
while key_array and node_key:
subkeys = node_key[-1].get_subkeys()
@@ -152,14 +188,18 @@ class RegistryHive(linear.LinearlyMappedLayer):
else:
node_key = []
if not node_key:
raise KeyError("Key {} not found under {}".format(key_array[0], '\\'.join(found_key)))
raise KeyError(
"Key {} not found under {}".format(key_array[0], "\\".join(found_key))
)
if return_list:
return node_key
return node_key[-1]
def visit_nodes(self,
visitor: Callable[[objects.StructType], None],
node: Optional[objects.StructType] = None) -> None:
def visit_nodes(
self,
visitor: Callable[[objects.StructType], None],
node: Optional[objects.StructType] = None,
) -> None:
"""Applies a callable (visitor) to all nodes within the registry tree
from a given node."""
if not node:
@@ -172,22 +212,28 @@ class RegistryHive(linear.LinearlyMappedLayer):
def _mask(value: int, high_bit: int, low_bit: int) -> int:
"""Returns the bits of a value between highbit and lowbit inclusive."""
high_mask = (2 ** (high_bit + 1)) - 1
low_mask = (2 ** low_bit) - 1
mask = (high_mask ^ low_mask)
low_mask = (2**low_bit) - 1
mask = high_mask ^ low_mask
# print(high_bit, low_bit, bin(mask), bin(value))
return value & mask
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
IntRequirement(name = 'hive_offset',
description = 'Offset within the base layer at which the hive lives',
default = 0,
optional = False),
requirements.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"),
TranslationLayerRequirement(name = 'base_layer',
description = 'Layer in which the registry hive lives',
optional = False)
IntRequirement(
name="hive_offset",
description="Offset within the base layer at which the hive lives",
default=0,
optional=False,
),
requirements.SymbolTableRequirement(
name="nt_symbols", description="Windows kernel symbols"
),
TranslationLayerRequirement(
name="base_layer",
description="Layer in which the registry hive lives",
optional=False,
),
]
def _translate(self, offset: int) -> int:
@@ -196,15 +242,20 @@ class RegistryHive(linear.LinearlyMappedLayer):
# Ignore the volatile bit when determining maxaddr validity
volatile = self._mask(offset, 31, 31) >> 31
if offset & 0x7fffffff > self._get_hive_maxaddr(volatile):
vollog.log(constants.LOGLEVEL_VVV,
"Layer {} couldn't translate offset {}, greater than {} in {} store of {}".format(
self.name,
hex(offset & 0x7fffffff),
hex(self._get_hive_maxaddr(volatile)),
"volative" if volatile else "non-volatile",
self.get_name()))
raise RegistryInvalidIndex(self.name, "Mapping request for value greater than maxaddr")
if offset & 0x7FFFFFFF > self._get_hive_maxaddr(volatile):
vollog.log(
constants.LOGLEVEL_VVV,
"Layer {} couldn't translate offset {}, greater than {} in {} store of {}".format(
self.name,
hex(offset & 0x7FFFFFFF),
hex(self._get_hive_maxaddr(volatile)),
"volative" if volatile else "non-volatile",
self.get_name(),
),
)
raise RegistryInvalidIndex(
self.name, "Mapping request for value greater than maxaddr"
)
storage = self.hive.Storage[volatile]
dir_index = self._mask(offset, 30, 21) >> 21
@@ -215,10 +266,9 @@ class RegistryHive(linear.LinearlyMappedLayer):
entry = table.Table[table_index]
return entry.get_block_offset() + suboffset
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
if length < 0:
raise ValueError("Mapping length of RegistryHive must be positive or zero")
@@ -234,7 +284,15 @@ class RegistryHive(linear.LinearlyMappedLayer):
chunk_size = min(chunk_size, remaining_length, self._page_size)
try:
translated_offset = self._translate(current_offset)
response.append((current_offset, chunk_size, translated_offset, chunk_size, self._base_layer))
response.append(
(
current_offset,
chunk_size,
translated_offset,
chunk_size,
self._base_layer,
)
)
except exceptions.LayerException:
if not ignore_errors:
raise
@@ -246,16 +304,18 @@ class RegistryHive(linear.LinearlyMappedLayer):
@property
def dependencies(self) -> List[str]:
"""Returns a list of layer names that this layer translates onto."""
return [self.config['base_layer']]
return [self.config["base_layer"]]
def is_valid(self, offset: int, length: int = 1) -> bool:
"""Returns a boolean based on whether the offset is valid or not."""
with contextlib.suppress(exceptions.InvalidAddressException):
# Pass this to the lower layers for now
return all([
self.context.layers[layer].is_valid(offset, length)
for (_, _, offset, length, layer) in self.mapping(offset, length)
])
return all(
[
self.context.layers[layer].is_valid(offset, length)
for (_, _, offset, length, layer) in self.mapping(offset, length)
]
)
return False
@property
+89 -54
View File
@@ -31,6 +31,7 @@ try:
# Import so that the handler is found by the framework.class_subclasses callc
import smb.SMBHandler # lgtm [py/unused-import]
except ImportError:
# If we fail to import this, it means that SMB handling won't be available
pass
vollog = logging.getLogger(__name__)
@@ -62,10 +63,12 @@ class ResourceAccessor(object):
list_handlers = True
def __init__(self,
progress_callback: Optional[constants.ProgressCallback] = None,
context: Optional[ssl.SSLContext] = None,
enable_cache: bool = True) -> None:
def __init__(
self,
progress_callback: Optional[constants.ProgressCallback] = None,
context: Optional[ssl.SSLContext] = None,
enable_cache: bool = True,
) -> None:
"""Creates a resource accessor.
Note: context is an SSL context, not a volatility context
@@ -75,20 +78,24 @@ class ResourceAccessor(object):
self._handlers = list(framework.class_subclasses(urllib.request.BaseHandler))
self._enable_cache = enable_cache
if self.list_handlers:
vollog.log(constants.LOGLEVEL_VVV,
f"Available URL handlers: {', '.join([x.__name__ for x in self._handlers])}")
vollog.log(
constants.LOGLEVEL_VVV,
f"Available URL handlers: {', '.join([x.__name__ for x in self._handlers])}",
)
self.__class__.list_handlers = False
def uses_cache(self, url: str) -> bool:
"""Determines whether a URLs contents should be cached"""
parsed_url = urllib.parse.urlparse(url)
return self._enable_cache and parsed_url.scheme not in self._non_cached_schemes()
return (
self._enable_cache and parsed_url.scheme not in self._non_cached_schemes()
)
@staticmethod
def _non_cached_schemes() -> List[str]:
"""Returns the list of schemes not to be cached"""
result = ['file']
result = ["file"]
for clazz in framework.class_subclasses(VolatilityHandler):
result += clazz.non_cached_schemes()
return result
@@ -102,34 +109,44 @@ class ResourceAccessor(object):
urllib.request.install_opener(urllib.request.build_opener(*self._handlers))
# Python bug 46654
if sys.platform == 'win32':
if sys.platform == "win32":
# We only need to worry about UNC paths on windows, on linux they'd be smb:// and need pysmb or similar
parsed_url = urllib.parse.urlparse(url, scheme = 'file')
parsed_url = urllib.parse.urlparse(url, scheme="file")
# Only worry about file scheme URLs, make sure that there's either a host or
# the unparsing left an extra slash at the start (which will get lost with urlunparse)
if parsed_url.scheme == 'file' and (parsed_url.netloc or parsed_url.path.startswith('//')):
if parsed_url.scheme == "file" and (
parsed_url.netloc or parsed_url.path.startswith("//")
):
# Change the netloc to '/' and then prepend the netloc to the path
# Urlunparse will remove extra initial slashes from path, hence setting netloc
new_url = urllib.parse.urlunparse((parsed_url.scheme, '/',
'/' + parsed_url.netloc + parsed_url.path, parsed_url.params,
parsed_url.query, parsed_url.fragment))
vollog.log(constants.LOGLEVEL_VVVV, f'UNC path detected, converted path {url} to {new_url}')
new_url = urllib.parse.urlunparse(
(
parsed_url.scheme,
"/",
"/" + parsed_url.netloc + parsed_url.path,
parsed_url.params,
parsed_url.query,
parsed_url.fragment,
)
)
vollog.log(
constants.LOGLEVEL_VVVV,
f"UNC path detected, converted path {url} to {new_url}",
)
url = new_url
try:
fp = urllib.request.urlopen(url, context = self._context)
fp = urllib.request.urlopen(url, context=self._context)
except error.URLError as excp:
if excp.args:
# TODO: As of python3.7 this can be removed
unverified_retrieval = (hasattr(ssl, "SSLCertVerificationError") and isinstance(
excp.args[0], ssl.SSLCertVerificationError)) or (isinstance(excp.args[0], ssl.SSLError) and
excp.args[0].reason == "CERTIFICATE_VERIFY_FAILED")
if unverified_retrieval:
vollog.warning("SSL certificate verification failed: attempting UNVERIFIED retrieval")
if isinstance(excp.args[0], ssl.SSLCertVerificationError):
vollog.warning(
"SSL certificate verification failed: attempting UNVERIFIED retrieval"
)
non_verifying_ctx = ssl.SSLContext()
non_verifying_ctx.check_hostname = False
non_verifying_ctx.verify_mode = ssl.CERT_NONE
fp = urllib.request.urlopen(url, context = non_verifying_ctx)
fp = urllib.request.urlopen(url, context=non_verifying_ctx)
else:
raise excp
else:
@@ -143,40 +160,43 @@ class ResourceAccessor(object):
if not self.uses_cache(url):
# ZipExtFiles (files in zips) cannot seek, so must be cached in order to use and/or decompress
curfile = urllib.request.urlopen(url, context = self._context)
curfile = urllib.request.urlopen(url, context=self._context)
else:
# TODO: find a way to check if we already have this file (look at http headers?)
block_size = 1028 * 8
temp_filename = os.path.join(
constants.CACHE_PATH,
"data_" + hashlib.sha512(bytes(url, 'raw_unicode_escape')).hexdigest() + ".cache")
"data_"
+ hashlib.sha512(bytes(url, "raw_unicode_escape")).hexdigest()
+ ".cache",
)
if not os.path.exists(temp_filename):
vollog.debug(f"Caching file at: {temp_filename}")
try:
content_length = fp.info().get('Content-Length', -1)
content_length = fp.info().get("Content-Length", -1)
except AttributeError:
# If our fp doesn't have an info member, carry on gracefully
content_length = -1
cache_file = open(temp_filename, "wb")
count = 0
block = fp.read(block_size)
while block:
count += len(block)
if self._progress_callback:
self._progress_callback(count * 100 / max(count, int(content_length)),
f"Reading file {url}")
cache_file.write(block)
with open(temp_filename, "wb") as cache_file:
count = 0
block = fp.read(block_size)
cache_file.close()
while block:
count += len(block)
if self._progress_callback:
self._progress_callback(
count * 100 / max(count, int(content_length)),
f"Reading file {url}",
)
cache_file.write(block)
block = fp.read(block_size)
else:
vollog.debug(f"Using already cached file at: {temp_filename}")
# Re-open the cache with a different mode
# Since we don't want people thinking they're able to save to the cache file,
# open it in read mode only and allow breakages to happen if they wanted to write
curfile = open(temp_filename, mode = "rb")
curfile = open(temp_filename, mode="rb")
# Determine whether the file is a particular type of file, and if so, open it as such
IMPORTED_MAGIC = False
@@ -192,13 +212,21 @@ class ResourceAccessor(object):
# Only file's python has magic.detect_from_fobj
if detected:
if detected.mime_type == 'application/x-xz':
curfile = cascadeCloseFile(lzma.LZMAFile(curfile, mode), curfile)
elif detected.mime_type == 'application/x-bzip2':
if detected.mime_type == "application/x-xz":
curfile = cascadeCloseFile(
lzma.LZMAFile(curfile, mode), curfile
)
elif detected.mime_type == "application/x-bzip2":
curfile = cascadeCloseFile(bz2.BZ2File(curfile, mode), curfile)
elif detected.mime_type == 'application/x-gzip':
curfile = cascadeCloseFile(gzip.GzipFile(fileobj = curfile, mode = mode), curfile)
if detected.mime_type in ['application/x-xz', 'application/x-bzip2', 'application/x-gzip']:
elif detected.mime_type == "application/x-gzip":
curfile = cascadeCloseFile(
gzip.GzipFile(fileobj=curfile, mode=mode), curfile
)
if detected.mime_type in [
"application/x-xz",
"application/x-bzip2",
"application/x-gzip",
]:
# Read and rewind to ensure we're inside any compressed file layers
curfile.read(1)
curfile.seek(0)
@@ -221,7 +249,9 @@ class ResourceAccessor(object):
elif extension == "bz2":
curfile = cascadeCloseFile(bz2.BZ2File(curfile, mode), curfile)
elif extension == "gz":
curfile = cascadeCloseFile(gzip.GzipFile(fileobj = curfile, mode = mode), curfile)
curfile = cascadeCloseFile(
gzip.GzipFile(fileobj=curfile, mode=mode), curfile
)
else:
stop = True
@@ -232,7 +262,6 @@ class ResourceAccessor(object):
class VolatilityHandler(urllib.request.BaseHandler):
@classmethod
def non_cached_schemes(cls) -> List[str]:
return []
@@ -250,21 +279,27 @@ class JarHandler(VolatilityHandler):
@classmethod
def non_cached_schemes(cls) -> List[str]:
return ['jar']
return ["jar"]
@staticmethod
def default_open(req: urllib.request.Request) -> Optional[Any]:
"""Handles the request if it's the jar scheme."""
if req.type == 'jar':
subscheme, remainder = req.full_url.split(":")[1], ":".join(req.full_url.split(":")[2:])
if subscheme != 'file':
vollog.log(constants.LOGLEVEL_VVV, f"Unsupported jar subscheme {subscheme}")
if req.type == "jar":
subscheme, remainder = req.full_url.split(":")[1], ":".join(
req.full_url.split(":")[2:]
)
if subscheme != "file":
vollog.log(
constants.LOGLEVEL_VVV, f"Unsupported jar subscheme {subscheme}"
)
return None
zipsplit = remainder.split("!")
if len(zipsplit) != 2:
vollog.log(constants.LOGLEVEL_VVV,
f"Path did not contain exactly one fragment indicator: {remainder}")
vollog.log(
constants.LOGLEVEL_VVV,
f"Path did not contain exactly one fragment indicator: {remainder}",
)
return None
zippath, filepath = zipsplit
@@ -275,6 +310,6 @@ class JarHandler(VolatilityHandler):
class OfflineHandler(VolatilityHandler):
@staticmethod
def default_open(req: urllib.request.Request) -> Optional[Any]:
if constants.OFFLINE and req.type in ['http', 'https']:
if constants.OFFLINE and req.type in ["http", "https"]:
raise exceptions.OfflineException(req.full_url)
return None
@@ -35,6 +35,7 @@ class RegExScanner(layers.ScannerInterface):
The default flags include DOTALL, since the searches are through binary data and the newline character should
have no specific significance in such searches"""
thread_safe = True
_required_framework_version = (2, 0, 0)
@@ -80,7 +81,7 @@ class MultiStringScanner(layers.ScannerInterface):
def _process_trie(self, trie: Optional[Dict[int, Optional[Dict]]]) -> bytes:
if trie is None or len(trie) == 1 and -1 in trie:
# We've reached the end of this path, return the empty byte string
return b''
return b""
choices = []
suffixes = []
@@ -101,16 +102,16 @@ class MultiStringScanner(layers.ScannerInterface):
if len(suffixes) == 1:
choices.append(suffixes[0])
elif len(suffixes) > 1:
choices.append(b'[' + b''.join(suffixes) + b']')
choices.append(b"[" + b"".join(suffixes) + b"]")
if len(choices) == 0:
# If there's none, return the empty byte string
response = b''
response = b""
elif len(choices) == 1:
# If there's only one return it
response = choices[0]
else:
response = b'(?:' + b'|'.join(choices) + b')'
response = b"(?:" + b"|".join(choices) + b")"
if finished:
# We finished one string, so everything after this is optional
@@ -118,7 +119,9 @@ class MultiStringScanner(layers.ScannerInterface):
return response
def __call__(self, data: bytes, data_offset: int) -> Generator[Tuple[int, bytes], None, None]:
def __call__(
self, data: bytes, data_offset: int
) -> Generator[Tuple[int, bytes], None, None]:
"""Runs through the data looking for the needles."""
for offset, pattern in self.search(data):
if offset < self.chunk_size:
@@ -128,6 +131,8 @@ class MultiStringScanner(layers.ScannerInterface):
if not isinstance(haystack, bytes):
raise TypeError("Search haystack must be a byte string")
if not self._regex:
raise ValueError("MultiRegexp cannot be used with an empty set of search strings")
raise ValueError(
"MultiRegexp cannot be used with an empty set of search strings"
)
for match in re.finditer(self._regex, haystack):
yield match.start(0), match.group()
@@ -11,7 +11,7 @@ class MultiRegexp(object):
def __init__(self) -> None:
self._pattern_strings: List[bytes] = []
self._regex = re.compile(b'')
self._regex = re.compile(b"")
def add_pattern(self, pattern: bytes) -> None:
self._pattern_strings.append(pattern)
@@ -19,12 +19,14 @@ class MultiRegexp(object):
def preprocess(self) -> None:
if not self._pattern_strings:
raise ValueError("No strings to compile into a regular expression")
self._regex = re.compile(b'|'.join(map(re.escape, self._pattern_strings)))
self._regex = re.compile(b"|".join(map(re.escape, self._pattern_strings)))
def search(self, haystack: bytes) -> Generator[Tuple[int, bytes], None, None]:
if not isinstance(haystack, bytes):
raise TypeError("Search haystack must be a byte string")
if not self._regex.pattern:
raise ValueError("MultiRegexp cannot be used with an empty set of search strings")
raise ValueError(
"MultiRegexp cannot be used with an empty set of search strings"
)
for match in re.finditer(self._regex, haystack):
yield (match.start(0), match.group())
+52 -24
View File
@@ -10,19 +10,25 @@ from volatility3.framework.configuration import requirements
from volatility3.framework.layers import linear
class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, metaclass = ABCMeta):
class NonLinearlySegmentedLayer(
interfaces.layers.TranslationLayerInterface, metaclass=ABCMeta
):
"""A class to handle a single run-based layer-to-layer mapping.
In the documentation "mapped address" or "mapped offset" refers to
an offset once it has been mapped to the underlying layer
"""
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(context = context, config_path = config_path, name = name, metadata = metadata)
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
super().__init__(
context=context, config_path=config_path, name=name, metadata=metadata
)
self._base_layer = self.config["base_layer"]
self._segments: List[Tuple[int, int, int, int]] = []
@@ -45,11 +51,17 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
try:
base_layer = self._context.layers[self._base_layer]
return all(
[base_layer.is_valid(mapped_offset) for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length)])
[
base_layer.is_valid(mapped_offset)
for _i, _i, mapped_offset, _i, _s in self.mapping(offset, length)
]
)
except exceptions.InvalidAddressException:
return False
def _find_segment(self, offset: int, next: bool = False) -> Tuple[int, int, int, int]:
def _find_segment(
self, offset: int, next: bool = False
) -> Tuple[int, int, int, int]:
"""Finds the segment containing a given offset.
Returns the segment tuple (offset, mapped_offset, length, mapped_length)
@@ -59,7 +71,10 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
self._load_segments()
# Find rightmost value less than or equal to x
i = bisect_right(self._segments, (offset, self.context.layers[self._base_layer].maximum_address))
i = bisect_right(
self._segments,
(offset, self.context.layers[self._base_layer].maximum_address),
)
if i and not next:
segment = self._segments[i - 1]
if segment[0] <= offset < segment[0] + segment[2]:
@@ -67,16 +82,17 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
if next:
if i < len(self._segments):
return self._segments[i]
raise exceptions.InvalidAddressException(self.name, offset, f"Invalid address at {offset:0x}")
raise exceptions.InvalidAddressException(
self.name, offset, f"Invalid address at {offset:0x}"
)
# Determines whether larger segments are in use and the offsets within them should be tracked linearly
# When no decoding of the data occurs, this should be set to true
_track_offset = False
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
"""Returns a sorted iterable of (offset, length, mapped_offset, mapped_length, layer)
mappings."""
done = False
@@ -84,7 +100,9 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
while not done:
try:
# Search for the appropriate segment that contains the current_offset
logical_offset, mapped_offset, size, mapped_size = self._find_segment(current_offset)
logical_offset, mapped_offset, size, mapped_size = self._find_segment(
current_offset
)
# If it starts before the current_offset, bring the lower edge up to the right place
if current_offset > logical_offset:
difference = current_offset - logical_offset
@@ -98,7 +116,12 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
raise
try:
# Find the next valid segment after our current_offset
logical_offset, mapped_offset, size, mapped_size = self._find_segment(current_offset, next = True)
(
logical_offset,
mapped_offset,
size,
mapped_size,
) = self._find_segment(current_offset, next=True)
# We know that the logical_offset must be greater than current_offset so skip to that value
current_offset = logical_offset
# If it starts too late then we're done
@@ -140,16 +163,21 @@ class NonLinearlySegmentedLayer(interfaces.layers.TranslationLayerInterface, met
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [requirements.TranslationLayerRequirement(name = 'base_layer', optional = False)]
return [
requirements.TranslationLayerRequirement(name="base_layer", optional=False)
]
class SegmentedLayer(NonLinearlySegmentedLayer, linear.LinearlyMappedLayer, metaclass = ABCMeta):
class SegmentedLayer(
NonLinearlySegmentedLayer, linear.LinearlyMappedLayer, metaclass=ABCMeta
):
_track_offset = True
def mapping(self,
offset: int,
length: int,
ignore_errors: bool = False) -> Iterable[Tuple[int, int, int, int, str]]:
def mapping(
self, offset: int, length: int, ignore_errors: bool = False
) -> Iterable[Tuple[int, int, int, int, str]]:
# Linear mappings must return the same length of segment as that requested
for offset, length, mapped_offset, mapped_length, layer in super().mapping(offset, length, ignore_errors):
for offset, length, mapped_offset, mapped_length, layer in super().mapping(
offset, length, ignore_errors
):
yield offset, length, mapped_offset, length, layer
+116 -50
View File
@@ -22,18 +22,23 @@ class VmwareLayer(segmented.SegmentedLayer):
header_structure = "<4sII"
group_structure = "64sQQ"
def __init__(self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None) -> None:
def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
# Construct these so we can use self.config
self._context = context
self._config_path = config_path
self._page_size = 0x1000
self._base_layer, self._meta_layer = self.config["base_layer"], self.config["meta_layer"]
self._base_layer, self._meta_layer = (
self.config["base_layer"],
self.config["meta_layer"],
)
# Then call the super, which will call load_segments (which needs the base_layer before it'll work)
super().__init__(context, config_path = config_path, name = name, metadata = metadata)
super().__init__(context, config_path=config_path, name=name, metadata=metadata)
def _load_segments(self) -> None:
"""Loads up the segments from the meta_layer."""
@@ -46,22 +51,33 @@ class VmwareLayer(segmented.SegmentedLayer):
def _read_header(self) -> None:
"""Checks the vmware header to make sure it's valid."""
if "vmware" not in self._context.symbol_space:
self._context.symbol_space.append(native.NativeTable("vmware", native.std_ctypes))
self._context.symbol_space.append(
native.NativeTable("vmware", native.std_ctypes)
)
meta_layer = self.context.layers.get(self._meta_layer, None)
header_size = struct.calcsize(self.header_structure)
data = meta_layer.read(0, header_size)
magic, unknown, groupCount = struct.unpack(self.header_structure, data)
if magic not in [b"\xD0\xBE\xD2\xBE", b"\xD1\xBA\xD1\xBA", b"\xD2\xBE\xD2\xBE", b"\xD3\xBE\xD3\xBE"]:
raise VmwareFormatException(self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}")
if magic not in [
b"\xD0\xBE\xD2\xBE",
b"\xD1\xBA\xD1\xBA",
b"\xD2\xBE\xD2\xBE",
b"\xD3\xBE\xD3\xBE",
]:
raise VmwareFormatException(
self.name, f"Wrong magic bytes for Vmware layer: {repr(magic)}"
)
version = magic[0] & 0xf
version = magic[0] & 0xF
group_size = struct.calcsize(self.group_structure)
groups = {}
for group in range(groupCount):
name, tag_location, _unknown = struct.unpack(
self.group_structure, meta_layer.read(header_size + (group * group_size), group_size))
self.group_structure,
meta_layer.read(header_size + (group * group_size), group_size),
)
name = name.rstrip(b"\x00")
groups[name] = tag_location
memory = groups[b"memory"]
@@ -75,43 +91,70 @@ class VmwareLayer(segmented.SegmentedLayer):
name_len = ord(meta_layer.read(offset + 1, 1))
tags_read = (flags == 0) and (name_len == 0)
if not tags_read:
name = self._context.object("vmware!string",
layer_name = self._meta_layer,
offset = offset + 2,
max_length = name_len)
name = self._context.object(
"vmware!string",
layer_name=self._meta_layer,
offset=offset + 2,
max_length=name_len,
)
indices_len = (flags >> 6) & 3
indices = []
for index in range(indices_len):
indices.append(
self._context.object("vmware!unsigned int",
offset = offset + name_len + 2 + (index * index_len),
layer_name = self._meta_layer))
data_len = flags & 0x3f
self._context.object(
"vmware!unsigned int",
offset=offset + name_len + 2 + (index * index_len),
layer_name=self._meta_layer,
)
)
data_len = flags & 0x3F
if data_len in [62, 63]: # Handle special data sizes that indicate a longer data stream
if data_len in [
62,
63,
]: # Handle special data sizes that indicate a longer data stream
data_len = 4 if version == 0 else 8
# Read the size of the data
data_size = self._context.object(self._choose_type(data_len),
layer_name = self._meta_layer,
offset = offset + 2 + name_len + (indices_len * index_len))
data_size = self._context.object(
self._choose_type(data_len),
layer_name=self._meta_layer,
offset=offset + 2 + name_len + (indices_len * index_len),
)
# Skip two bytes of padding (as it seems?)
# Read the actual data
data = self._context.object("vmware!bytes",
layer_name = self._meta_layer,
offset = offset + 2 + name_len + (indices_len * index_len) +
2 * data_len + 2,
length = data_size)
offset += 2 + name_len + (indices_len * index_len) + 2 * data_len + 2 + data_size
data = self._context.object(
"vmware!bytes",
layer_name=self._meta_layer,
offset=offset
+ 2
+ name_len
+ (indices_len * index_len)
+ 2 * data_len
+ 2,
length=data_size,
)
offset += (
2
+ name_len
+ (indices_len * index_len)
+ 2 * data_len
+ 2
+ data_size
)
else: # Handle regular cases
data = self._context.object(self._choose_type(data_len),
layer_name = self._meta_layer,
offset = offset + 2 + name_len + (indices_len * index_len))
data = self._context.object(
self._choose_type(data_len),
layer_name=self._meta_layer,
offset=offset + 2 + name_len + (indices_len * index_len),
)
offset += 2 + name_len + (indices_len * index_len) + data_len
tags[(name, tuple(indices))] = (flags, data)
if tags[("regionsCount", ())][1] == 0:
raise VmwareFormatException(self.name, "VMware VMEM is not split into regions")
raise VmwareFormatException(
self.name, "VMware VMEM is not split into regions"
)
for region in range(tags[("regionsCount", ())][1]):
offset = tags[("regionPPN", (region,))][1] * self._page_size
mapped_offset = tags[("regionPageNum", (region,))][1] * self._page_size
@@ -127,8 +170,8 @@ class VmwareLayer(segmented.SegmentedLayer):
"""This vmware translation layer always requires a separate metadata
layer."""
return [
requirements.TranslationLayerRequirement(name = 'base_layer', optional = False),
requirements.TranslationLayerRequirement(name = 'meta_layer', optional = False)
requirements.TranslationLayerRequirement(name="base_layer", optional=False),
requirements.TranslationLayerRequirement(name="meta_layer", optional=False),
]
@@ -136,10 +179,12 @@ class VmwareStacker(interfaces.automagic.StackerLayerInterface):
stack_order = 20
@classmethod
def stack(cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None) -> Optional[interfaces.layers.DataLayerInterface]:
def stack(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
progress_callback: constants.ProgressCallback = None,
) -> Optional[interfaces.layers.DataLayerInterface]:
"""Attempt to stack this based on the starting information."""
memlayer = context.layers[layer_name]
if not isinstance(memlayer, physical.FileLayer):
@@ -149,31 +194,52 @@ class VmwareStacker(interfaces.automagic.StackerLayerInterface):
vmss = location[:-5] + ".vmss"
vmsn = location[:-5] + ".vmsn"
current_layer_name = context.layers.free_layer_name("VmwareMetaLayer")
current_config_path = interfaces.configuration.path_join("automagic", "layer_stacker", "stack",
current_layer_name)
current_config_path = interfaces.configuration.path_join(
"automagic", "layer_stacker", "stack", current_layer_name
)
vmss_success = False
with contextlib.suppress(IOError):
_ = resources.ResourceAccessor().open(vmss).read(10)
context.config[interfaces.configuration.path_join(current_config_path, "location")] = vmss
context.layers.add_layer(physical.FileLayer(context, current_config_path, current_layer_name))
with resources.ResourceAccessor().open(vmss) as fp:
_ = fp.read(10)
context.config[
interfaces.configuration.path_join(current_config_path, "location")
] = vmss
context.layers.add_layer(
physical.FileLayer(context, current_config_path, current_layer_name)
)
vmss_success = True
vmsn_success = False
if not vmss_success:
with contextlib.suppress(IOError):
_ = resources.ResourceAccessor().open(vmsn).read(10)
context.config[interfaces.configuration.path_join(current_config_path, "location")] = vmsn
context.layers.add_layer(physical.FileLayer(context, current_config_path, current_layer_name))
context.config[
interfaces.configuration.path_join(
current_config_path, "location"
)
] = vmsn
context.layers.add_layer(
physical.FileLayer(
context, current_config_path, current_layer_name
)
)
vmsn_success = True
vollog.log(constants.LOGLEVEL_VVVV, f"Metadata found: VMSS ({vmss_success}) or VMSN ({vmsn_success})")
vollog.log(
constants.LOGLEVEL_VVVV,
f"Metadata found: VMSS ({vmss_success}) or VMSN ({vmsn_success})",
)
if not vmss_success and not vmsn_success:
return None
new_layer_name = context.layers.free_layer_name("VmwareLayer")
context.config[interfaces.configuration.path_join(current_config_path, "base_layer")] = layer_name
context.config[interfaces.configuration.path_join(current_config_path, "meta_layer")] = current_layer_name
context.config[
interfaces.configuration.path_join(current_config_path, "base_layer")
] = layer_name
context.config[
interfaces.configuration.path_join(current_config_path, "meta_layer")
] = current_layer_name
new_layer = VmwareLayer(context, current_config_path, new_layer_name)
return new_layer
return None
File diff suppressed because it is too large Load Diff
+40 -14
View File
@@ -22,13 +22,22 @@ class ObjectTemplate(interfaces.objects.Template):
* etc
"""
def __init__(self, object_class: Type[interfaces.objects.ObjectInterface], type_name: str, **arguments) -> None:
arguments['object_class'] = object_class
super().__init__(type_name = type_name, **arguments)
def __init__(
self,
object_class: Type[interfaces.objects.ObjectInterface],
type_name: str,
**arguments,
) -> None:
arguments["object_class"] = object_class
super().__init__(type_name=type_name, **arguments)
proxy_cls = self.vol.object_class.VolTemplateProxy
for method_name in proxy_cls._methods:
setattr(self, method_name, functools.partial(getattr(proxy_cls, method_name), self))
setattr(
self,
method_name,
functools.partial(getattr(proxy_cls, method_name), self),
)
@property
def size(self) -> int:
@@ -54,28 +63,39 @@ class ObjectTemplate(interfaces.objects.Template):
plateProxy`)"""
return self.vol.object_class.VolTemplateProxy.child_template(self, child)
def replace_child(self, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template) -> None:
def replace_child(
self,
old_child: interfaces.objects.Template,
new_child: interfaces.objects.Template,
) -> None:
"""Replaces `old_child` for `new_child` in the templated object's child
list (see :class:`~volatility3.framework.interfaces.objects.ObjectInterf
ace.VolTemplateProxy`)"""
return self.vol.object_class.VolTemplateProxy.replace_child(self, old_child, new_child)
return self.vol.object_class.VolTemplateProxy.replace_child(
self, old_child, new_child
)
def has_member(self, member_name: str) -> bool:
"""Returns whether the object would contain a member called
member_name."""
return self.vol.object_class.VolTemplateProxy.has_member(self, member_name)
def __call__(self, context: interfaces.context.ContextInterface,
object_info: interfaces.objects.ObjectInformation) -> interfaces.objects.ObjectInterface:
def __call__(
self,
context: interfaces.context.ContextInterface,
object_info: interfaces.objects.ObjectInformation,
) -> interfaces.objects.ObjectInterface:
"""Constructs the object.
Returns: an object adhering to the :class:`~volatility3.framework.interfaces.objects.ObjectInterface`
"""
arguments: Dict[str, Any] = {}
for arg in self.vol:
if arg != 'object_class':
if arg != "object_class":
arguments[arg] = self.vol[arg]
return self.vol.object_class(context = context, object_info = object_info, **arguments)
return self.vol.object_class(
context=context, object_info=object_info, **arguments
)
class ReferenceTemplate(interfaces.objects.Template):
@@ -99,8 +119,10 @@ class ReferenceTemplate(interfaces.objects.Template):
table_name = type_name[0]
symbol_name = type_name[-1]
raise exceptions.SymbolError(
symbol_name, table_name,
f"Template contains no information about its structure: {self.vol.type_name}")
symbol_name,
table_name,
f"Template contains no information about its structure: {self.vol.type_name}",
)
size: ClassVar[Any] = property(_unresolved)
replace_child: ClassVar[Any] = _unresolved
@@ -108,6 +130,10 @@ class ReferenceTemplate(interfaces.objects.Template):
child_template: ClassVar[Any] = _unresolved
has_member: ClassVar[Any] = _unresolved
def __call__(self, context: interfaces.context.ContextInterface, object_info: interfaces.objects.ObjectInformation):
def __call__(
self,
context: interfaces.context.ContextInterface,
object_info: interfaces.objects.ObjectInformation,
):
template = context.symbol_space.get_type(self.vol.type_name)
return template(context = context, object_info = object_info)
return template(context=context, object_info=object_info)
+20 -13
View File
@@ -7,9 +7,9 @@ from typing import Optional, Union
from volatility3.framework import interfaces, objects, constants
def array_to_string(array: 'objects.Array',
count: Optional[int] = None,
errors: str = 'replace') -> interfaces.objects.ObjectInterface:
def array_to_string(
array: "objects.Array", count: Optional[int] = None, errors: str = "replace"
) -> interfaces.objects.ObjectInterface:
"""Takes a volatility Array of characters and returns a string."""
# TODO: Consider checking the Array's target is a native char
if count is None:
@@ -17,28 +17,35 @@ def array_to_string(array: 'objects.Array',
if not isinstance(array, objects.Array):
raise TypeError("Array_to_string takes an Array of char")
return array.cast("string", max_length = count, errors = errors)
return array.cast("string", max_length=count, errors=errors)
def pointer_to_string(pointer: 'objects.Pointer', count: int, errors: str = 'replace'):
def pointer_to_string(pointer: "objects.Pointer", count: int, errors: str = "replace"):
"""Takes a volatility Pointer to characters and returns a string."""
if not isinstance(pointer, objects.Pointer):
raise TypeError("pointer_to_string takes a Pointer")
if count < 1:
raise ValueError("pointer_to_string requires a positive count")
char = pointer.dereference()
return char.cast("string", max_length = count, errors = errors)
return char.cast("string", max_length=count, errors=errors)
def array_of_pointers(array: interfaces.objects.ObjectInterface, count: int,
subtype: Union[str, interfaces.objects.Template],
context: interfaces.context.ContextInterface) -> interfaces.objects.ObjectInterface:
def array_of_pointers(
array: interfaces.objects.ObjectInterface,
count: int,
subtype: Union[str, interfaces.objects.Template],
context: interfaces.context.ContextInterface,
) -> interfaces.objects.ObjectInterface:
"""Takes an object, and recasts it as an array of pointers to subtype."""
symbol_table = array.vol.type_name.split(constants.BANG)[0]
if isinstance(subtype, str) and context is not None:
subtype = context.symbol_space.get_type(subtype)
if not isinstance(subtype, interfaces.objects.Template) or subtype is None:
raise TypeError("Subtype must be a valid template (or string name of an object template)")
subtype_pointer = context.symbol_space.get_type(symbol_table + constants.BANG + "pointer")
subtype_pointer.update_vol(subtype = subtype)
return array.cast("array", count = count, subtype = subtype_pointer)
raise TypeError(
"Subtype must be a valid template (or string name of an object template)"
)
subtype_pointer = context.symbol_space.get_type(
symbol_table + constants.BANG + "pointer"
)
subtype_pointer.update_vol(subtype=subtype)
return array.cast("array", count=count, subtype=subtype_pointer)
+22 -9
View File
@@ -15,11 +15,14 @@ from volatility3.framework import interfaces, automagic, exceptions, constants
vollog = logging.getLogger(__name__)
def construct_plugin(context: interfaces.context.ContextInterface,
automagics: List[interfaces.automagic.AutomagicInterface],
plugin: Type[interfaces.plugins.PluginInterface], base_config_path: str,
progress_callback: constants.ProgressCallback,
open_method: Type[interfaces.plugins.FileHandlerInterface]) -> interfaces.plugins.PluginInterface:
def construct_plugin(
context: interfaces.context.ContextInterface,
automagics: List[interfaces.automagic.AutomagicInterface],
plugin: Type[interfaces.plugins.PluginInterface],
base_config_path: str,
progress_callback: constants.ProgressCallback,
open_method: Type[interfaces.plugins.FileHandlerInterface],
) -> interfaces.plugins.PluginInterface:
"""Constructs a plugin object based on the parameters.
Clever magic figures out how to fulfill each requirement that might not be fulfilled
@@ -35,9 +38,17 @@ def construct_plugin(context: interfaces.context.ContextInterface,
Returns:
The constructed plugin object
"""
errors = automagic.run(automagics, context, plugin, base_config_path, progress_callback = progress_callback)
errors = automagic.run(
automagics,
context,
plugin,
base_config_path,
progress_callback=progress_callback,
)
# Plugins always get their configuration stored under their plugin name
plugin_config_path = interfaces.configuration.path_join(base_config_path, plugin.__name__)
plugin_config_path = interfaces.configuration.path_join(
base_config_path, plugin.__name__
)
# Check all the requirements and/or go back to the automagic step
unsatisfied = plugin.unsatisfied(context, plugin_config_path)
@@ -45,10 +56,12 @@ def construct_plugin(context: interfaces.context.ContextInterface,
for error in errors:
error_string = [x for x in error.format_exception_only()][-1]
vollog.warning(f"Automagic exception occurred: {error_string[:-1]}")
vollog.log(constants.LOGLEVEL_V, "".join(error.format(chain = True)))
vollog.log(constants.LOGLEVEL_V, "".join(error.format(chain=True)))
raise exceptions.UnsatisfiedException(unsatisfied)
constructed = plugin(context, plugin_config_path, progress_callback = progress_callback)
constructed = plugin(
context, plugin_config_path, progress_callback=progress_callback
)
if open_method:
constructed.set_open_method(open_method)
return constructed
+27 -12
View File
@@ -19,32 +19,47 @@ class Banners(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [requirements.TranslationLayerRequirement(name = 'primary', description = 'Memory layer to scan')]
return [
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer to scan"
)
]
def _generator(self):
layer = self.context.layers[self.config['primary']]
layer = self.context.layers[self.config["primary"]]
if isinstance(layer, layers.intel.Intel):
layer = self.context.layers[layer.config['memory_layer']]
layer = self.context.layers[layer.config["memory_layer"]]
for offset, banner in self.locate_banners(self.context, layer.name):
yield 0, (offset, banner)
@classmethod
def locate_banners(cls, context: interfaces.context.ContextInterface, layer_name: str):
def locate_banners(
cls, context: interfaces.context.ContextInterface, layer_name: str
):
"""Identifies banners from a memory image"""
layer = context.layers[layer_name]
for offset in layer.scan(
context = context,
scanner = scanners.RegExScanner(rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+")):
data = layer.read(offset, 0xfff)
data_index = data.find(b'\x00')
context=context,
scanner=scanners.RegExScanner(
rb"(Linux version|Darwin Kernel Version) [0-9]+\.[0-9]+\.[0-9]+"
),
):
data = layer.read(offset, 0xFFF)
data_index = data.find(b"\x00")
if data_index > 0:
data = data[:data_index].strip()
failed = [
char for char in data
if char not in b' #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~'
char
for char in data
if char
not in b" #()+,;/-.0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"
]
if not failed:
yield format_hints.Hex(offset), str(data, encoding = 'latin-1', errors = '?')
yield format_hints.Hex(offset), str(
data, encoding="latin-1", errors="?"
)
def run(self):
return renderers.TreeGrid([("Offset", format_hints.Hex), ("Banner", str)], self._generator())
return renderers.TreeGrid(
[("Offset", format_hints.Hex), ("Banner", str)], self._generator()
)
+21 -10
View File
@@ -22,25 +22,36 @@ class ConfigWriter(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.TranslationLayerRequirement(name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]),
requirements.BooleanRequirement(name = 'extra',
description = 'Outputs whole configuration tree',
default = False,
optional = True)
requirements.TranslationLayerRequirement(
name="primary",
description="Memory layer for the kernel",
architectures=["Intel32", "Intel64"],
),
requirements.BooleanRequirement(
name="extra",
description="Outputs whole configuration tree",
default=False,
optional=True,
),
]
def _generator(self):
filename = "config.json"
config = dict(self.build_configuration())
if self.config.get('extra', False):
vollog.debug("Outputting additional information, this will NOT work with the -c option")
if self.config.get("extra", False):
vollog.debug(
"Outputting additional information, this will NOT work with the -c option"
)
config = dict(self.context.config)
filename = "config.extra"
try:
with self.open(filename) as file_data:
file_data.write(bytes(json.dumps(config, sort_keys = True, indent = 2), 'raw_unicode_escape'))
file_data.write(
bytes(
json.dumps(config, sort_keys=True, indent=2),
"raw_unicode_escape",
)
)
except Exception as excp:
vollog.warning(f"Unable to JSON encode configuration: {excp}")
@@ -20,19 +20,19 @@ class FrameworkInfo(plugins.PluginInterface):
def _generator(self):
categories = {
'Automagic': interfaces.automagic.AutomagicInterface,
'Requirement': interfaces.configuration.RequirementInterface,
'Layer': interfaces.layers.DataLayerInterface,
'LayerStacker': interfaces.automagic.StackerLayerInterface,
'Object': interfaces.objects.ObjectInterface,
'Plugin': interfaces.plugins.PluginInterface,
'Renderer': interfaces.renderers.Renderer
"Automagic": interfaces.automagic.AutomagicInterface,
"Requirement": interfaces.configuration.RequirementInterface,
"Layer": interfaces.layers.DataLayerInterface,
"LayerStacker": interfaces.automagic.StackerLayerInterface,
"Object": interfaces.objects.ObjectInterface,
"Plugin": interfaces.plugins.PluginInterface,
"Renderer": interfaces.renderers.Renderer,
}
for category, module_interface in categories.items():
yield (0, (category, ))
yield (0, (category,))
for clazz in framework.class_subclasses(module_interface):
yield (1, (clazz.__name__, ))
yield (1, (clazz.__name__,))
def run(self):
return renderers.TreeGrid([("Data", str)], self._generator())
+102 -48
View File
@@ -27,41 +27,53 @@ class IsfInfo(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ListRequirement(name = 'filter',
description = 'String that must be present in the file URI to display the ISF',
optional = True,
default = []),
requirements.URIRequirement(name = 'isf',
description = "Specific ISF file to process",
default = None,
optional = True),
requirements.BooleanRequirement(name = 'validate',
description = 'Validate against schema if possible',
default = False,
optional = True),
requirements.VersionRequirement(name = 'SQLiteCache',
component = symbol_cache.SqliteCache,
version = (1, 0, 0)),
requirements.BooleanRequirement(name = 'live',
description = 'Traverse all files, rather than use the cache',
default = False,
optional = True)
requirements.ListRequirement(
name="filter",
description="String that must be present in the file URI to display the ISF",
optional=True,
default=[],
),
requirements.URIRequirement(
name="isf",
description="Specific ISF file to process",
default=None,
optional=True,
),
requirements.BooleanRequirement(
name="validate",
description="Validate against schema if possible",
default=False,
optional=True,
),
requirements.VersionRequirement(
name="SQLiteCache",
component=symbol_cache.SqliteCache,
version=(1, 0, 0),
),
requirements.BooleanRequirement(
name="live",
description="Traverse all files, rather than use the cache",
default=False,
optional=True,
),
]
@classmethod
def list_all_isf_files(cls) -> Generator[str, None, None]:
"""Lists all the ISF files that can be found"""
for symbol_path in symbols.__path__:
for root, dirs, files in os.walk(symbol_path, followlinks = True):
for root, dirs, files in os.walk(symbol_path, followlinks=True):
for filename in files:
base_name = os.path.join(root, filename)
if filename.endswith('zip'):
with zipfile.ZipFile(base_name, 'r') as zfile:
if filename.endswith("zip"):
with zipfile.ZipFile(base_name, "r") as zfile:
for name in zfile.namelist():
for extension in constants.ISF_EXTENSIONS:
# By ending with an extension (and therefore, not /), we should not return any directories
if name.endswith(extension):
yield "jar:file:" + str(pathlib.Path(base_name)) + "!" + name
yield "jar:file:" + str(
pathlib.Path(base_name)
) + "!" + name
else:
for extension in constants.ISF_EXTENSIONS:
@@ -69,81 +81,123 @@ class IsfInfo(plugins.PluginInterface):
yield pathlib.Path(base_name).as_uri()
def _generator(self):
if self.config.get('isf', None) is not None:
file_list = [self.config['isf']]
if self.config.get("isf", None) is not None:
file_list = [self.config["isf"]]
else:
file_list = list(self.list_all_isf_files())
# Filter the files
filtered_list = []
if not len(self.config['filter']):
if not len(self.config["filter"]):
filtered_list = file_list
else:
for isf_file in file_list:
for filter_item in self.config['filter']:
for filter_item in self.config["filter"]:
if filter_item in isf_file:
filtered_list.append(isf_file)
try:
import jsonschema
if not self.config['validate']:
if not self.config["validate"]:
raise ImportError # Act as if we couldn't import if validation is turned off
def check_valid(data):
return "True" if schemas.validate(data, True) else "False"
except ImportError:
def check_valid(data):
return "Unknown"
if self.config['live']:
if self.config["live"]:
# Process the filtered list
for entry in filtered_list:
num_types = num_enums = num_bases = num_symbols = 0
valid = "Unknown"
with resources.ResourceAccessor().open(url = entry) as fp:
with resources.ResourceAccessor().open(url=entry) as fp:
try:
data = json.load(fp)
num_symbols = len(data.get('symbols', []))
num_types = len(data.get('user_types', []))
num_enums = len(data.get('enums', []))
num_bases = len(data.get('base_types', []))
num_symbols = len(data.get("symbols", []))
num_types = len(data.get("user_types", []))
num_enums = len(data.get("enums", []))
num_bases = len(data.get("base_types", []))
identifiers_path = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
identifier_cache = symbol_cache.SqliteCache(identifiers_path)
identifier = identifier_cache.get_identifier(location = entry)
identifier = identifier_cache.get_identifier(location=entry)
if identifier:
identifier = identifier.decode('utf-8', errors = 'replace')
identifier = identifier.decode("utf-8", errors="replace")
else:
identifier = renderers.NotAvailableValue()
valid = check_valid(data)
except (UnicodeDecodeError, json.decoder.JSONDecodeError):
vollog.warning(f"Invalid ISF: {entry}")
yield (0, (entry, valid, num_bases, num_types, num_symbols, num_enums, identifier))
yield (
0,
(
entry,
valid,
num_bases,
num_types,
num_symbols,
num_enums,
identifier,
),
)
else:
identifiers_path = os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME)
identifiers_path = os.path.join(
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
)
cache = symbol_cache.SqliteCache(identifiers_path)
valid = 'Unknown'
valid = "Unknown"
for identifier, location in cache.get_identifier_dictionary().items():
num_bases, num_types, num_enums, num_symbols = cache.get_location_statistics(location)
(
num_bases,
num_types,
num_enums,
num_symbols,
) = cache.get_location_statistics(location)
if identifier:
json_hash = cache.get_hash(location)
if json_hash and json_hash in schemas.cached_validations:
valid = 'True (cached)'
if self.config['validate']:
valid = "True (cached)"
if self.config["validate"]:
# Even if we're not live, if we've been explicitly asked to validate, then do-so
with resources.ResourceAccessor().open(url = location) as fp:
with resources.ResourceAccessor().open(url=location) as fp:
try:
data = json.load(fp)
valid = check_valid(data)
except (UnicodeDecodeError, json.decoder.JSONDecodeError):
vollog.warning(f"Invalid ISF: {location}")
yield (0, (location, valid, num_bases, num_types, num_symbols, num_enums, str(identifier)))
yield (
0,
(
location,
valid,
num_bases,
num_types,
num_symbols,
num_enums,
str(identifier),
),
)
# Try to open the file, load it as JSON, read the data from it
def run(self):
return renderers.TreeGrid([("URI", str), ("Valid", str),
("Number of base_types", int), ("Number of types", int), ("Number of symbols", int),
("Number of enums", int), ("Identifying information", str)], self._generator())
return renderers.TreeGrid(
[
("URI", str),
("Valid", str),
("Number of base_types", int),
("Number of types", int),
("Number of symbols", int),
("Number of enums", int),
("Identifying information", str),
],
self._generator(),
)
+58 -42
View File
@@ -23,32 +23,40 @@ class LayerWriter(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.TranslationLayerRequirement(name = 'primary', description = 'Memory layer for the kernel'),
requirements.IntRequirement(name = 'block_size',
description = "Size of blocks to copy over",
default = cls.default_block_size,
optional = True),
requirements.BooleanRequirement(name = 'list',
description = 'List available layers',
default = False,
optional = True),
requirements.TranslationLayerRequirement(
name="primary", description="Memory layer for the kernel"
),
requirements.IntRequirement(
name="block_size",
description="Size of blocks to copy over",
default=cls.default_block_size,
optional=True,
),
requirements.BooleanRequirement(
name="list",
description="List available layers",
default=False,
optional=True,
),
requirements.ListRequirement(
name = 'layers',
element_type = str,
description = 'Names of layers to write (defaults to the highest non-mapped layer)',
default = None,
optional = True)
name="layers",
element_type=str,
description="Names of layers to write (defaults to the highest non-mapped layer)",
default=None,
optional=True,
),
]
@classmethod
def write_layer(
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
preferred_name: str,
open_method: Type[plugins.FileHandlerInterface],
chunk_size: Optional[int] = None,
progress_callback: Optional[constants.ProgressCallback] = None) -> Optional[plugins.FileHandlerInterface]:
cls,
context: interfaces.context.ContextInterface,
layer_name: str,
preferred_name: str,
open_method: Type[plugins.FileHandlerInterface],
chunk_size: Optional[int] = None,
progress_callback: Optional[constants.ProgressCallback] = None,
) -> Optional[plugins.FileHandlerInterface]:
"""Produces a FileHandler from the named layer in the provided context or None on failure
Args:
@@ -70,42 +78,48 @@ class LayerWriter(plugins.PluginInterface):
file_handle = open_method(preferred_name)
for i in range(0, layer.maximum_address, chunk_size):
current_chunk_size = min(chunk_size, layer.maximum_address - i)
data = layer.read(i, current_chunk_size, pad = True)
data = layer.read(i, current_chunk_size, pad=True)
file_handle.write(data)
if progress_callback:
progress_callback((i / layer.maximum_address) * 100, f'Writing layer {layer_name}')
progress_callback(
(i / layer.maximum_address) * 100, f"Writing layer {layer_name}"
)
return file_handle
def _generator(self):
if self.config['list']:
if self.config["list"]:
for name in self.context.layers:
yield 0, (name, )
yield 0, (name,)
else:
# Choose the most recently added layer that isn't virtual
if not self.config['layers']:
self.config['layers'] = []
if not self.config["layers"]:
self.config["layers"] = []
for name in self.context.layers:
if not self.context.layers[name].metadata.get('mapped', False):
self.config['layers'] = [name]
if not self.context.layers[name].metadata.get("mapped", False):
self.config["layers"] = [name]
for name in self.config['layers']:
for name in self.config["layers"]:
# Check the layer exists and validate the output file
if name not in self.context.layers:
yield 0, (f'Layer Name {name} does not exist', )
yield 0, (f"Layer Name {name} does not exist",)
else:
output_name = self.config.get('output', ".".join([name, "raw"]))
output_name = self.config.get("output", ".".join([name, "raw"]))
try:
file_handle = self.write_layer(self.context,
name,
output_name,
self.open,
self.config.get('block_size', self.default_block_size),
progress_callback = self._progress_callback)
file_handle = self.write_layer(
self.context,
name,
output_name,
self.open,
self.config.get("block_size", self.default_block_size),
progress_callback=self._progress_callback,
)
file_handle.close()
except IOError as excp:
yield 0, (f"Layer cannot be written to {self.config['output_name']}: {excp}", )
yield 0, (
f"Layer cannot be written to {self.config['output_name']}: {excp}",
)
yield 0, (f'Layer has been written to {output_name}', )
yield 0, (f"Layer has been written to {output_name}",)
def _generate_layers(self):
"""List layer names from this run"""
@@ -113,6 +127,8 @@ class LayerWriter(plugins.PluginInterface):
yield (0, (name, self.context.layers[name].__class__.__name__))
def run(self):
if self.config['list']:
return renderers.TreeGrid([("Layer name", str), ('Layer type', str)], self._generate_layers())
if self.config["list"]:
return renderers.TreeGrid(
[("Layer name", str), ("Layer type", str)], self._generate_layers()
)
return renderers.TreeGrid([("Status", str)], self._generator())
+63 -34
View File
@@ -26,18 +26,27 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
element_type = int,
description = "Process IDs to include (all other processes are excluded)",
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
element_type=int,
description="Process IDs to include (all other processes are excluded)",
optional=True,
),
]
def _generator(self, tasks):
vmlinux = self.context.modules[self.config["kernel"]]
is_32bit = not symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name)
is_32bit = not symbols.symbol_table_is_64bit(
self.context, vmlinux.symbol_table_name
)
if is_32bit:
pack_format = "I"
bash_json_file = "bash32"
@@ -45,10 +54,13 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
pack_format = "Q"
bash_json_file = "bash64"
bash_table_name = BashIntermedSymbols.create(self.context, self.config_path, "linux", bash_json_file)
bash_table_name = BashIntermedSymbols.create(
self.context, self.config_path, "linux", bash_json_file
)
ts_offset = self.context.symbol_space.get_type(bash_table_name + constants.BANG +
"hist_entry").relative_child_offset("timestamp")
ts_offset = self.context.symbol_space.get_type(
bash_table_name + constants.BANG + "hist_entry"
).relative_child_offset("timestamp")
for task in tasks:
task_name = utility.array_to_string(task.comm)
@@ -64,44 +76,61 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
bang_addrs = []
# find '#' values on the heap
for address in proc_layer.scan(self.context,
scanners.BytesScanner(b"#"),
sections = task.get_process_memory_sections(heap_only = True)):
for address in proc_layer.scan(
self.context,
scanners.BytesScanner(b"#"),
sections=task.get_process_memory_sections(heap_only=True),
):
bang_addrs.append(struct.pack(pack_format, address))
history_entries = []
if bang_addrs:
for address, _ in proc_layer.scan(self.context,
scanners.MultiStringScanner(bang_addrs),
sections = task.get_process_memory_sections(heap_only = True)):
hist = self.context.object(bash_table_name + constants.BANG + "hist_entry",
offset = address - ts_offset,
layer_name = proc_layer_name)
for address, _ in proc_layer.scan(
self.context,
scanners.MultiStringScanner(bang_addrs),
sections=task.get_process_memory_sections(heap_only=True),
):
hist = self.context.object(
bash_table_name + constants.BANG + "hist_entry",
offset=address - ts_offset,
layer_name=proc_layer_name,
)
if hist.is_valid():
history_entries.append(hist)
for hist in sorted(history_entries, key = lambda x: x.get_time_as_integer()):
yield (0, (task.pid, task_name, hist.get_time_object(), hist.get_command()))
for hist in sorted(history_entries, key=lambda x: x.get_time_as_integer()):
yield (
0,
(task.pid, task_name, hist.get_time_object(), hist.get_command()),
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid([("PID", int), ("Process", str), ("CommandTime", datetime.datetime),
("Command", str)],
self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("CommandTime", datetime.datetime),
("Command", str),
],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
def generate_timeline(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
for row in self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)):
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
):
_depth, row_data = row
description = f"{row_data[0]} ({row_data[1]}): \"{row_data[3]}\""
description = f'{row_data[0]} ({row_data[1]}): "{row_data[3]}"'
yield (description, timeliner.TimeLinerType.CREATED, row_data[2])
@@ -23,8 +23,11 @@ class Check_afinfo(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
]
# returns whether the symbol is found within the kernel (system.map) or not
@@ -40,7 +43,7 @@ class Check_afinfo(plugins.PluginInterface):
continue
if check == "write":
addr = var_ops.member(attr = 'write')
addr = var_ops.member(attr="write")
else:
addr = getattr(var_ops, check)
@@ -48,12 +51,16 @@ class Check_afinfo(plugins.PluginInterface):
yield check, addr
def _check_afinfo(self, var_name, var, op_members, seq_members):
for hooked_member, hook_address in self._check_members(var.seq_fops, var_name, op_members):
for hooked_member, hook_address in self._check_members(
var.seq_fops, var_name, op_members
):
yield var_name, hooked_member, hook_address
# newer kernels
if var.has_member("seq_ops"):
for hooked_member, hook_address in self._check_members(var.seq_ops, var_name, seq_members):
for hooked_member, hook_address in self._check_members(
var.seq_ops, var_name, seq_members
):
yield var_name, hooked_member, hook_address
# this is the most commonly hooked member by rootkits, so a force a check on it
@@ -62,13 +69,21 @@ class Check_afinfo(plugins.PluginInterface):
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
op_members = vmlinux.get_type('file_operations').members
seq_members = vmlinux.get_type('seq_operations').members
op_members = vmlinux.get_type("file_operations").members
seq_members = vmlinux.get_type("seq_operations").members
tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"])
udp = ("udp_seq_afinfo", ["udplite6_seq_afinfo", "udp6_seq_afinfo", "udplite4_seq_afinfo", "udp4_seq_afinfo"])
udp = (
"udp_seq_afinfo",
[
"udplite6_seq_afinfo",
"udp6_seq_afinfo",
"udplite4_seq_afinfo",
"udp4_seq_afinfo",
],
)
protocols = [tcp, udp]
for (struct_type, global_vars) in protocols:
@@ -79,12 +94,22 @@ class Check_afinfo(plugins.PluginInterface):
except exceptions.SymbolError:
continue
global_var = vmlinux.object(object_type = struct_type, offset = global_var.address)
global_var = vmlinux.object(
object_type=struct_type, offset=global_var.address
)
for name, member, address in self._check_afinfo(global_var_name, global_var, op_members, seq_members):
for name, member, address in self._check_afinfo(
global_var_name, global_var, op_members, seq_members
):
yield 0, (name, member, format_hints.Hex(address))
def run(self):
return renderers.TreeGrid([("Symbol Name", str), ("Member", str), ("Handler Address", format_hints.Hex)],
self._generator())
return renderers.TreeGrid(
[
("Symbol Name", str),
("Member", str),
("Handler Address", format_hints.Hex),
],
self._generator(),
)
@@ -19,13 +19,18 @@ class Check_creds(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
]
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
type_task = vmlinux.get_type("task_struct")
@@ -15,27 +15,38 @@ vollog = logging.getLogger(__name__)
class Check_idt(interfaces.plugins.PluginInterface):
""" Checks if the IDT has been altered """
"""Checks if the IDT has been altered"""
_required_framework_version = (2, 0, 0)
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
]
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(
self.context, vmlinux.name, modules
)
is_32bit = not symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name)
is_32bit = not symbols.symbol_table_is_64bit(
self.context, vmlinux.symbol_table_name
)
idt_table_size = 256
@@ -59,11 +70,13 @@ class Check_idt(interfaces.plugins.PluginInterface):
addrs = vmlinux.object_from_symbol("idt_table")
table = vmlinux.object(object_type = 'array',
offset = addrs.vol.offset,
subtype = vmlinux.get_type(idt_type),
count = idt_table_size,
absolute = True)
table = vmlinux.object(
object_type="array",
offset=addrs.vol.offset,
subtype=vmlinux.get_type(idt_type),
count=idt_table_size,
absolute=True,
)
for i in check_idxs:
ent = table[i]
@@ -86,10 +99,27 @@ class Check_idt(interfaces.plugins.PluginInterface):
idt_addr = idt_addr & address_mask
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, idt_addr)
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, idt_addr
)
yield (0, [format_hints.Hex(i), format_hints.Hex(idt_addr), module_name, symbol_name])
yield (
0,
[
format_hints.Hex(i),
format_hints.Hex(idt_addr),
module_name,
symbol_name,
],
)
def run(self):
return renderers.TreeGrid([("Index", format_hints.Hex), ("Address", format_hints.Hex), ("Module", str),
("Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Index", format_hints.Hex),
("Address", format_hints.Hex),
("Module", str),
("Symbol", str),
],
self._generator(),
)
@@ -23,13 +23,20 @@ class Check_modules(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
]
@classmethod
def get_kset_modules(self, context: interfaces.context.ContextInterface, vmlinux_name: str):
def get_kset_modules(
cls, context: interfaces.context.ContextInterface, vmlinux_name: str
):
vmlinux = context.modules[vmlinux_name]
@@ -45,12 +52,17 @@ class Check_modules(plugins.PluginInterface):
ret = {}
kobj_off = vmlinux.get_type('module_kobject').relative_child_offset('kobj')
kobj_off = vmlinux.get_type("module_kobject").relative_child_offset("kobj")
for kobj in module_kset.list.to_list(vmlinux.symbol_table_name + constants.BANG + "kobject", "entry"):
for kobj in module_kset.list.to_list(
vmlinux.symbol_table_name + constants.BANG + "kobject", "entry"
):
mod_kobj = vmlinux.object(object_type = "module_kobject", offset = kobj.vol.offset - kobj_off,
absolute = True)
mod_kobj = vmlinux.object(
object_type="module_kobject",
offset=kobj.vol.offset - kobj_off,
absolute=True,
)
mod = mod_kobj.mod
@@ -61,14 +73,18 @@ class Check_modules(plugins.PluginInterface):
return ret
def _generator(self):
kset_modules = self.get_kset_modules(self.context, self.config['kernel'])
kset_modules = self.get_kset_modules(self.context, self.config["kernel"])
lsmod_modules = set(
str(utility.array_to_string(modules.name))
for modules in lsmod.Lsmod.list_modules(self.context, self.config['kernel']))
for modules in lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
)
for mod_name in set(kset_modules.keys()).difference(lsmod_modules):
yield (0, (format_hints.Hex(kset_modules[mod_name]), str(mod_name)))
def run(self):
return renderers.TreeGrid([("Module Address", format_hints.Hex), ("Module Name", str)], self._generator())
return renderers.TreeGrid(
[("Module Address", format_hints.Hex), ("Module Name", str)],
self._generator(),
)
@@ -30,8 +30,11 @@ class Check_syscall(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
]
def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux):
@@ -63,8 +66,12 @@ class Check_syscall(plugins.PluginInterface):
accurate."""
return len(
[sym for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols if
sym.startswith("__syscall_meta__")])
[
sym
for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols
if sym.startswith("__syscall_meta__")
]
)
def _get_table_info_other(self, table_addr, ptr_sz, vmlinux):
table_size_meta = self._get_table_size_meta(vmlinux)
@@ -100,12 +107,12 @@ class Check_syscall(plugins.PluginInterface):
# if we can't find the disassemble function then bail and rely on a different method
return 0
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
data = self.context.layers.read(vmlinux.layer_name, func_addr, 6)
for (address, size, mnemonic, op_str) in md.disasm_lite(data, func_addr):
if mnemonic == 'CMP':
table_size = int(op_str.split(",")[1].strip()) & 0xffff
if mnemonic == "CMP":
table_size = int(op_str.split(",")[1].strip()) & 0xFFFF
break
return table_size
@@ -126,7 +133,7 @@ class Check_syscall(plugins.PluginInterface):
# TODO - add finding and parsing unistd.h once cached file enumeration is added
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
ptr_sz = vmlinux.get_type("pointer").size
if ptr_sz == 4:
@@ -155,10 +162,12 @@ class Check_syscall(plugins.PluginInterface):
tables.append(("32bit", ia32_info))
for (table_name, (tableaddr, tblsz)) in tables:
table = vmlinux.object(object_type = "array",
subtype = vmlinux.get_type("pointer"),
offset = tableaddr,
count = tblsz)
table = vmlinux.object(
object_type="array",
subtype=vmlinux.get_type("pointer"),
offset=tableaddr,
count=tblsz,
)
for (i, call_addr) in enumerate(table):
if not call_addr:
@@ -167,14 +176,34 @@ class Check_syscall(plugins.PluginInterface):
symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr))
if len(symbols) > 0:
sym_name = str(symbols[0].split(constants.BANG)[1]) if constants.BANG in symbols[0] else \
str(symbols[0])
sym_name = (
str(symbols[0].split(constants.BANG)[1])
if constants.BANG in symbols[0]
else str(symbols[0])
)
else:
sym_name = "UNKNOWN"
yield (0, (format_hints.Hex(tableaddr), table_name, i, format_hints.Hex(call_addr), sym_name))
yield (
0,
(
format_hints.Hex(tableaddr),
table_name,
i,
format_hints.Hex(call_addr),
sym_name,
),
)
def run(self):
return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int),
("Handler Address", format_hints.Hex), ("Handler Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Table Address", format_hints.Hex),
("Table Name", str),
("Index", int),
("Handler Address", format_hints.Hex),
("Handler Symbol", str),
],
self._generator(),
)
+46 -17
View File
@@ -22,13 +22,20 @@ class Elfs(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _generator(self, tasks):
@@ -42,20 +49,42 @@ class Elfs(plugins.PluginInterface):
name = utility.array_to_string(task.comm)
for vma in task.mm.get_mmap_iter():
hdr = proc_layer.read(vma.vm_start, 4, pad = True)
if not (hdr[0] == 0x7f and hdr[1] == 0x45 and hdr[2] == 0x4c and hdr[3] == 0x46):
hdr = proc_layer.read(vma.vm_start, 4, pad=True)
if not (
hdr[0] == 0x7F
and hdr[1] == 0x45
and hdr[2] == 0x4C
and hdr[3] == 0x46
):
continue
path = vma.get_name(self.context, task)
yield (0, (task.pid, name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), path))
yield (
0,
(
task.pid,
name,
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
path,
),
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex),
("End", format_hints.Hex), ("File Path", str)],
self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Start", format_hints.Hex),
("End", format_hints.Hex),
("File Path", str),
],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
@@ -21,18 +21,27 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)),
requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
]
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(
self.context, vmlinux.name, modules
)
try:
knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list")
@@ -46,14 +55,25 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface):
"This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt."
)
knl = vmlinux.object(object_type = "atomic_notifier_head", offset = knl_addr.vol.offset, absolute = True)
knl = vmlinux.object(
object_type="atomic_notifier_head",
offset=knl_addr.vol.offset,
absolute=True,
)
for call_back in linux.LinuxUtilities.walk_internal_list(vmlinux, "notifier_block", "next", knl.head):
for call_back in linux.LinuxUtilities.walk_internal_list(
vmlinux, "notifier_block", "next", knl.head
):
call_addr = call_back.notifier_call
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, call_addr)
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, call_addr
)
yield (0, [format_hints.Hex(call_addr), module_name, symbol_name])
def run(self):
return renderers.TreeGrid([("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], self._generator())
return renderers.TreeGrid(
[("Address", format_hints.Hex), ("Module", str), ("Symbol", str)],
self._generator(),
)
+90 -49
View File
@@ -6,7 +6,13 @@ from abc import ABC, abstractmethod
from enum import Enum
from typing import Generator, Iterator, List, Tuple
from volatility3.framework import class_subclasses, constants, contexts, interfaces, renderers
from volatility3.framework import (
class_subclasses,
constants,
contexts,
interfaces,
renderers,
)
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.framework.objects import utility
@@ -24,6 +30,7 @@ class DescStateEnum(Enum):
class ABCKmsg(ABC):
"""Kernel log buffer reader"""
LEVELS = (
"emerg", # system is unusable
"alert", # action must be taken immediately
@@ -47,27 +54,27 @@ class ABCKmsg(ABC):
"uucp", # UUCP subsystem
"cron", # clock daemon
"authpriv", # security/authorization messages (private)
"ftp" # FTP daemon
"ftp", # FTP daemon
)
def __init__(
self,
context: interfaces.context.ContextInterface,
config: interfaces.configuration.HierarchicalDict
self,
context: interfaces.context.ContextInterface,
config: interfaces.configuration.HierarchicalDict,
):
self._context = context
self._config = config
vmlinux = context.modules[self._config['kernel']]
vmlinux = context.modules[self._config["kernel"]]
self.layer_name = vmlinux.layer_name # type: ignore
symbol_table_name = vmlinux.symbol_table_name # type: ignore
self.vmlinux = contexts.Module.create(context, symbol_table_name, self.layer_name, 0) # type: ignore
self.long_unsigned_int_size = self.vmlinux.get_type('long unsigned int').size
self.long_unsigned_int_size = self.vmlinux.get_type("long unsigned int").size
@classmethod
def run_all(
cls,
context: interfaces.context.ContextInterface,
config: interfaces.configuration.HierarchicalDict
cls,
context: interfaces.context.ContextInterface,
config: interfaces.configuration.HierarchicalDict,
) -> Iterator[Tuple[str, str, str, str, str]]:
"""It calls each subclass symtab_checks() to test the required
conditions to that specific kernel implementation.
@@ -79,17 +86,24 @@ class ABCKmsg(ABC):
Yields:
kmsg records
"""
vmlinux = context.modules[config['kernel']]
vmlinux = context.modules[config["kernel"]]
kmsg_inst = None # type: ignore
for subclass in class_subclasses(cls):
if not subclass.symtab_checks(vmlinux = vmlinux):
vollog.log(constants.LOGLEVEL_VVVV,
"Kmsg implementation '%s' doesn't match this memory dump", subclass.__name__)
if not subclass.symtab_checks(vmlinux=vmlinux):
vollog.log(
constants.LOGLEVEL_VVVV,
"Kmsg implementation '%s' doesn't match this memory dump",
subclass.__name__,
)
continue
vollog.log(constants.LOGLEVEL_VVVV, "Kmsg implementation '%s' matches!", subclass.__name__)
kmsg_inst = subclass(context = context, config = config)
vollog.log(
constants.LOGLEVEL_VVVV,
"Kmsg implementation '%s' matches!",
subclass.__name__,
)
kmsg_inst = subclass(context=context, config=config)
# More than one class could be executed for an specific kernel
# version i.e. Netfilter Ingress hooks
# We expect just one implementation to be executed for an specific kernel
@@ -116,7 +130,7 @@ class ABCKmsg(ABC):
def get_string(self, addr: int, length: int) -> str:
txt = self._context.layers[self.layer_name].read(addr, length) # type: ignore
return txt.decode(encoding = 'utf8', errors = 'replace')
return txt.decode(encoding="utf8", errors="replace")
def nsec_to_sec_str(self, nsec: int) -> str:
# See kernel/printk/printk.c:print_time()
@@ -138,19 +152,24 @@ class ABCKmsg(ABC):
# In some kernel versions, it's only available if CONFIG_PRINTK_CALLER is defined.
# caller_id is a member of printk_log struct from 5.1 to the latest 5.9
# From kernels 5.10 on, it's a member of printk_info struct
if obj.has_member('caller_id'):
if obj.has_member("caller_id"):
return self.get_caller_text(obj.caller_id)
else:
return ""
def get_caller_text(self, caller_id):
caller_name = 'CPU' if caller_id & 0x80000000 else 'Task'
caller_name = "CPU" if caller_id & 0x80000000 else "Task"
caller = "%s(%u)" % (caller_name, caller_id & ~0x80000000)
return caller
def get_prefix(self, obj) -> Tuple[int, int, str, str]:
# obj could be printk_log or printk_info
return obj.facility, obj.level, self.get_timestamp_in_sec_str(obj), self.get_caller(obj)
return (
obj.facility,
obj.level,
self.get_timestamp_in_sec_str(obj),
self.get_caller(obj),
)
@classmethod
def get_level_text(cls, level: int) -> str:
@@ -185,10 +204,10 @@ class KmsgLegacy(ABCKmsg):
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return vmlinux.has_type('printk_log')
return vmlinux.has_type("printk_log")
def get_text_from_printk_log(self, msg) -> str:
msg_offset = msg.vol.offset + self.vmlinux.get_type('printk_log').size
msg_offset = msg.vol.offset + self.vmlinux.get_type("printk_log").size
return self.get_string(msg_offset, msg.text_len)
def get_log_lines(self, msg) -> Generator[str, None, None]:
@@ -199,26 +218,34 @@ class KmsgLegacy(ABCKmsg):
def get_dict_lines(self, msg) -> Generator[str, None, None]:
if msg.dict_len == 0:
return None
dict_offset = msg.vol.offset + self.vmlinux.get_type('printk_log').size + msg.text_len
dict_data = self._context.layers[self.layer_name].read(dict_offset, msg.dict_len)
for chunk in dict_data.split(b'\x00'):
dict_offset = (
msg.vol.offset + self.vmlinux.get_type("printk_log").size + msg.text_len
)
dict_data = self._context.layers[self.layer_name].read(
dict_offset, msg.dict_len
)
for chunk in dict_data.split(b"\x00"):
yield " " + chunk.decode()
def run(self) -> Iterator[Tuple[str, str, str, str, str]]:
log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name = 'log_buf')
log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name="log_buf")
if log_buf_ptr == 0:
# This is weird, let's fallback to check the static ringbuffer.
log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name = '__log_buf').vol.offset
log_buf_ptr = self.vmlinux.object_from_symbol(
symbol_name="__log_buf"
).vol.offset
if log_buf_ptr == 0:
raise ValueError("Log buffer is not available")
log_first_idx = int(self.vmlinux.object_from_symbol(symbol_name = 'log_first_idx'))
log_first_idx = int(
self.vmlinux.object_from_symbol(symbol_name="log_first_idx")
)
cur_idx = log_first_idx
end_idx = None # We don't need log_next_idx here. See below msg.len == 0
while cur_idx != end_idx:
end_idx = log_first_idx
msg_offset = log_buf_ptr + cur_idx # type: ignore
msg = self.vmlinux.object(object_type = 'printk_log', offset = msg_offset)
msg = self.vmlinux.object(object_type="printk_log", offset=msg_offset)
if msg.len == 0:
# As per kernel/printk/printk.c:
# A length == 0 for the next message indicates a wrap-around to
@@ -284,7 +311,7 @@ class KmsgFiveTen(ABCKmsg):
@classmethod
def symtab_checks(cls, vmlinux) -> bool:
return vmlinux.has_symbol('prb')
return vmlinux.has_symbol("prb")
def get_text_from_data_ring(self, text_data_ring, desc, info) -> str:
text_data_sz = text_data_ring.size_bits
@@ -327,20 +354,24 @@ class KmsgFiveTen(ABCKmsg):
def run(self) -> Iterator[Tuple[str, str, str, str, str]]:
# static struct printk_ringbuffer *prb = &printk_rb_static;
ringbuffers = self.vmlinux.object_from_symbol(symbol_name = 'prb').dereference()
ringbuffers = self.vmlinux.object_from_symbol(symbol_name="prb").dereference()
desc_ring = ringbuffers.desc_ring
text_data_ring = ringbuffers.text_data_ring
desc_count = 1 << desc_ring.count_bits
desc_arr = self.vmlinux.object(object_type = "array",
offset = desc_ring.descs,
subtype = self.vmlinux.get_type("prb_desc"),
count = desc_count)
info_arr = self.vmlinux.object(object_type = "array",
offset = desc_ring.infos,
subtype = self.vmlinux.get_type("printk_info"),
count = desc_count)
desc_arr = self.vmlinux.object(
object_type="array",
offset=desc_ring.descs,
subtype=self.vmlinux.get_type("prb_desc"),
count=desc_count,
)
info_arr = self.vmlinux.object(
object_type="array",
offset=desc_ring.infos,
subtype=self.vmlinux.get_type("printk_info"),
count=desc_count,
)
# See kernel/printk/printk_ringbuffer.h
desc_state_var_bytes_sz = self.long_unsigned_int_size
@@ -356,7 +387,10 @@ class KmsgFiveTen(ABCKmsg):
desc = desc_arr[cur_id % desc_count] # type: ignore
info = info_arr[cur_id % desc_count] # type: ignore
desc_state = DescStateEnum((desc.state_var.counter >> desc_flags_shift) & 3)
if desc_state in (DescStateEnum.desc_committed, DescStateEnum.desc_finalized):
if desc_state in (
DescStateEnum.desc_committed,
DescStateEnum.desc_finalized,
):
facility, level, timestamp, caller = self.get_prefix(info)
level_txt = self.get_level_text(level)
facility_txt = self.get_facility_text(facility)
@@ -380,18 +414,25 @@ class Kmsg(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ['Intel32', 'Intel64']),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
]
def _generator(self) -> Iterator[Tuple[int, Tuple[str, str, str, str, str]]]:
for values in ABCKmsg.run_all(context = self.context, config = self.config):
for values in ABCKmsg.run_all(context=self.context, config=self.config):
yield (0, values)
def run(self):
return renderers.TreeGrid([("facility", str),
("level", str),
("timestamp", str),
("caller", str),
("line", str)],
self._generator()) # type: ignore
return renderers.TreeGrid(
[
("facility", str),
("level", str),
("timestamp", str),
("caller", str),
("line", str),
],
self._generator(),
) # type: ignore
+14 -7
View File
@@ -25,13 +25,17 @@ class Lsmod(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def list_modules(cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str) -> Iterable[
interfaces.objects.ObjectInterface]:
def list_modules(
cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Lists all the modules in the primary layer.
Args:
@@ -46,7 +50,7 @@ class Lsmod(plugins.PluginInterface):
"""
vmlinux = context.modules[vmlinux_module_name]
modules = vmlinux.object_from_symbol(symbol_name = "modules").cast("list_head")
modules = vmlinux.object_from_symbol(symbol_name="modules").cast("list_head")
table_name = modules.vol.type_name.split(constants.BANG)[0]
@@ -55,7 +59,7 @@ class Lsmod(plugins.PluginInterface):
def _generator(self):
try:
for module in self.list_modules(self.context, self.config['kernel']):
for module in self.list_modules(self.context, self.config["kernel"]):
mod_size = module.get_init_size() + module.get_core_size()
@@ -69,4 +73,7 @@ class Lsmod(plugins.PluginInterface):
)
def run(self):
return renderers.TreeGrid([("Offset", format_hints.Hex), ("Name", str), ("Size", int)], self._generator())
return renderers.TreeGrid(
[("Offset", format_hints.Hex), ("Name", str), ("Size", int)],
self._generator(),
)
+18 -9
View File
@@ -26,14 +26,23 @@ class Lsof(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
@classmethod
@@ -77,4 +86,4 @@ class Lsof(plugins.PluginInterface):
symbol_table = self.config['kernel']
tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)]
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table))
+54 -21
View File
@@ -20,13 +20,20 @@ class Malfind(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _list_injections(self, task):
@@ -41,13 +48,18 @@ class Malfind(interfaces.plugins.PluginInterface):
for vma in task.mm.get_mmap_iter():
if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]":
data = proc_layer.read(vma.vm_start, 64, pad = True)
data = proc_layer.read(vma.vm_start, 64, pad=True)
yield vma, data
def _generator(self, tasks):
# determine if we're on a 32 or 64 bit kernel
vmlinux = self.context.modules[self.config['kernel']]
if self.context.symbol_space.get_type(vmlinux.symbol_table_name + constants.BANG + "pointer").size == 4:
vmlinux = self.context.modules[self.config["kernel"]]
if (
self.context.symbol_space.get_type(
vmlinux.symbol_table_name + constants.BANG + "pointer"
).size
== 4
):
is_32bit_arch = True
else:
is_32bit_arch = False
@@ -61,18 +73,39 @@ class Malfind(interfaces.plugins.PluginInterface):
else:
architecture = "intel64"
disasm = interfaces.renderers.Disassembly(data, vma.vm_start, architecture)
disasm = interfaces.renderers.Disassembly(
data, vma.vm_start, architecture
)
yield (0, (task.pid, process_name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end),
vma.get_protection(), format_hints.HexBytes(data), disasm))
yield (
0,
(
task.pid,
process_name,
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
vma.get_protection(),
format_hints.HexBytes(data),
disasm,
),
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex),
("End", format_hints.Hex), ("Protection", str), ("Hexdump", format_hints.HexBytes),
("Disasm", interfaces.renderers.Disassembly)],
self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Start", format_hints.Hex),
("End", format_hints.Hex),
("Protection", str),
("Hexdump", format_hints.HexBytes),
("Disasm", interfaces.renderers.Disassembly),
],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
+124 -46
View File
@@ -13,8 +13,22 @@ from volatility3.plugins.linux import pslist
vollog = logging.getLogger(__name__)
MountInfoData = namedtuple("MountInfoData", ("mnt_id", "parent_id", "st_dev", "mnt_root_path", "path_root",
"mnt_opts", "fields", "mnt_type", "devname", "sb_opts"))
MountInfoData = namedtuple(
"MountInfoData",
(
"mnt_id",
"parent_id",
"st_dev",
"mnt_root_path",
"path_root",
"mnt_opts",
"fields",
"mnt_type",
"devname",
"sb_opts",
),
)
class MountInfo(plugins.PluginInterface):
"""Lists mount points on processes mount namespaces"""
@@ -26,25 +40,35 @@ class MountInfo(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name="kernel", description="Linux kernel",
architectures=["Intel32", "Intel64"]),
requirements.PluginRequirement(name="pslist",
plugin=pslist.PsList, version=(2, 0, 0)),
requirements.ListRequirement(name="pids",
description="Filter on specific process IDs.",
element_type=int,
optional=True),
requirements.ListRequirement(name="mntns",
description="Filter results by mount namespace. "
"Otherwise, all of them are shown.",
element_type=int,
optional=True),
requirements.BooleanRequirement(name="mount-format",
description="Shows a brief summary of the mount points information "
"with similar output format to the older /proc/[pid]/mounts or the "
"user-land command 'mount -l'.",
optional=True,
default=False),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pids",
description="Filter on specific process IDs.",
element_type=int,
optional=True,
),
requirements.ListRequirement(
name="mntns",
description="Filter results by mount namespace. "
"Otherwise, all of them are shown.",
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="mount-format",
description="Shows a brief summary of the mount points information "
"with similar output format to the older /proc/[pid]/mounts or the "
"user-land command 'mount -l'.",
optional=True,
default=False,
),
]
@classmethod
@@ -79,8 +103,11 @@ class MountInfo(plugins.PluginInterface):
return path
@classmethod
def get_mountinfo(cls, mnt, task) -> Union[None, Tuple[int, int, str, str, str, List[str],
List[str], str, str, List[str]]]:
def get_mountinfo(
cls, mnt, task
) -> Union[
None, Tuple[int, int, str, str, str, List[str], List[str], str, str, List[str]]
]:
"""Extract various information about a mount point.
It mimics the Linux kernel show_mountinfo function.
"""
@@ -129,13 +156,31 @@ class MountInfo(plugins.PluginInterface):
sb_opts.append(superblock.get_flags_access())
sb_opts.extend(superblock.get_flags_opts())
return MountInfoData(mnt_id, parent_id, st_dev, mnt_root_path, path_root, mnt_opts, fields,
mnt_type, devname, sb_opts)
return MountInfoData(
mnt_id,
parent_id,
st_dev,
mnt_root_path,
path_root,
mnt_opts,
fields,
mnt_type,
devname,
sb_opts,
)
def _get_tasks_mountpoints(self, tasks: Iterable[interfaces.objects.ObjectInterface], per_namespace: bool):
def _get_tasks_mountpoints(
self, tasks: Iterable[interfaces.objects.ObjectInterface], per_namespace: bool
):
seen_namespaces = set()
for task in tasks:
if not (task and task.fs and task.fs.root and task.nsproxy and task.nsproxy.mnt_ns):
if not (
task
and task.fs
and task.fs.root
and task.nsproxy
and task.nsproxy.mnt_ns
):
# This task doesn't have all the information required
continue
@@ -152,11 +197,12 @@ class MountInfo(plugins.PluginInterface):
yield task, mount, mnt_ns_id
def _generator(
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
mnt_ns_ids: List[int],
mount_format: bool,
per_namespace: bool) -> Iterable[Tuple[int, Tuple]]:
self,
tasks: Iterable[interfaces.objects.ObjectInterface],
mnt_ns_ids: List[int],
mount_format: bool,
per_namespace: bool,
) -> Iterable[Tuple[int, Tuple]]:
for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(tasks, per_namespace):
if mnt_ns_ids and mnt_ns_id not in mnt_ns_ids:
@@ -172,15 +218,29 @@ class MountInfo(plugins.PluginInterface):
all_opts.update(mnt_info.sb_opts)
all_opts_str = ",".join(all_opts)
extra_fields_values = [mnt_info.devname, mnt_info.path_root, mnt_info.mnt_type, all_opts_str]
extra_fields_values = [
mnt_info.devname,
mnt_info.path_root,
mnt_info.mnt_type,
all_opts_str,
]
else:
mnt_opts_str = ",".join(mnt_info.mnt_opts)
fields_str = " ".join(mnt_info.fields)
sb_opts_str = ",".join(mnt_info.sb_opts)
extra_fields_values = [mnt_info.mnt_id, mnt_info.parent_id, mnt_info.st_dev, mnt_info.mnt_root_path,
mnt_info.path_root, mnt_opts_str, fields_str, mnt_info.mnt_type,
mnt_info.devname, sb_opts_str]
extra_fields_values = [
mnt_info.mnt_id,
mnt_info.parent_id,
mnt_info.st_dev,
mnt_info.mnt_root_path,
mnt_info.path_root,
mnt_opts_str,
fields_str,
mnt_info.mnt_type,
mnt_info.devname,
sb_opts_str,
]
fields_values = [mnt_ns_id]
if not per_namespace:
@@ -190,12 +250,14 @@ class MountInfo(plugins.PluginInterface):
yield (0, fields_values)
def run(self):
pids = self.config.get('pids')
mount_ns_ids = self.config.get('mntns')
mount_format = self.config.get('mount-format')
pids = self.config.get("pids")
mount_ns_ids = self.config.get("mntns")
mount_format = self.config.get("mount-format")
pid_filter = pslist.PsList.create_pid_filter(pids)
tasks = pslist.PsList.list_tasks(self.context, self.config['kernel'], filter_func=pid_filter)
tasks = pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=pid_filter
)
columns = [("MNT_NS_ID", int)]
# The PID column does not make sense when a PID filter is not specified. In that case, the default behavior is
@@ -206,14 +268,30 @@ class MountInfo(plugins.PluginInterface):
else:
per_namespace = True
if self.config.get('mount-format'):
extra_columns = [("DEVNAME", str), ("PATH", str), ("FSTYPE", str), ("MNT_OPTS", str)]
if self.config.get("mount-format"):
extra_columns = [
("DEVNAME", str),
("PATH", str),
("FSTYPE", str),
("MNT_OPTS", str),
]
else:
# /proc/[pid]/mountinfo output format
extra_columns = [("MOUNT ID", int), ("PARENT_ID", int), ("MAJOR:MINOR", str), ("ROOT", str),
("MOUNT_POINT", str), ("MOUNT_OPTIONS", str), ("FIELDS", str), ("FSTYPE", str),
("MOUNT_SRC", str), ("SB_OPTIONS", str)]
extra_columns = [
("MOUNT ID", int),
("PARENT_ID", int),
("MAJOR:MINOR", str),
("ROOT", str),
("MOUNT_POINT", str),
("MOUNT_OPTIONS", str),
("FIELDS", str),
("FSTYPE", str),
("MOUNT_SRC", str),
("SB_OPTIONS", str),
]
columns.extend(extra_columns)
return renderers.TreeGrid(columns, self._generator(tasks, mount_ns_ids, mount_format, per_namespace))
return renderers.TreeGrid(
columns, self._generator(tasks, mount_ns_ids, mount_format, per_namespace)
)
+49 -18
View File
@@ -21,13 +21,20 @@ class Maps(plugins.PluginInterface):
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _generator(self, tasks):
@@ -54,17 +61,41 @@ class Maps(plugins.PluginInterface):
path = vma.get_name(self.context, task)
yield (0, (task.pid, name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), flags,
format_hints.Hex(page_offset), major, minor, inode, path))
yield (
0,
(
task.pid,
name,
format_hints.Hex(vma.vm_start),
format_hints.Hex(vma.vm_end),
flags,
format_hints.Hex(page_offset),
major,
minor,
inode,
path,
),
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid([("PID", int), ("Process", str),
("Start", format_hints.Hex), ("End", format_hints.Hex), ("Flags", str),
("PgOff", format_hints.Hex), ("Major", int), ("Minor", int), ("Inode", int),
("File Path", str)],
self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Start", format_hints.Hex),
("End", format_hints.Hex),
("Flags", str),
("PgOff", format_hints.Hex),
("Major", int),
("Minor", int),
("Inode", int),
("File Path", str),
],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
+29 -18
View File
@@ -12,7 +12,7 @@ from volatility3.plugins.linux import pslist
class PsAux(plugins.PluginInterface):
""" Lists processes with their command line arguments """
"""Lists processes with their command line arguments"""
_required_framework_version = (2, 0, 0)
@@ -20,17 +20,25 @@ class PsAux(plugins.PluginInterface):
def get_requirements(cls):
# Since we're calling the plugin, make sure we have the plugin's requirements
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _get_command_line_args(self, task: interfaces.objects.ObjectInterface,
name: str) -> Optional[str]:
def _get_command_line_args(
self, task: interfaces.objects.ObjectInterface, name: str
) -> Optional[str]:
"""
Reads the command line arguments of a process
These are stored on the userland stack
@@ -69,7 +77,7 @@ class PsAux(plugins.PluginInterface):
return renderers.UnreadableValue()
# the arguments are null byte terminated, replace the nulls with spaces
s = argv.decode().split('\x00')
s = argv.decode().split("\x00")
args = " ".join(s)
else:
# kernel thread
@@ -84,7 +92,7 @@ class PsAux(plugins.PluginInterface):
return args
def _generator(self, tasks):
""" Generates a listing of processes along with command line arguments """
"""Generates a listing of processes along with command line arguments"""
# walk the process list and report the arguments
for task in tasks:
@@ -102,10 +110,13 @@ class PsAux(plugins.PluginInterface):
yield (0, (pid, ppid, name, args))
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str), ("ARGS", str)],
self._generator(
pslist.PsList.list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[("PID", int), ("PPID", int), ("COMM", str), ("ARGS", str)],
self._generator(
pslist.PsList.list_tasks(
self.context, self.config["kernel"], filter_func=filter_func
)
),
)
+53 -36
View File
@@ -19,20 +19,29 @@ class PsList(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True),
requirements.BooleanRequirement(name="threads",
description="Include user threads",
optional=True,
default=False),
requirements.BooleanRequirement(name="decorate_comm",
description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets",
optional=True,
default=False),
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
requirements.BooleanRequirement(
name="threads",
description="Include user threads",
optional=True,
default=False,
),
requirements.BooleanRequirement(
name="decorate_comm",
description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets",
optional=True,
default=False,
),
]
@classmethod
@@ -58,9 +67,8 @@ class PsList(interfaces.plugins.PluginInterface):
return lambda _: False
def _get_task_fields(
self,
task: interfaces.objects.ObjectInterface,
decorate_comm: bool = False) -> Tuple[int, int, int, str]:
self, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, str]:
"""Extract the fields needed for the final output
Args:
@@ -86,10 +94,11 @@ class PsList(interfaces.plugins.PluginInterface):
return task_fields
def _generator(
self,
pid_filter: Callable[[Any], bool],
include_threads: bool = False,
decorate_comm: bool = False):
self,
pid_filter: Callable[[Any], bool],
include_threads: bool = False,
decorate_comm: bool = False,
):
"""Generates the tasks list.
Args:
@@ -104,20 +113,20 @@ class PsList(interfaces.plugins.PluginInterface):
Yields:
Each rows
"""
for task in self.list_tasks(self.context,
self.config['kernel'],
pid_filter,
include_threads):
for task in self.list_tasks(
self.context, self.config["kernel"], pid_filter, include_threads
):
row = self._get_task_fields(task, decorate_comm)
yield (0, row)
@classmethod
def list_tasks(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False,
include_threads: bool = False) -> Iterable[interfaces.objects.ObjectInterface]:
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False,
include_threads: bool = False,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""Lists all the tasks in the primary layer.
Args:
@@ -130,7 +139,7 @@ class PsList(interfaces.plugins.PluginInterface):
"""
vmlinux = context.modules[vmlinux_module_name]
init_task = vmlinux.object_from_symbol(symbol_name = "init_task")
init_task = vmlinux.object_from_symbol(symbol_name="init_task")
# Note that the init_task itself is not yielded, since "ps" also never shows it.
for task in init_task.tasks:
@@ -143,10 +152,18 @@ class PsList(interfaces.plugins.PluginInterface):
yield from task.get_threads()
def run(self):
pids = self.config.get('pid')
include_threads = self.config.get('threads')
decorate_comm = self.config.get('decorate_comm')
pids = self.config.get("pid")
include_threads = self.config.get("threads")
decorate_comm = self.config.get("decorate_comm")
filter_func = self.create_pid_filter(pids)
columns = [("OFFSET (V)", format_hints.Hex), ("PID", int), ("TID", int), ("PPID", int), ("COMM", str)]
return renderers.TreeGrid(columns, self._generator(filter_func, include_threads, decorate_comm))
columns = [
("OFFSET (V)", format_hints.Hex),
("PID", int),
("TID", int),
("PPID", int),
("COMM", str),
]
return renderers.TreeGrid(
columns, self._generator(filter_func, include_threads, decorate_comm)
)
@@ -39,10 +39,8 @@ class PsTree(pslist.PsList):
self._levels[pid] = level
def _generator(
self,
pid_filter,
include_threads: bool = False,
decorate_com: bool = False):
self, pid_filter, include_threads: bool = False, decorate_com: bool = False
):
"""Generates the tasks hierarchy tree.
Args:
@@ -57,11 +55,13 @@ class PsTree(pslist.PsList):
Yields:
Each rows
"""
vmlinux = self.context.modules[self.config['kernel']]
for proc in self.list_tasks(self.context,
vmlinux.name,
filter_func=pid_filter,
include_threads=include_threads):
vmlinux = self.context.modules[self.config["kernel"]]
for proc in self.list_tasks(
self.context,
vmlinux.name,
filter_func=pid_filter,
include_threads=include_threads,
):
self._tasks[proc.pid] = proc
# Build the child/level maps
@@ -24,18 +24,27 @@ class tty_check(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)),
requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0)
),
]
def _generator(self):
vmlinux = self.context.modules[self.config['kernel']]
vmlinux = self.context.modules[self.config["kernel"]]
modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules)
handlers = linux.LinuxUtilities.generate_kernel_handler_info(
self.context, vmlinux.name, modules
)
try:
tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head")
@@ -49,13 +58,17 @@ class tty_check(plugins.PluginInterface):
"This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt."
)
for tty in tty_drivers.to_list(vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers"):
for tty in tty_drivers.to_list(
vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers"
):
try:
ttys = utility.array_of_pointers(tty.ttys.dereference(),
count = tty.num,
subtype = vmlinux.symbol_table_name + constants.BANG + "tty_struct",
context = self.context)
ttys = utility.array_of_pointers(
tty.ttys.dereference(),
count=tty.num,
subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct",
context=self.context,
)
except exceptions.PagedInvalidAddressException:
continue
@@ -68,10 +81,19 @@ class tty_check(plugins.PluginInterface):
recv_buf = tty_dev.ldisc.ops.receive_buf
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, recv_buf)
module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(
vmlinux, handlers, recv_buf
)
yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name))
def run(self):
return renderers.TreeGrid([("Name", str), ("Address", format_hints.Hex), ("Module", str), ("Symbol", str)],
self._generator())
return renderers.TreeGrid(
[
("Name", str),
("Address", format_hints.Hex),
("Module", str),
("Symbol", str),
],
self._generator(),
)
+75 -41
View File
@@ -25,18 +25,27 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _generator(self, tasks):
darwin = self.context.modules[self.config['kernel']]
is_32bit = not symbols.symbol_table_is_64bit(self.context, darwin.symbol_table_name)
darwin = self.context.modules[self.config["kernel"]]
is_32bit = not symbols.symbol_table_is_64bit(
self.context, darwin.symbol_table_name
)
if is_32bit:
pack_format = "I"
bash_json_file = "bash32"
@@ -44,10 +53,13 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
pack_format = "Q"
bash_json_file = "bash64"
bash_table_name = BashIntermedSymbols.create(self.context, self.config_path, "linux", bash_json_file)
bash_table_name = BashIntermedSymbols.create(
self.context, self.config_path, "linux", bash_json_file
)
ts_offset = self.context.symbol_space.get_type(bash_table_name + constants.BANG +
"hist_entry").relative_child_offset("timestamp")
ts_offset = self.context.symbol_space.get_type(
bash_table_name + constants.BANG + "hist_entry"
).relative_child_offset("timestamp")
for task in tasks:
task_name = utility.array_to_string(task.p_comm)
@@ -63,49 +75,71 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
bang_addrs = []
# find '#' values on the heap
for address in proc_layer.scan(self.context,
scanners.BytesScanner(b"#"),
sections = task.get_process_memory_sections(self.context,
self.config['kernel'],
rw_no_file = True)):
for address in proc_layer.scan(
self.context,
scanners.BytesScanner(b"#"),
sections=task.get_process_memory_sections(
self.context, self.config["kernel"], rw_no_file=True
),
):
bang_addrs.append(struct.pack(pack_format, address))
history_entries = []
for address, _ in proc_layer.scan(self.context,
scanners.MultiStringScanner(bang_addrs),
sections = task.get_process_memory_sections(self.context,
self.config['kernel'],
rw_no_file = True)):
hist = self.context.object(bash_table_name + constants.BANG + "hist_entry",
offset = address - ts_offset,
layer_name = proc_layer_name)
for address, _ in proc_layer.scan(
self.context,
scanners.MultiStringScanner(bang_addrs),
sections=task.get_process_memory_sections(
self.context, self.config["kernel"], rw_no_file=True
),
):
hist = self.context.object(
bash_table_name + constants.BANG + "hist_entry",
offset=address - ts_offset,
layer_name=proc_layer_name,
)
if hist.is_valid():
history_entries.append(hist)
for hist in sorted(history_entries, key = lambda x: x.get_time_as_integer()):
yield (0, (int(task.p_pid), task_name, hist.get_time_object(), hist.get_command()))
for hist in sorted(history_entries, key=lambda x: x.get_time_as_integer()):
yield (
0,
(
int(task.p_pid),
task_name,
hist.get_time_object(),
hist.get_command(),
),
)
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0]))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
list_tasks = pslist.PsList.get_list_tasks(
self.config.get("pslist_method", pslist.PsList.pslist_methods[0])
)
return renderers.TreeGrid([("PID", int), ("Process", str), ("CommandTime", datetime.datetime),
("Command", str)],
self._generator(
list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("CommandTime", datetime.datetime),
("Command", str),
],
self._generator(
list_tasks(self.context, self.config["kernel"], filter_func=filter_func)
),
)
def generate_timeline(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0]))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
list_tasks = pslist.PsList.get_list_tasks(
self.config.get("pslist_method", pslist.PsList.pslist_methods[0])
)
for row in self._generator(
list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)):
list_tasks(self.context, self.config["kernel"], filter_func=filter_func)
):
_depth, row_data = row
description = f"{row_data[0]} ({row_data[1]}): \"{row_data[3]}\""
description = f'{row_data[0]} ({row_data[1]}): "{row_data[3]}"'
yield (description, timeliner.TimeLinerType.CREATED, row_data[2])
@@ -23,20 +23,29 @@ class Check_syscall(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
]
def _generator(self):
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel'])
mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods)
handlers = mac.MacUtilities.generate_kernel_handler_info(
self.context, kernel.layer_name, kernel, mods
)
table = kernel.object_from_symbol(symbol_name = "sysent")
table = kernel.object_from_symbol(symbol_name="sysent")
for (i, ent) in enumerate(table):
try:
@@ -47,13 +56,31 @@ class Check_syscall(plugins.PluginInterface):
if not call_addr or call_addr == 0:
continue
module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers,
call_addr, self.config['kernel'])
module_name, symbol_name = mac.MacUtilities.lookup_module_address(
self.context, handlers, call_addr, self.config["kernel"]
)
yield (0, (format_hints.Hex(table.vol.offset), "SysCall", i, format_hints.Hex(call_addr), module_name,
symbol_name))
yield (
0,
(
format_hints.Hex(table.vol.offset),
"SysCall",
i,
format_hints.Hex(call_addr),
module_name,
symbol_name,
),
)
def run(self):
return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int),
("Handler Address", format_hints.Hex), ("Handler Module", str),
("Handler Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Table Address", format_hints.Hex),
("Table Name", str),
("Index", int),
("Handler Address", format_hints.Hex),
("Handler Module", str),
("Handler Symbol", str),
],
self._generator(),
)
@@ -25,10 +25,17 @@ class Check_sysctl(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
]
def _parse_global_variable_sysctls(self, kernel, name):
@@ -43,7 +50,7 @@ class Check_sysctl(plugins.PluginInterface):
var_name = known_sysctls[name]
try:
var_array = kernel.object_from_symbol(symbol_name = var_name)
var_array = kernel.object_from_symbol(symbol_name=var_name)
except exceptions.SymbolError:
var_array = None
@@ -52,7 +59,7 @@ class Check_sysctl(plugins.PluginInterface):
return var_str
def _process_sysctl_list(self, kernel, sysctl_list, recursive = 0):
def _process_sysctl_list(self, kernel, sysctl_list, recursive=0):
if type(sysctl_list) == volatility3.framework.objects.Pointer:
sysctl_list = sysctl_list.dereference().cast("sysctl_oid_list")
@@ -84,20 +91,22 @@ class Check_sysctl(plugins.PluginInterface):
if arg1 == 0 or arg1_ptr == 0:
val = self._parse_global_variable_sysctls(kernel, name)
elif ctltype == 'CTLTYPE_NODE':
elif ctltype == "CTLTYPE_NODE":
if sysctl.oid_handler == 0:
for info in self._process_sysctl_list(kernel, sysctl.oid_arg1, recursive = 1):
for info in self._process_sysctl_list(
kernel, sysctl.oid_arg1, recursive=1
):
yield info
val = "Node"
elif ctltype in ['CTLTYPE_INT', 'CTLTYPE_QUAD', 'CTLTYPE_OPAQUE']:
elif ctltype in ["CTLTYPE_INT", "CTLTYPE_QUAD", "CTLTYPE_OPAQUE"]:
try:
val = str(arg1.dereference().cast("int"))
except exceptions.InvalidAddressException:
val = "-1"
elif ctltype == 'CTLTYPE_STRING':
elif ctltype == "CTLTYPE_STRING":
try:
val = utility.pointer_to_string(sysctl.oid_arg1, 64)
except exceptions.InvalidAddressException:
@@ -113,13 +122,15 @@ class Check_sysctl(plugins.PluginInterface):
break
def _generator(self):
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel'])
mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods)
handlers = mac.MacUtilities.generate_kernel_handler_info(
self.context, kernel.layer_name, kernel, mods
)
sysctl_list = kernel.object_from_symbol(symbol_name = "sysctl__children")
sysctl_list = kernel.object_from_symbol(symbol_name="sysctl__children")
for sysctl, name, val in self._process_sysctl_list(kernel, sysctl_list):
try:
@@ -127,13 +138,33 @@ class Check_sysctl(plugins.PluginInterface):
except exceptions.InvalidAddressException:
continue
module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, check_addr,
self.config['kernel'])
module_name, symbol_name = mac.MacUtilities.lookup_module_address(
self.context, handlers, check_addr, self.config["kernel"]
)
yield (0, (name, sysctl.oid_number, sysctl.get_perms(), format_hints.Hex(check_addr), val, module_name,
symbol_name))
yield (
0,
(
name,
sysctl.oid_number,
sysctl.get_perms(),
format_hints.Hex(check_addr),
val,
module_name,
symbol_name,
),
)
def run(self):
return renderers.TreeGrid([("Name", str), ("Number", int), ("Perms", str),
("Handler Address", format_hints.Hex), ("Value", str), ("Handler Module", str),
("Handler Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Name", str),
("Number", int),
("Perms", str),
("Handler Address", format_hints.Hex),
("Value", str),
("Handler Module", str),
("Handler Symbol", str),
],
self._generator(),
)
@@ -24,20 +24,29 @@ class Check_trap_table(plugins.PluginInterface):
@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)),
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 0, 0)
),
]
def _generator(self):
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel'])
mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods)
handlers = mac.MacUtilities.generate_kernel_handler_info(
self.context, kernel.layer_name, kernel, mods
)
table = kernel.object_from_symbol(symbol_name = "mach_trap_table")
table = kernel.object_from_symbol(symbol_name="mach_trap_table")
for i, ent in enumerate(table):
try:
@@ -48,13 +57,31 @@ class Check_trap_table(plugins.PluginInterface):
if not call_addr or call_addr == 0:
continue
module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, call_addr,
self.config['kernel'])
module_name, symbol_name = mac.MacUtilities.lookup_module_address(
self.context, handlers, call_addr, self.config["kernel"]
)
yield (0, (format_hints.Hex(table.vol.offset), "TrapTable", i, format_hints.Hex(call_addr), module_name,
symbol_name))
yield (
0,
(
format_hints.Hex(table.vol.offset),
"TrapTable",
i,
format_hints.Hex(call_addr),
module_name,
symbol_name,
),
)
def run(self):
return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int),
("Handler Address", format_hints.Hex), ("Handler Module", str),
("Handler Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Table Address", format_hints.Hex),
("Table Name", str),
("Index", int),
("Handler Address", format_hints.Hex),
("Handler Module", str),
("Handler Symbol", str),
],
self._generator(),
)
+20 -8
View File
@@ -16,18 +16,23 @@ class Ifconfig(plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 0, 0)
),
]
def _generator(self):
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
try:
list_head = kernel.object_from_symbol(symbol_name = "ifnet_head")
list_head = kernel.object_from_symbol(symbol_name="ifnet_head")
except exceptions.SymbolError:
list_head = kernel.object_from_symbol(symbol_name = "dlil_ifnet_head")
list_head = kernel.object_from_symbol(symbol_name="dlil_ifnet_head")
for ifnet in mac.MacUtilities.walk_tailq(list_head, "if_link"):
name = utility.pointer_to_string(ifnet.if_name, 32)
@@ -46,5 +51,12 @@ class Ifconfig(plugins.PluginInterface):
yield (0, (f"{name}{unit}", ip, mac_addr, prom))
def run(self):
return renderers.TreeGrid([("Interface", str), ("IP Address", str), ("Mac Address", str),
("Promiscuous", bool)], self._generator())
return renderers.TreeGrid(
[
("Interface", str),
("IP Address", str),
("Mac Address", str),
("Promiscuous", bool),
],
self._generator(),
)
@@ -11,33 +11,44 @@ from volatility3.plugins.mac import lsmod, kauth_scopes
class Kauth_listeners(interfaces.plugins.PluginInterface):
""" Lists kauth listeners and their status """
"""Lists kauth listeners and their status"""
_required_framework_version = (2, 0, 0)
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 1, 0)),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)),
requirements.PluginRequirement(name = 'kauth_scopes',
plugin = kauth_scopes.Kauth_scopes,
version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 1, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
requirements.PluginRequirement(
name="kauth_scopes", plugin=kauth_scopes.Kauth_scopes, version=(2, 0, 0)
),
]
def _generator(self):
"""
Enumerates the listeners for each kauth scope
"""
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel'])
mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods)
handlers = mac.MacUtilities.generate_kernel_handler_info(
self.context, kernel.layer_name, kernel, mods
)
for scope in kauth_scopes.Kauth_scopes.list_kauth_scopes(self.context, self.config['kernel']):
for scope in kauth_scopes.Kauth_scopes.list_kauth_scopes(
self.context, self.config["kernel"]
):
scope_name = utility.pointer_to_string(scope.ks_identifier, 128)
@@ -46,12 +57,29 @@ class Kauth_listeners(interfaces.plugins.PluginInterface):
if callback == 0:
continue
module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, callback,
self.config['kernel'])
module_name, symbol_name = mac.MacUtilities.lookup_module_address(
self.context, handlers, callback, self.config["kernel"]
)
yield (0, (scope_name, format_hints.Hex(listener.kll_idata), format_hints.Hex(callback), module_name,
symbol_name))
yield (
0,
(
scope_name,
format_hints.Hex(listener.kll_idata),
format_hints.Hex(callback),
module_name,
symbol_name,
),
)
def run(self):
return renderers.TreeGrid([("Name", str), ("IData", format_hints.Hex), ("Callback Address", format_hints.Hex),
("Module", str), ("Symbol", str)], self._generator())
return renderers.TreeGrid(
[
("Name", str),
("IData", format_hints.Hex),
("Callback Address", format_hints.Hex),
("Module", str),
("Symbol", str),
],
self._generator(),
)
@@ -15,7 +15,7 @@ vollog = logging.getLogger(__name__)
class Kauth_scopes(interfaces.plugins.PluginInterface):
""" Lists kauth scopes and their status """
"""Lists kauth scopes and their status"""
_version = (2, 0, 0)
_required_framework_version = (2, 0, 0)
@@ -23,18 +23,26 @@ class Kauth_scopes(interfaces.plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 1, 0)),
requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0))
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 1, 0)
),
requirements.PluginRequirement(
name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0)
),
]
@classmethod
def list_kauth_scopes(cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False) -> \
Iterable[interfaces.objects.ObjectInterface]:
def list_kauth_scopes(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False,
) -> Iterable[interfaces.objects.ObjectInterface]:
"""
Enumerates the registered kauth scopes and yields each object
Uses smear-safe enumeration API
@@ -48,27 +56,47 @@ class Kauth_scopes(interfaces.plugins.PluginInterface):
yield scope
def _generator(self):
kernel = self.context.modules[self.config['kernel']]
kernel = self.context.modules[self.config["kernel"]]
mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel'])
mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"])
handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods)
handlers = mac.MacUtilities.generate_kernel_handler_info(
self.context, kernel.layer_name, kernel, mods
)
for scope in self.list_kauth_scopes(self.context, self.config['kernel']):
for scope in self.list_kauth_scopes(self.context, self.config["kernel"]):
callback = scope.ks_callback
if callback == 0:
continue
module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, callback,
self.config['kernel'])
module_name, symbol_name = mac.MacUtilities.lookup_module_address(
self.context, handlers, callback, self.config["kernel"]
)
identifier = utility.pointer_to_string(scope.ks_identifier, 128)
yield (0, (identifier, format_hints.Hex(scope.ks_idata), len([l for l in scope.get_listeners()]),
format_hints.Hex(callback), module_name, symbol_name))
yield (
0,
(
identifier,
format_hints.Hex(scope.ks_idata),
len([l for l in scope.get_listeners()]),
format_hints.Hex(callback),
module_name,
symbol_name,
),
)
def run(self):
return renderers.TreeGrid([("Name", str), ("IData", format_hints.Hex), ("Listeners", int),
("Callback Address", format_hints.Hex), ("Module", str), ("Symbol", str)],
self._generator())
return renderers.TreeGrid(
[
("Name", str),
("IData", format_hints.Hex),
("Listeners", int),
("Callback Address", format_hints.Hex),
("Module", str),
("Symbol", str),
],
self._generator(),
)
+75 -33
View File
@@ -12,7 +12,7 @@ from volatility3.plugins.mac import pslist
class Kevents(interfaces.plugins.PluginInterface):
""" Lists event handlers registered by processes """
"""Lists event handlers registered by processes"""
_required_framework_version = (2, 0, 0)
_version = (1, 0, 0)
@@ -28,34 +28,61 @@ class Kevents(interfaces.plugins.PluginInterface):
8: "EVFILT_MACHPORT",
9: "EVFILT_FS",
10: "EVFILT_USER",
12: "EVFILT_VM"
12: "EVFILT_VM",
}
vnode_filters = [("NOTE_DELETE", 1), ("NOTE_WRITE", 2), ("NOTE_EXTEND", 4), ("NOTE_ATTRIB", 8), ("NOTE_LINK", 0x10),
("NOTE_RENAME", 0x20), ("NOTE_REVOKE", 0x40)]
vnode_filters = [
("NOTE_DELETE", 1),
("NOTE_WRITE", 2),
("NOTE_EXTEND", 4),
("NOTE_ATTRIB", 8),
("NOTE_LINK", 0x10),
("NOTE_RENAME", 0x20),
("NOTE_REVOKE", 0x40),
]
proc_filters = [("NOTE_EXIT", 0x80000000), ("NOTE_EXITSTATUS", 0x04000000), ("NOTE_FORK", 0x40000000),
("NOTE_EXEC", 0x20000000), ("NOTE_SIGNAL", 0x08000000), ("NOTE_REAP", 0x10000000)]
proc_filters = [
("NOTE_EXIT", 0x80000000),
("NOTE_EXITSTATUS", 0x04000000),
("NOTE_FORK", 0x40000000),
("NOTE_EXEC", 0x20000000),
("NOTE_SIGNAL", 0x08000000),
("NOTE_REAP", 0x10000000),
]
timer_filters = [("NOTE_SECONDS", 1), ("NOTE_USECONDS", 2), ("NOTE_NSECONDS", 4), ("NOTE_ABSOLUTE", 8)]
timer_filters = [
("NOTE_SECONDS", 1),
("NOTE_USECONDS", 2),
("NOTE_NSECONDS", 4),
("NOTE_ABSOLUTE", 8),
]
all_filters = {
4: vnode_filters, # EVFILT_VNODE
5: proc_filters, # EVFILT_PROC
7: timer_filters # EVFILT_TIMER
7: timer_filters, # EVFILT_TIMER
}
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 2, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 2, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _parse_flags(self, filter_index, filter_flags):
@@ -81,10 +108,12 @@ class Kevents(interfaces.plugins.PluginInterface):
klist_array_pointer = getattr(fdp, array_pointer_member)
array_size = getattr(fdp, array_size_member)
klist_array = kernel.object(object_type = "array",
offset = klist_array_pointer,
count = array_size + 1,
subtype = kernel.get_type("klist"))
klist_array = kernel.object(
object_type="array",
offset=klist_array_pointer,
count=array_size + 1,
subtype=kernel.get_type("klist"),
)
except exceptions.InvalidAddressException:
return
@@ -117,13 +146,18 @@ class Kevents(interfaces.plugins.PluginInterface):
yield kn
@classmethod
def list_kernel_events(cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False) -> \
Iterable[Tuple[interfaces.objects.ObjectInterface,
interfaces.objects.ObjectInterface,
interfaces.objects.ObjectInterface]]:
def list_kernel_events(
cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
filter_func: Callable[[int], bool] = lambda _: False,
) -> Iterable[
Tuple[
interfaces.objects.ObjectInterface,
interfaces.objects.ObjectInterface,
interfaces.objects.ObjectInterface,
]
]:
"""
Returns the kernel event filters registered
@@ -145,11 +179,11 @@ class Kevents(interfaces.plugins.PluginInterface):
yield task_name, pid, kn
def _generator(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
for task_name, pid, kn in self.list_kernel_events(self.context,
self.config['kernel'],
filter_func = filter_func):
for task_name, pid, kn in self.list_kernel_events(
self.context, self.config["kernel"], filter_func=filter_func
):
filter_index = kn.kn_kevent.filter * -1
if filter_index in self.event_types:
@@ -167,5 +201,13 @@ class Kevents(interfaces.plugins.PluginInterface):
yield (0, (pid, task_name, ident, filter_name, context))
def run(self):
return renderers.TreeGrid([("PID", int), ("Process", str), ("Ident", int), ("Filter", str), ("Context", str)],
self._generator())
return renderers.TreeGrid(
[
("PID", int),
("Process", str),
("Ident", int),
("Filter", str),
("Context", str),
],
self._generator(),
)
+25 -18
View File
@@ -23,9 +23,14 @@ class List_Files(plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.PluginRequirement(name = 'mount', plugin = mount.Mount, version = (2, 0, 0)),
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="mount", plugin=mount.Mount, version=(2, 0, 0)
),
]
@classmethod
@@ -50,8 +55,9 @@ class List_Files(plugins.PluginInterface):
except exceptions.InvalidAddressException:
return None
if parent and not context.layers[vnode.vol.native_layer_name].is_valid(parent.vol.offset,
parent.vol.size):
if parent and not context.layers[vnode.vol.native_layer_name].is_valid(
parent.vol.offset, parent.vol.size
):
return None
return parent
@@ -65,8 +71,9 @@ class List_Files(plugins.PluginInterface):
and holds its name, parent address, and object
"""
if not context.layers[vnode.vol.native_layer_name].is_valid(vnode.vol.offset,
vnode.vol.size):
if not context.layers[vnode.vol.native_layer_name].is_valid(
vnode.vol.offset, vnode.vol.size
):
return False
key = vnode.vol.offset
@@ -104,7 +111,7 @@ class List_Files(plugins.PluginInterface):
if not cls._add_vnode(context, vnode, loop_vnodes):
break
added = True
parent = cls._get_parent(context, vnode)
@@ -127,10 +134,9 @@ class List_Files(plugins.PluginInterface):
cls._walk_vnode(context, vnode, loop_vnodes)
@classmethod
def _walk_mounts(cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str) -> \
Iterable[interfaces.objects.ObjectInterface]:
def _walk_mounts(
cls, context: interfaces.context.ContextInterface, kernel_module_name: str
) -> Iterable[interfaces.objects.ObjectInterface]:
loop_vnodes = {}
@@ -177,10 +183,9 @@ class List_Files(plugins.PluginInterface):
return path
@classmethod
def list_files(cls,
context: interfaces.context.ContextInterface,
kernel_module_name: str) -> \
Iterable[interfaces.objects.ObjectInterface]:
def list_files(
cls, context: interfaces.context.ContextInterface, kernel_module_name: str
) -> Iterable[interfaces.objects.ObjectInterface]:
vnodes = cls._walk_mounts(context, kernel_module_name)
@@ -190,9 +195,11 @@ class List_Files(plugins.PluginInterface):
yield vnode, full_path
def _generator(self):
for vnode, full_path in self.list_files(self.context, self.config['kernel']):
for vnode, full_path in self.list_files(self.context, self.config["kernel"]):
yield (0, (format_hints.Hex(vnode.vol.offset), full_path))
def run(self):
return renderers.TreeGrid([("Address", format_hints.Hex), ("File Path", str)], self._generator())
return renderers.TreeGrid(
[("Address", format_hints.Hex), ("File Path", str)], self._generator()
)
+18 -11
View File
@@ -22,12 +22,17 @@ class Lsmod(plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
]
@classmethod
def list_modules(cls, context: interfaces.context.ContextInterface, darwin_module_name: str):
def list_modules(
cls, context: interfaces.context.ContextInterface, darwin_module_name: str
):
"""Lists all the modules in the primary layer.
Args:
@@ -41,25 +46,23 @@ class Lsmod(plugins.PluginInterface):
kernel = context.modules[darwin_module_name]
kernel_layer = context.layers[kernel.layer_name]
kmod_ptr = kernel.object_from_symbol(symbol_name = "kmod")
kmod_ptr = kernel.object_from_symbol(symbol_name="kmod")
try:
kmod = kmod_ptr.dereference().cast("kmod_info")
except exceptions.InvalidAddressException:
return []
return # Generation finished
yield kmod
try:
kmod = kmod.next
except exceptions.InvalidAddressException:
return []
return # Generation finished
seen: Set = set()
while kmod != 0 and \
kmod not in seen and \
len(seen) < 1024:
while kmod != 0 and kmod not in seen and len(seen) < 1024:
kmod_obj = kmod.dereference()
@@ -74,9 +77,10 @@ class Lsmod(plugins.PluginInterface):
kmod = kmod.next
except exceptions.InvalidAddressException:
return
return # Generation finished
def _generator(self):
for module in self.list_modules(self.context, self.config['kernel']):
for module in self.list_modules(self.context, self.config["kernel"]):
mod_name = utility.array_to_string(module.name)
mod_size = module.size
@@ -84,4 +88,7 @@ class Lsmod(plugins.PluginInterface):
yield 0, (format_hints.Hex(module.vol.offset), mod_name, mod_size)
def run(self):
return renderers.TreeGrid([("Offset", format_hints.Hex), ("Name", str), ("Size", int)], self._generator())
return renderers.TreeGrid(
[("Offset", format_hints.Hex), ("Name", str), ("Size", int)],
self._generator(),
)
+31 -19
View File
@@ -21,33 +21,45 @@ class Lsof(plugins.PluginInterface):
@classmethod
def get_requirements(cls):
return [
requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS',
architectures = ["Intel32", "Intel64"]),
requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)),
requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)),
requirements.ListRequirement(name = 'pid',
description = 'Filter on specific process IDs',
element_type = int,
optional = True)
requirements.ModuleRequirement(
name="kernel",
description="Kernel module for the OS",
architectures=["Intel32", "Intel64"],
),
requirements.VersionRequirement(
name="macutils", component=mac.MacUtilities, version=(1, 0, 0)
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(3, 0, 0)
),
requirements.ListRequirement(
name="pid",
description="Filter on specific process IDs",
element_type=int,
optional=True,
),
]
def _generator(self, tasks):
darwin = self.context.modules[self.config['kernel']]
darwin = self.context.modules[self.config["kernel"]]
for task in tasks:
pid = task.p_pid
for _, filepath, fd in mac.MacUtilities.files_descriptors_for_process(self.context,
darwin.symbol_table_name,
task):
for _, filepath, fd in mac.MacUtilities.files_descriptors_for_process(
self.context, darwin.symbol_table_name, task
):
if filepath and len(filepath) > 0:
yield (0, (pid, fd, filepath))
def run(self):
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0]))
filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None))
list_tasks = pslist.PsList.get_list_tasks(
self.config.get("pslist_method", pslist.PsList.pslist_methods[0])
)
return renderers.TreeGrid([("PID", int), ("File Descriptor", int), ("File Path", str)],
self._generator(
list_tasks(self.context,
self.config['kernel'],
filter_func = filter_func)))
return renderers.TreeGrid(
[("PID", int), ("File Descriptor", int), ("File Path", str)],
self._generator(
list_tasks(self.context, self.config["kernel"], filter_func=filter_func)
),
)

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