mirror of
https://github.com/volatilityfoundation/volatility3.git
synced 2026-08-30 03:39:51 +02:00
Merge branch 'develop' into feature/cli-config-hierarchy
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Black python linter
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
options: "--check --diff --verbose"
|
||||
src: "./volatility3"
|
||||
@@ -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: |
|
||||
|
||||
@@ -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}}"
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Install Volatility3 test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
install_test:
|
||||
runs-on: ${{ matrix.host }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
host: [ ubuntu-latest, windows-latest ]
|
||||
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup python-pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Install volatility3
|
||||
run: pip install .
|
||||
|
||||
- name: Run volatility3
|
||||
run: vol --help
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 200
|
||||
days-before-issue-close: 60
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 200 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 60 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
exempt-issue-labels: "enhancement,plugin-request,question"
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Test Volatility3
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install Cmake
|
||||
pip install setuptools wheel
|
||||
pip install -r ./test/requirements-testing.txt
|
||||
|
||||
- name: Build PyPi packages
|
||||
run: |
|
||||
python setup.py sdist --formats=gztar,zip
|
||||
python setup.py bdist_wheel
|
||||
|
||||
- name: Download images
|
||||
run: |
|
||||
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/linux-sample-1.bin.gz"
|
||||
gunzip linux-sample-1.bin.gz
|
||||
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz"
|
||||
gunzip win-xp-laptop-2005-06-25.img.gz
|
||||
|
||||
- name: Download and Extract symbols
|
||||
run: |
|
||||
cd ./volatility3/symbols
|
||||
curl -sLO https://downloads.volatilityfoundation.org/volatility3/symbols/linux.zip
|
||||
unzip linux.zip
|
||||
cd -
|
||||
|
||||
- name: Testing...
|
||||
run: |
|
||||
py.test ./test/test_volatility.py --volatility=vol.py --image win-xp-laptop-2005-06-25.img -k test_windows -v
|
||||
py.test ./test/test_volatility.py --volatility=vol.py --image linux-sample-1.bin -k test_linux -v
|
||||
|
||||
- name: Clean up post-test
|
||||
run: |
|
||||
rm -rf *.lime
|
||||
rm -rf *.img
|
||||
cd volatility3/symbols
|
||||
rm -rf linux
|
||||
rm -rf linux.zip
|
||||
cd -
|
||||
@@ -38,3 +38,7 @@ ENV/
|
||||
# Memory dump files
|
||||
*.dmp
|
||||
*.vmem
|
||||
*.img
|
||||
|
||||
# PyTest cache files
|
||||
.pytest_cache/
|
||||
|
||||
+5
-1
@@ -12,8 +12,12 @@ sphinx:
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
formats: all
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: doc/requirements.txt
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ each_dict_entry_on_separate_line=True
|
||||
i18n_comment=
|
||||
|
||||
# The i18n function call names. The presence of this function stops
|
||||
# reformattting on that line, because the string it has cannot be moved
|
||||
# reformatting on that line, because the string it has cannot be moved
|
||||
# away from the i18n comment.
|
||||
i18n_function_call=
|
||||
|
||||
|
||||
@@ -4,6 +4,26 @@ API Changes
|
||||
When an addition to the existing API is made, the minor version is bumped.
|
||||
When an API feature or function is removed or changed, the major version is bumped.
|
||||
|
||||
2.5.0
|
||||
=====
|
||||
Add in support for specifying a type override for object_from_symbol
|
||||
|
||||
2.4.0
|
||||
=====
|
||||
Add a `get_size()` method to Windows VAD structures and fix several off-by-one issues when calculating VAD sizes.
|
||||
|
||||
2.3.1
|
||||
=====
|
||||
Update in the windows `_EPROCESS.owning_process` method to support Windows Vista and later versions.
|
||||
|
||||
2.3.0
|
||||
=====
|
||||
Add in `child_template` to template class
|
||||
|
||||
2.2.0
|
||||
=====
|
||||
Changes to linux core calls
|
||||
|
||||
2.1.0
|
||||
=====
|
||||
Add in the linux `task.get_threads` method to the API.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Volatility 3
|
||||
message: >-
|
||||
If you reference this software, please feel free to cite
|
||||
it using the information below.
|
||||
type: software
|
||||
authors:
|
||||
- name: Volatility Foundation
|
||||
country: US
|
||||
website: 'https://www.volatilityfoundation.org/'
|
||||
identifiers:
|
||||
- type: url
|
||||
value: 'https://github.com/volatilityfoundation/volatility3'
|
||||
description: Volatility 3 source code respository
|
||||
repository-code: 'https://github.com/volatilityfoundation/volatility3'
|
||||
url: 'https://github.com/volatilityfoundation/volatility3'
|
||||
abstract: >-
|
||||
Volatility is the world's most widely used framework for
|
||||
extracting digital artifacts from volatile memory (RAM)
|
||||
samples. The extraction techniques are performed
|
||||
completely independent of the system being investigated
|
||||
but offer visibility into the runtime state of the system.
|
||||
The framework is intended to introduce people to the
|
||||
techniques and complexities associated with extracting
|
||||
digital artifacts from volatile memory samples and provide
|
||||
a platform for further work into this exciting area of
|
||||
research.
|
||||
keywords:
|
||||
- malware
|
||||
- forensics
|
||||
- memory
|
||||
- python
|
||||
- ram
|
||||
- volatility
|
||||
+1
-1
@@ -31,7 +31,7 @@ If you make any Additions available to others, such as by providing copies of th
|
||||
- You are responsible to ensure you have rights in Additions necessary to comply with this section.
|
||||
|
||||
Contributing
|
||||
If you contribute (or offer to contribute) any materials to Volatility Foundation for the software, such as by submitting a pull request to the repository for the software or related content run by Volatility Foundation, you agree to contribute them under the under the BSD 2-Clause Plus Patent License (in the case of software) or the Creative Commons Zero Public Domain Dedication (in the case of content), unless you clearly mark them "Not a Contribution."
|
||||
If you contribute (or offer to contribute) any materials to Volatility Foundation for the software, such as by submitting a pull request to the repository for the software or related content run by Volatility Foundation, you agree to contribute them under the BSD 2-Clause Plus Patent License (in the case of software) or the Creative Commons Zero Public Domain Dedication (in the case of content), unless you clearly mark them "Not a Contribution."
|
||||
|
||||
Trademarks
|
||||
This license grants you no rights to any trademarks or service marks.
|
||||
|
||||
@@ -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
|
||||
@@ -94,6 +94,9 @@ Symbol tables zip files must be placed, as named, into the `volatility3/symbols`
|
||||
|
||||
Windows symbols that cannot be found will be queried, downloaded, generated and cached. Mac and Linux symbol tables must be manually produced by a tool such as [dwarf2json](https://github.com/volatilityfoundation/dwarf2json).
|
||||
|
||||
Important: The first run of volatility with new symbol files will require the cache to be updated. The symbol packs contain a large number of symbol files and so may take some time to update!
|
||||
However, this process only needs to be run once on each new symbol file, so assuming the pack stays in the same location will not need to be done again. Please also note it can be interrupted and next run will restart itself.
|
||||
|
||||
Please note: These are representative and are complete up to the point of creation for Windows and Mac. Due to the ease of compiling Linux kernels and the inability to uniquely distinguish them, an exhaustive set of Linux symbol tables cannot easily be supplied.
|
||||
|
||||
## Documentation
|
||||
@@ -104,7 +107,7 @@ The latest generated copy of the documentation can be found at: <https://volatil
|
||||
|
||||
## Licensing and Copyright
|
||||
|
||||
Copyright (C) 2007-2022 Volatility Foundation
|
||||
Copyright (C) 2007-2024 Volatility Foundation
|
||||
|
||||
All Rights Reserved
|
||||
|
||||
|
||||
@@ -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,4 +1,8 @@
|
||||
# These packages are required for building the documentation.
|
||||
sphinx>=4.0.0
|
||||
sphinx>=4.0.0,<7
|
||||
sphinx_autodoc_typehints>=1.4.0
|
||||
sphinx-rtd-theme>=0.4.3
|
||||
|
||||
yara-python
|
||||
pycryptodome
|
||||
pefile
|
||||
|
||||
+58
-15
@@ -1,7 +1,7 @@
|
||||
Volatility 3 Basics
|
||||
===================
|
||||
|
||||
Volatility splits memory analysis down to several components:
|
||||
Volatility splits memory analysis down to several components. The main ones are:
|
||||
|
||||
* Memory layers
|
||||
* Templates and Objects
|
||||
@@ -13,22 +13,65 @@ which acts as a container for all the various layers and tables necessary to con
|
||||
Memory layers
|
||||
-------------
|
||||
|
||||
A memory layer is a body of data that can be accessed by requesting data at a specific address. Memory is seen as
|
||||
sequential when accessed through sequential addresses, however, there is no obligation for the data to be stored
|
||||
sequentially, and modern processors tend to store the memory in a paged format. Moreover, there is no need for the data
|
||||
to be stored in an easily accessible format, it could be encoded or encrypted or more, it could be the combination of
|
||||
two other sources. These are typically handled by programs that process file formats, or the memory manager of the
|
||||
processor, but these are all translations (either in the geometric or linguistic sense) of the original data.
|
||||
A memory layer is a body of data that can be accessed by requesting data at a specific address. At its lowest level
|
||||
this data is stored on a phyiscal medium (RAM) and very early computers addresses locations in memory directly. However,
|
||||
as the size of memory increased and it became more difficult to manage memory most architectures moved to a "paged" model
|
||||
of memory, where the available memory is cut into specific fixed-sized pages. To help further, programs can ask for any address
|
||||
and the processor will look up their (virtual) address in a map, to find out where the (physical) address that it lives at is,
|
||||
in the actual memory of the system.
|
||||
|
||||
In Volatility 3 this is represented by a directed graph, whose end nodes are
|
||||
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are
|
||||
specifically called a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
|
||||
In this way, a raw memory image in the LiME file format and a page file can be
|
||||
combined to form a single Intel virtual memory layer. When requesting addresses from the Intel layer, it will use the
|
||||
Intel memory mapping algorithm, along with the address of the directory table base or page table map, to translate that
|
||||
Volatility can work with these layers as long as it knows the map (so, for example that virtual address `1` looks up at physical
|
||||
address `9`). The automagic that runs at the start of every volatility session often locates the kernel's memory map, and creates
|
||||
a kernel virtual layer, which allows for kernel addresses to be looked up and the correct data returned. There can, however, be
|
||||
several maps, and in general there is a different map for each process (although a portion of the operating system's memory is
|
||||
usually mapped to the same location across all processes). The maps may take the same address but point to a different part of
|
||||
physical memory. It also means that two processes could theoretically share memory, but having an virtual address mapped to the
|
||||
same physical address as another process. See the worked example below for more information.
|
||||
|
||||
To translate an address on a layer, call :py:meth:`layer.mapping(offset, length, ignore_errors) <volatility3.framework.interfaces.layers.TranslationLayerInterface.mapping>` and it will return a list of chunks without overlap, in order,
|
||||
for the requested range. If a portion cannot be mapped, an exception will be thrown unless `ignore_errors` is true. Each
|
||||
chunk will contain the original offset of the chunk, the translated offset, the original size and the translated size of
|
||||
the chunk, as well as the lower layer the chunk lives within.
|
||||
|
||||
Worked example
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The operating system and two programs may all appear to have access to all of physical memory, but actually the maps they each have
|
||||
mean they each see something different:
|
||||
|
||||
.. code-block::
|
||||
:caption: Memory mapping example
|
||||
|
||||
Operating system map Physical Memory
|
||||
1 -> 9 1 - Free
|
||||
2 -> 3 2 - OS.4, Process 1.4, Process 2.4
|
||||
3 -> 7 3 - OS.2
|
||||
4 -> 2 4 - Free
|
||||
5 - Free
|
||||
Process 1 map 6 - Process 1.2, Process 2.3
|
||||
1 -> 12 7 - OS.3
|
||||
2 -> 6 8 - Process1.3
|
||||
3 -> 8 9 - OS.1
|
||||
4 -> 2 10 - Process2.1
|
||||
11 - Free
|
||||
Process 2 map 12 - Process1.1
|
||||
1 -> 10 13 - Free
|
||||
2 -> 15 14 - Free
|
||||
3 -> 6 15 - Process2.2
|
||||
4 -> 2 16 - Free
|
||||
|
||||
In this example, part of the operating system is visible across all processes (although not all processes can write to the memory, there
|
||||
is a permissions model for intel addressing which is not discussed further here).)
|
||||
|
||||
In Volatility 3 mappings are represented by a directed graph of layers, whose end nodes are
|
||||
:py:class:`DataLayers <volatility3.framework.interfaces.layers.DataLayerInterface>` and whose internal nodes are :py:class:`TranslationLayers <volatility3.framework.interfaces.layers.TranslationLayerInterface>`.
|
||||
In this way, a raw memory image in the LiME file format and a page file can be combined to form a single Intel virtual
|
||||
memory layer. When requesting addresses from the Intel layer, it will use the Intel memory mapping algorithm, along
|
||||
with the address of the directory table base or page table map, to translate that
|
||||
address into a physical address, which will then either be directed towards the swap layer or the LiME layer. Should it
|
||||
be directed towards the LiME layer, the LiME file format algorithm will be translated to determine where within the file
|
||||
the data is stored and that will be returned.
|
||||
be directed towards the LiME layer, the LiME file format algorithm will be translate the new address to determine where
|
||||
within the file the data is stored. When the :py:meth:`layer.read() <volatility3.framework.interfaces.layers.TranslationLayerInterface.read>`
|
||||
method is called, the translation is done automatically and the correct data gathered and combined.
|
||||
|
||||
.. note:: Volatility 2 had a similar concept, called address spaces, but these could only stack linearly one on top of another.
|
||||
|
||||
|
||||
+102
-45
@@ -21,57 +21,72 @@ import sphinx.ext.apidoc
|
||||
|
||||
|
||||
def setup(app):
|
||||
volatility_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'volatility3'))
|
||||
volatility_directory = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "volatility3")
|
||||
)
|
||||
|
||||
source_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
sphinx.ext.apidoc.main(argv = ['-e', '-M', '-f', '-T', '-o', source_dir, volatility_directory])
|
||||
sphinx.ext.apidoc.main(
|
||||
["-e", "-M", "-f", "-T", "-o", source_dir, volatility_directory]
|
||||
)
|
||||
|
||||
# Go through the volatility3.framework.plugins files and change them to volatility3.plugins
|
||||
for dir, _, files in os.walk(os.path.dirname(__file__)):
|
||||
for filename in files:
|
||||
if filename.startswith('volatility3.framework.plugins') and filename != 'volatility3.framework.plugins.rst':
|
||||
if (
|
||||
filename.startswith("volatility3.framework.plugins")
|
||||
and filename != "volatility3.framework.plugins.rst"
|
||||
):
|
||||
# Change all volatility3.framework.plugins to volatility3.plugins in the file
|
||||
# Rename the file
|
||||
new_filename = filename.replace('volatility3.framework.plugins', 'volatility3.plugins')
|
||||
new_filename = filename.replace(
|
||||
"volatility3.framework.plugins", "volatility3.plugins"
|
||||
)
|
||||
|
||||
replace_string = b"Submodules\n----------\n\n.. toctree::\n\n"
|
||||
submodules = replace_string
|
||||
|
||||
# If file already exists, read out the subpackages entries from it add them to the new list
|
||||
if os.path.exists(os.path.join(dir, new_filename)):
|
||||
with open(os.path.join(dir, new_filename), 'rb') as newfile:
|
||||
with open(os.path.join(dir, new_filename), "rb") as newfile:
|
||||
data = newfile.read()
|
||||
index = data.find(replace_string)
|
||||
if index > -1:
|
||||
submodules = data[index:]
|
||||
|
||||
with open(os.path.join(dir, new_filename), 'wb') as newfile:
|
||||
with open(os.path.join(dir, new_filename), "wb") as newfile:
|
||||
with open(os.path.join(dir, filename), "rb") as oldfile:
|
||||
line = oldfile.read()
|
||||
correct_plugins = line.replace(b'volatility3.framework.plugins', b'volatility3.plugins')
|
||||
correct_submodules = correct_plugins.replace(replace_string, submodules)
|
||||
correct_plugins = line.replace(
|
||||
b"volatility3.framework.plugins", b"volatility3.plugins"
|
||||
)
|
||||
correct_submodules = correct_plugins.replace(
|
||||
replace_string, submodules
|
||||
)
|
||||
newfile.write(correct_submodules)
|
||||
os.remove(os.path.join(dir, filename))
|
||||
elif filename == 'volatility3.framework.rst':
|
||||
elif filename == "volatility3.framework.rst":
|
||||
with open(os.path.join(dir, filename), "rb") as contents:
|
||||
lines = contents.readlines()
|
||||
plugins_seen = False
|
||||
with open(os.path.join(dir, filename), "wb") as contents:
|
||||
for line in lines:
|
||||
if b'volatility3.framework.plugins' in line:
|
||||
if b"volatility3.framework.plugins" in line:
|
||||
plugins_seen = True
|
||||
if plugins_seen and line == b'':
|
||||
contents.write(b' volatility3.plugins')
|
||||
if plugins_seen and line == b"":
|
||||
contents.write(b" volatility3.plugins")
|
||||
contents.write(line)
|
||||
elif filename == 'volatility3.plugins.rst':
|
||||
elif filename == "volatility3.plugins.rst":
|
||||
with open(os.path.join(dir, filename), "rb") as contents:
|
||||
lines = contents.readlines()
|
||||
with open(os.path.join(dir, 'volatility3.framework.plugins.rst'), "rb") as contents:
|
||||
with open(
|
||||
os.path.join(dir, "volatility3.framework.plugins.rst"), "rb"
|
||||
) as contents:
|
||||
real_lines = contents.readlines()
|
||||
|
||||
# Process real_lines
|
||||
for line_index in range(len(real_lines)):
|
||||
if b'Submodules' in real_lines[line_index]:
|
||||
if b"Submodules" in real_lines[line_index]:
|
||||
break
|
||||
else:
|
||||
line_index = len(real_lines)
|
||||
@@ -82,60 +97,79 @@ def setup(app):
|
||||
for line in lines:
|
||||
contents.write(line)
|
||||
for line in submodule_lines:
|
||||
contents.write(line.replace(b'volatility3.framework.plugins', b'volatility3.plugins'))
|
||||
contents.write(
|
||||
line.replace(
|
||||
b"volatility3.framework.plugins", b"volatility3.plugins"
|
||||
)
|
||||
)
|
||||
|
||||
# Clear up the framework.plugins page
|
||||
with open(os.path.join(os.path.dirname(__file__), 'volatility3.framework.plugins.rst'), "rb") as contents:
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "volatility3.framework.plugins.rst"),
|
||||
"rb",
|
||||
) as contents:
|
||||
real_lines = contents.readlines()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'volatility3.framework.plugins.rst'), "wb") as contents:
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), "volatility3.framework.plugins.rst"),
|
||||
"wb",
|
||||
) as contents:
|
||||
for line in real_lines:
|
||||
if b'volatility3.framework.plugins.' not in line:
|
||||
if b"volatility3.framework.plugins." not in line:
|
||||
contents.write(line)
|
||||
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
from volatility3.framework import constants
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = '2.0'
|
||||
needs_sphinx = "2.0"
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage', 'sphinx.ext.viewcode'
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
]
|
||||
|
||||
autosectionlabel_prefix_document = True
|
||||
|
||||
try:
|
||||
import sphinx_autodoc_typehints
|
||||
|
||||
extensions.append('sphinx_autodoc_typehints')
|
||||
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.
|
||||
# templates_path = ['tools/templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = 'Volatility 3'
|
||||
copyright = '2012-2022, Volatility Foundation'
|
||||
project = "Volatility 3"
|
||||
copyright = "2012-2024, Volatility Foundation"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
@@ -144,7 +178,7 @@ copyright = '2012-2022, Volatility Foundation'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = constants.PACKAGE_VERSION
|
||||
# The short X.Y version.
|
||||
version = ".".join(release.split('.')[0:2])
|
||||
version = ".".join(release.split(".")[0:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -177,7 +211,7 @@ add_module_names = False
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
@@ -193,8 +227,8 @@ pygments_style = 'sphinx'
|
||||
# html_theme = 'pydoctheme'
|
||||
# html_theme_options = {'collapsiblesidebar': True}
|
||||
# html_theme_path = ['tools']
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_options = {'logo_only': True}
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_options = {"logo_only": True}
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@@ -213,17 +247,17 @@ html_theme_options = {'logo_only': True}
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = '_static/vol.png'
|
||||
html_logo = "_static/vol.png"
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = '_static/favicon.ico'
|
||||
html_favicon = "_static/favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
@@ -272,17 +306,15 @@ html_static_path = ['_static']
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Volatilitydoc'
|
||||
htmlhelp_basename = "Volatilitydoc"
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
@@ -291,7 +323,13 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'Volatility.tex', 'Volatility 3 Documentation', 'Volatility Foundation', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"Volatility.tex",
|
||||
"Volatility 3 Documentation",
|
||||
"Volatility Foundation",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -318,7 +356,15 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [('vol-cli', 'volatility', 'Volatility 3 Documentation', ['Volatility Foundation'], 1)]
|
||||
man_pages = [
|
||||
(
|
||||
"vol-cli",
|
||||
"volatility",
|
||||
"Volatility 3 Documentation",
|
||||
["Volatility Foundation"],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
@@ -329,8 +375,15 @@ man_pages = [('vol-cli', 'volatility', 'Volatility 3 Documentation', ['Volatilit
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Volatility', 'Volatility 3 Documentation', 'Volatility Foundation', 'Volatility',
|
||||
'Memory forensics framework.', 'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"Volatility",
|
||||
"Volatility 3 Documentation",
|
||||
"Volatility Foundation",
|
||||
"Volatility",
|
||||
"Memory forensics framework.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -346,10 +399,14 @@ texinfo_documents = [
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'http://docs.python.org/': None}
|
||||
intersphinx_mapping = {"python": ("http://docs.python.org/", None)}
|
||||
|
||||
# -- Autodoc options -------------------------------------------------------
|
||||
|
||||
# autodoc_member_order = 'groupwise'
|
||||
autodoc_default_options = {'members': True, 'inherited-members': True, 'show-inheritance': True}
|
||||
autoclass_content = 'both'
|
||||
autodoc_default_options = {
|
||||
"members": True,
|
||||
"inherited-members": True,
|
||||
"show-inheritance": True,
|
||||
}
|
||||
autoclass_content = "both"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
Linux Tutorial
|
||||
==============
|
||||
|
||||
This guide will give you a brief overview of how volatility3 works as well as a demonstration of several of the plugins available in the suite.
|
||||
|
||||
Acquiring memory
|
||||
----------------
|
||||
|
||||
Volatility3 does not provide the ability to acquire memory. Below are some examples of tools that can be used to acquire memory, but more are available:
|
||||
|
||||
* `AVML - Acquire Volatile Memory for Linux <https://github.com/microsoft/avml>`_
|
||||
* `LiME - Linux Memory Extract <https://github.com/504ensicsLabs/LiME>`_
|
||||
|
||||
|
||||
Procedure to create symbol tables for linux
|
||||
--------------------------------------------
|
||||
|
||||
To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`.
|
||||
|
||||
.. tip:: It may be possible to locate pre-made ISF files from the `Linux ISF Server <https://isf-server.techanarchy.net/>`_ ,
|
||||
which is built and maintained by `kevthehermit <https://twitter.com/kevthehermit>`_.
|
||||
After creating the file or downloading it from the ISF server, place the file under the directory ``volatility3/symbols/linux``.
|
||||
If necessary create a linux directory under the symbols directory (this will become unnecessary in future versions).
|
||||
|
||||
|
||||
Listing plugins
|
||||
---------------
|
||||
|
||||
The following is a sample of the linux plugins available for volatility3, it is not complete and more more plugins may
|
||||
be added. For a complete reference, please see the volatility 3 :doc:`list of plugins <volatility3.plugins>`.
|
||||
For plugin requests, please create an issue with a description of the requested plugin.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py --help | grep -i linux. | head -n 5
|
||||
banners.Banners Attempts to identify potential linux banners in an
|
||||
linux.bash.Bash Recovers bash command history from memory.
|
||||
linux.check_afinfo.Check_afinfo
|
||||
linux.check_creds.Check_creds
|
||||
linux.check_idt.Check_idt
|
||||
|
||||
.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of linux plugins.
|
||||
|
||||
|
||||
Using plugins
|
||||
-------------
|
||||
|
||||
The following is the syntax to run the volatility CLI.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f <path to memory image> <plugin_name> <plugin_option>
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
banners
|
||||
~~~~~~~
|
||||
|
||||
In this example we will be using a memory dump from the Insomni'hack teaser 2020 CTF Challenge called Getdents. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge.
|
||||
Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/insomnihack/2020/09/17/Insomihack-getdents.html>`_.
|
||||
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f memory.vmem banners
|
||||
|
||||
Volatility 3 Framework 2.0.1
|
||||
|
||||
Progress: 100.00 PDB scanning finished
|
||||
Offset Banner
|
||||
|
||||
0x141c1390 Linux version 4.15.0-42-generic (buildd@lgw01-amd64-023) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #45-Ubuntu SMP Thu Nov 15 19:32:57 UTC 2018 (Ubuntu 4.15.0-42.45-generic 4.15.18)
|
||||
0x63a00160 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18)
|
||||
0x6455c4d4 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18)
|
||||
0x6e1e055f Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18)
|
||||
0x7fde0010 Linux version 4.15.0-72-generic (buildd@lcy01-amd64-026) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #81-Ubuntu SMP Tue Nov 26 12:20:02 UTC 2019 (Ubuntu 4.15.0-72.81-generic 4.15.18)
|
||||
|
||||
|
||||
The above command helps us to find the memory dump's kernel version and the distribution version. Now using the above banner we can search for the needed ISF file from the ISF server.
|
||||
If ISF file cannot be found then, follow the instructions on :ref:`getting-started-linux-tutorial:Procedure to create symbol tables for linux`. After that, place the ISF file under the ``volatility3/symbols/linux`` directory.
|
||||
|
||||
.. tip:: Use the banner text which is most repeated to search from ISF Server.
|
||||
|
||||
linux.pslist
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f memory.vmem linux.pslist
|
||||
|
||||
Volatility 3 Framework 2.0.1 Stacking attempts finished
|
||||
|
||||
PID PPID COMM
|
||||
|
||||
1 0 systemd
|
||||
2 0 kthreadd
|
||||
3 2 kworker/0:0
|
||||
4 2 kworker/0:0H
|
||||
5 2 kworker/u256:0
|
||||
6 2 mm_percpu_wq
|
||||
7 2 ksoftirqd/0
|
||||
8 2 rcu_sched
|
||||
9 2 rcu_bh
|
||||
10 2 migration/0
|
||||
11 2 watchdog/0
|
||||
12 2 cpuhp/0
|
||||
13 2 kdevtmpfs
|
||||
14 2 netns
|
||||
15 2 rcu_tasks_kthre
|
||||
16 2 kauditd
|
||||
.....
|
||||
|
||||
``linux.pslist`` helps us to list the processes which are running, their PIDs and PPIDs.
|
||||
|
||||
linux.pstree
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f memory.vmem linux.pstree
|
||||
Volatility 3 Framework 2.0.1
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
PID PPID COMM
|
||||
|
||||
1 0 systemd
|
||||
* 636 1 polkitd
|
||||
* 514 1 acpid
|
||||
* 1411 1 pulseaudio
|
||||
* 517 1 rsyslogd
|
||||
* 637 1 cups-browsed
|
||||
* 903 1 whoopsie
|
||||
* 522 1 ModemManager
|
||||
* 525 1 cron
|
||||
* 526 1 avahi-daemon
|
||||
** 542 526 avahi-daemon
|
||||
* 657 1 unattended-upgr
|
||||
* 914 1 kerneloops
|
||||
* 532 1 dbus-daemon
|
||||
* 1429 1 ibus-x11
|
||||
* 929 1 kerneloops
|
||||
* 1572 1 gsd-printer
|
||||
* 933 1 upowerd
|
||||
* 1071 1 rtkit-daemon
|
||||
* 692 1 gdm3
|
||||
** 1234 692 gdm-session-wor
|
||||
*** 1255 1234 gdm-x-session
|
||||
**** 1257 1255 Xorg
|
||||
**** 1266 1255 gnome-session-b
|
||||
***** 1537 1266 gsd-clipboard
|
||||
***** 1539 1266 gsd-color
|
||||
***** 1542 1266 gsd-datetime
|
||||
***** 2950 1266 deja-dup-monito
|
||||
***** 1546 1266 gsd-housekeepin
|
||||
***** 1548 1266 gsd-keyboard
|
||||
***** 1550 1266 gsd-media-keys
|
||||
|
||||
``linux.pstree`` helps us to display the parent child relationships between processes.
|
||||
|
||||
linux.bash
|
||||
~~~~~~~~~~
|
||||
|
||||
Now to find the commands that were run in the bash shell by using ``linux.bash``.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f memory.vmem linux.bash
|
||||
|
||||
Volatility 3 Framework 2.0.1
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
PID Process CommandTime Command
|
||||
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo reboot
|
||||
1733 bash 2020-01-16 14:00:36.000000 AWAVH��
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo reboot
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt update
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt update
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo reboot
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt update
|
||||
1733 bash 2020-01-16 14:00:36.000000 rub
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade
|
||||
1733 bash 2020-01-16 14:00:36.000000 uname -a
|
||||
1733 bash 2020-01-16 14:00:36.000000 uname -a
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt autoclean
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo reboot
|
||||
1733 bash 2020-01-16 14:00:36.000000 sudo apt upgrade
|
||||
1733 bash 2020-01-16 14:00:41.000000 chmod +x meterpreter
|
||||
1733 bash 2020-01-16 14:00:42.000000 sudo ./meterpreter
|
||||
@@ -0,0 +1,153 @@
|
||||
macOS Tutorial
|
||||
==============
|
||||
|
||||
This guide will give you a brief overview of how volatility3 works as well as a demonstration of several of the plugins available in the suite.
|
||||
|
||||
Acquiring memory
|
||||
----------------
|
||||
|
||||
Volatility3 does not provide the ability to acquire memory. The example below is an open source tool. Other commercial tools are also available.
|
||||
|
||||
* `osxpmem <https://github.com/Velocidex/c-aff4/releases/download/3.2/osxpmem_3.2.zip>`_
|
||||
|
||||
|
||||
|
||||
Procedure to create symbol tables for macOS
|
||||
--------------------------------------------
|
||||
|
||||
To create a symbol table please refer to :ref:`symbol-tables:Mac or Linux symbol tables`.
|
||||
|
||||
.. tip:: It may be possible to locate pre-made ISF files from the `download link <https://downloads.volatilityfoundation.org/volatility3/symbols/mac.zip>`_ ,
|
||||
which is built and maintained by `volatilityfoundation <https://www.volatilityfoundation.org/>`_.
|
||||
After creating the file or downloading it from the link, place the file under the directory ``volatility3/symbols/``.
|
||||
|
||||
|
||||
Listing plugins
|
||||
---------------
|
||||
|
||||
The following is a sample of the macOS plugins available for volatility3, it is not complete and more plugins may
|
||||
be added. For a complete reference, please see the volatility 3 :doc:`list of plugins <volatility3.plugins>`.
|
||||
For plugin requests, please create an issue with a description of the requested plugin.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py --help | grep -i mac. | head -n 4
|
||||
mac.bash.Bash Recovers bash command history from memory.
|
||||
mac.check_syscall.Check_syscall
|
||||
mac.check_sysctl.Check_sysctl
|
||||
mac.check_trap_table.Check_trap_table
|
||||
|
||||
.. note:: Here the the command is piped to grep and head in-order to provide the start of the list of macOS plugins.
|
||||
|
||||
|
||||
Using plugins
|
||||
-------------
|
||||
|
||||
The following is the syntax to run the volatility CLI.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f <path to memory image> <plugin_name> <plugin_option>
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
banners
|
||||
~~~~~~~
|
||||
|
||||
In this example we will be using a memory dump from the Securinets CTF Quals 2019 Challenge called Contact_me. We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenge.
|
||||
Thanks go to `stuxnet <https://github.com/stuxnet999/>`_ for providing this memory dump and `writeup <https://stuxnet999.github.io/securinets-ctf/2019/08/24/SecurinetsQuals2019-Contact-Me.html>`_.
|
||||
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me banners.Banners
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
|
||||
Progress: 100.00 PDB scanning finished
|
||||
Offset Banner
|
||||
|
||||
0x4d2c7d0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0xb42b180 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0xcda9100 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x1275e7d0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x1284fba4 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
0x34ad0180 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64
|
||||
|
||||
|
||||
The above command helps us to find the memory dump's Darwin kernel version. Now using the above banner we can search for the needed ISF file.
|
||||
If ISF file cannot be found then, follow the instructions on :ref:`getting-started-mac-tutorial:Procedure to create symbol tables for macOS`. After that, place the ISF file under the ``volatility3/symbols`` directory.
|
||||
|
||||
mac.pslist
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me mac.pslist.PsList
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
|
||||
PID PPID COMM
|
||||
|
||||
0 0 kernel_task
|
||||
1 0 launchd
|
||||
35 1 UserEventAgent
|
||||
38 1 kextd
|
||||
39 1 fseventsd
|
||||
37 1 uninstalld
|
||||
45 1 configd
|
||||
46 1 powerd
|
||||
52 1 logd
|
||||
58 1 warmd
|
||||
.....
|
||||
|
||||
``mac.pslist`` helps us to list the processes which are running, their PIDs and PPIDs.
|
||||
|
||||
mac.pstree
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me mac.pstree.PsTree
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
PID PPID COMM
|
||||
|
||||
35 1 UserEventAgent
|
||||
38 1 kextd
|
||||
39 1 fseventsd
|
||||
37 1 uninstalld
|
||||
204 1 softwareupdated
|
||||
* 449 204 SoftwareUpdateCo
|
||||
337 1 system_installd
|
||||
* 455 337 update_dyld_shar
|
||||
|
||||
``mac.pstree`` helps us to display the parent child relationships between processes.
|
||||
|
||||
mac.ifconfig
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f contact_me mac.ifconfig.Ifconfig
|
||||
|
||||
Volatility 3 Framework 2.4.2
|
||||
Progress: 100.00 Stacking attempts finished
|
||||
Interface IP Address Mac Address Promiscuous
|
||||
|
||||
lo0 False
|
||||
lo0 127.0.0.1 False
|
||||
lo0 ::1 False
|
||||
lo0 fe80:1::1 False
|
||||
gif0 False
|
||||
stf0 False
|
||||
en0 00:0C:29:89:8B:F0 00:0C:29:89:8B:F0 False
|
||||
en0 fe80:4::10fb:c89d:217f:52ae 00:0C:29:89:8B:F0 False
|
||||
en0 192.168.140.128 00:0C:29:89:8B:F0 False
|
||||
utun0 False
|
||||
utun0 fe80:5::2a95:bb15:87e3:977c False
|
||||
|
||||
we can use the ``mac.ifconfig`` plugin to get information about the configuration of the network interfaces of the host under investigation.
|
||||
@@ -0,0 +1,124 @@
|
||||
Windows Tutorial
|
||||
================
|
||||
|
||||
This guide provides a brief introduction to how volatility3 works as a demonstration of several of the plugins available in the suite.
|
||||
|
||||
Acquiring memory
|
||||
----------------
|
||||
|
||||
Volatility does not provide the ability to acquire memory.
|
||||
Memory can be acquired using a number of tools, below are some examples but others exist:
|
||||
|
||||
* `WinPmem <https://github.com/Velocidex/WinPmem/releases/latest>`_
|
||||
* `FTK Imager <https://accessdata.com/product-download/ftk-imager-version-4-5>`_
|
||||
|
||||
Listing Plugins
|
||||
---------------
|
||||
|
||||
The following is a sample of the windows plugins available for volatility3, it is not complete and more more plugins may
|
||||
be added. For a complete reference, please see the volatility 3 :doc:`list of plugins <volatility3.plugins>`.
|
||||
For plugin requests, please create an issue with a description of the requested plugin.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py --help | grep windows | head -n 5
|
||||
windows.bigpools.BigPools
|
||||
windows.cmdline.CmdLine
|
||||
windows.crashinfo.Crashinfo
|
||||
windows.dlllist.DllList
|
||||
|
||||
.. note:: Here the the command is piped to grep and head in-order to provide the start of a list of the available windows plugins.
|
||||
|
||||
Using plugins
|
||||
-------------
|
||||
|
||||
The following is the syntax to run the volatility CLI.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f <path to memory image> plugin_name plugin_option
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
windows.pslist
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
In this example we will be using a memory dump from the PragyanCTF'22.
|
||||
We will limit the discussion to memory forensics with volatility 3 and not extend it to other parts of the challenges.
|
||||
|
||||
When using windows plugins in volatility 3, the required ISF file can often be generated from PDB files automatically
|
||||
downloaded from Microsoft servers, and therefore does not require locating or adding specific ISF files to the volatility 3 symbols directory.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f MemDump.DMP windows.pslist | head -n 10
|
||||
|
||||
Volatility 3 Framework 2.0.1 PDB scanning finished
|
||||
|
||||
PID PPID ImageFileName Offset(V) Threads Handles SessionId Wow64 CreateTime ExitTime File output
|
||||
|
||||
4 0 System 0xfa8000cbc040 85 492 N/A False 2022-02-07 16:30:12.000000 N/A Disabled
|
||||
276 4 smss.exe 0xfa8001e04040 2 29 N/A False 2022-02-07 16:30:12.000000 N/A Disabled
|
||||
352 336 csrss.exe 0xfa8002110b30 9 375 0 False 2022-02-07 16:30:13.000000 N/A Disabled
|
||||
404 336 wininit.exe 0xfa800219f060 3 74 0 False 2022-02-07 16:30:13.000000 N/A Disabled
|
||||
412 396 csrss.exe 0xfa80021c5b30 9 224 1 False 2022-02-07 16:30:13.000000 N/A Disabled
|
||||
468 396 winlogon.exe 0xfa8002284060 5 113 1 False 2022-02-07 16:30:14.000000 N/A Disabled
|
||||
|
||||
``windows.pslist`` helps list the processes running while the memory dump was taken.
|
||||
|
||||
windows.pstree
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f MemDump.DMP windows.pstree | head -n 20
|
||||
Volatility 3 Framework 2.0.1 PDB scanning finished
|
||||
|
||||
PID PPID ImageFileName Offset(V) Threads Handles SessionId Wow64 CreateTime ExitTime
|
||||
|
||||
4 0 System 0xfa8000cbc040 85 492 N/A False 2022-02-07 16:30:12.000000 N/A
|
||||
* 276 4 smss.exe 0xfa8001e04040 2 29 N/A False 2022-02-07 16:30:12.000000 N/A
|
||||
352 336 csrss.exe 0xfa8002110b30 9 375 0 False 2022-02-07 16:30:13.000000 N/A
|
||||
404 336 wininit.exe 0xfa800219f060 3 74 0 False 2022-02-07 16:30:13.000000 N/A
|
||||
* 504 404 services.exe 0xfa80022ccb30 7 190 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
** 960 504 svchost.exe 0xfa8001c17b30 39 1003 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
** 1216 504 svchost.exe 0xfa80026e0b30 18 311 0 False 2022-02-07 16:30:15.000000 N/A
|
||||
** 1312 504 svchost.exe 0xfa8002740380 19 287 0 False 2022-02-07 16:30:15.000000 N/A
|
||||
** 1984 504 taskhost.exe 0xfa8002eb1b30 8 129 1 False 2022-02-07 16:30:27.000000 N/A
|
||||
** 804 504 svchost.exe 0xfa80024ca5f0 20 450 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
*** 100 804 audiodg.exe 0xfa80025b4b30 6 131 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
** 1568 504 SearchIndexer. 0xfa800254b480 12 616 0 False 2022-02-07 16:30:32.000000 N/A
|
||||
** 744 504 svchost.exe 0xfa8002477b30 8 265 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
** 1096 504 svchost.exe 0xfa800260db30 14 357 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
** 616 504 svchost.exe 0xfa8002b86ab0 13 314 0 False 2022-02-07 16:32:16.000000 N/A
|
||||
** 624 504 svchost.exe 0xfa8002410630 10 350 0 False 2022-02-07 16:30:14.000000 N/A
|
||||
|
||||
``windows.pstree`` helps to display the parent child relationships between processes.
|
||||
|
||||
.. note:: Here the the command is piped to head in-order to provide smaller output, here listing only the first 20.
|
||||
|
||||
windows.hashdump
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python3 vol.py -f MemDump.DMP windows.hashdump
|
||||
Volatility 3 Framework 2.0.3
|
||||
Progress: 100.00 PDB scanning finished
|
||||
User rid lmhash nthash
|
||||
|
||||
Administrator 500 aad3b435b51404eeaad3b435b51404ee 31d6cfe0d16ae931b73c59d7e0c089c0
|
||||
Guest 501 aad3b435b51404eeaad3b435b51404ee 31d6cfe0d16ae931b73c59d7e0c089c0
|
||||
Frank Reynolds 1000 aad3b435b51404eeaad3b435b51404ee a88d1e18706d3aa676e01e5943d15911
|
||||
HomeGroupUser$ 1002 aad3b435b51404eeaad3b435b51404ee af10ecac6ea817d2bb56e3e5c33ce1cd
|
||||
Dennis 1003 aad3b435b51404eeaad3b435b51404ee cf96684bbc7877920adaa9663698bf54
|
||||
|
||||
``windows.hashdump`` helps to list the hashes of the users in the system.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+14
-3
@@ -7,9 +7,10 @@ Volatility 3 is Open Source.
|
||||
|
||||
:doc:`List of plugins <volatility3.plugins>`
|
||||
|
||||
Here are some guidelines for using Volatility 3 effectively:
|
||||
Below is the main documentation regarding volatility 3:
|
||||
|
||||
.. toctree::
|
||||
:caption: Documentation
|
||||
|
||||
basics
|
||||
development
|
||||
@@ -18,12 +19,22 @@ Here are some guidelines for using Volatility 3 effectively:
|
||||
volshell
|
||||
glossary
|
||||
|
||||
Python Packages
|
||||
===============
|
||||
There is also some information to get you started quickly:
|
||||
|
||||
.. toctree::
|
||||
:caption: Getting Started
|
||||
|
||||
getting-started-linux-tutorial
|
||||
getting-started-mac-tutorial
|
||||
getting-started-windows-tutorial
|
||||
|
||||
|
||||
.. toctree::
|
||||
:caption: Python Packages
|
||||
|
||||
volatility3
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
+105
-54
@@ -6,6 +6,12 @@ This guide will step through how to construct a simple plugin using Volatility 3
|
||||
The example plugin we'll use is :py:class:`~volatility3.plugins.windows.dlllist.DllList`, which features the main traits
|
||||
of a normal plugin, and reuses other plugins appropriately.
|
||||
|
||||
.. note::
|
||||
|
||||
This document will not include the complete code necessary for a
|
||||
working plugin (such as imports, etc) since it's designed to focus on the necessary components for writing a plugin.
|
||||
For complete and functioning plugins, the ``framework/plugins`` directory should be consulted.
|
||||
|
||||
Inherit from PluginInterface
|
||||
----------------------------
|
||||
|
||||
@@ -30,20 +36,20 @@ to be able to run properly. Any that are defined as optional need not necessari
|
||||
|
||||
::
|
||||
|
||||
_version = (1, 0, 0)
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls):
|
||||
return [requirements.TranslationLayerRequirement(name = 'primary',
|
||||
description = 'Memory layer for the kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
requirements.SymbolTableRequirement(name = "nt_symbols",
|
||||
description = "Windows kernel symbols"),
|
||||
requirements.PluginRequirement(name = 'pslist',
|
||||
plugin = pslist.PsList,
|
||||
version = (1, 0, 0)),
|
||||
return [requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
requirements.ListRequirement(name = 'pid',
|
||||
element_type = int,
|
||||
description = "Process IDs to include (all other processes are excluded)",
|
||||
optional = True)]
|
||||
optional = True),
|
||||
requirements.PluginRequirement(name = 'pslist',
|
||||
plugin = pslist.PsList,
|
||||
version = (2, 0, 0))]
|
||||
|
||||
|
||||
This is a classmethod, because it is called before the specific plugin object has been instantiated (in order to know how
|
||||
@@ -51,69 +57,112 @@ to instantiate the plugin). At the moment these requirements are fairly straigh
|
||||
|
||||
::
|
||||
|
||||
requirements.TranslationLayerRequirement(name = 'primary',
|
||||
description = 'Memory layer for the kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
|
||||
This requirement indicates that the plugin will operate on a single
|
||||
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
|
||||
loaded layer will appear in the plugin's configuration under the name ``primary``. Requirement values can be
|
||||
accessed within the plugin through the plugin's `config` attribute (for example ``self.config['pid']``).
|
||||
This requirement specifies the need for a particular submodule. Each module requires a
|
||||
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a
|
||||
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, which are fulfilled by two
|
||||
subrequirements: a
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` and a
|
||||
:py:class:`~volatility3.framework.configuration.requirements.SymbolTableRequirement`. At the moment, the automagic
|
||||
only fills `ModuleRequirements` with kernels, and so has relatively few parameters. It requires the architecture for
|
||||
the underlying TranslationLayer, and the offset of the module within that layer.
|
||||
|
||||
.. note:: The name itself is dynamic depending on the other layers already present in the Context. Always use the value
|
||||
from the configuration rather than attempting to guess what the layer will be called.
|
||||
The name of the module will be stored in the ``kernel`` configuration option, and the module object itself
|
||||
can be accessed from the ``context.modules`` collection. This requirement is a Complex Requirement and therefore will
|
||||
not be requested directly from the user.
|
||||
|
||||
Finally, this defines that the translation layer must be on the Intel Architecture. At the moment, this acts as a filter,
|
||||
failing to be satisfied by memory images that do not match the architecture required.
|
||||
|
||||
Most plugins will only operate on a single layer, but it is entirely possible for a plugin to request two different
|
||||
layers, for example a plugin that carries out some form of difference or statistics against multiple memory images.
|
||||
.. note::
|
||||
|
||||
This requirement (and the next two) are known as Complex Requirements, and user interfaces will likely not directly
|
||||
request a value for this from a user. The value stored in the configuration tree for a
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` is
|
||||
the string name of a layer present in the context's memory that satisfies the requirement.
|
||||
In previous versions of volatility 3, there was no `ModuleRequirement`, and instead two requirements were defined
|
||||
a :py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>` and a `SymbolTableRequirement`. These still exist, and can be used, most plugins just
|
||||
define a single `ModuleRequirement` for the kernel, which the automagic will populate. The `ModuleRequirement` has
|
||||
two automatic sub-requirements, a `TranslationLayerRequirement` and a `SymbolTableRequirement`, but the module also
|
||||
includes the offset of the module, and will allow future expansion to specify specific modules when application
|
||||
level plugins become more common. Below are how the requirements would be specified:
|
||||
|
||||
::
|
||||
::
|
||||
|
||||
requirements.SymbolTableRequirement(name = "nt_symbols",
|
||||
description = "Windows kernel symbols"),
|
||||
requirements.TranslationLayerRequirement(name = 'primary',
|
||||
description = 'Memory layer for the kernel',
|
||||
architectures = ["Intel32", "Intel64"]),
|
||||
|
||||
This requirement specifies the need for a particular
|
||||
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
to be loaded. This gets populated by various
|
||||
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` as the nearest sibling to a particular
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`.
|
||||
This means that if the :py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`
|
||||
is satisfied and the :py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` can determine
|
||||
the appropriate :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, the
|
||||
name of the :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>` will be stored in the configuration.
|
||||
This requirement indicates that the plugin will operate on a single
|
||||
:py:class:`TranslationLayer <volatility3.framework.interfaces.layers.TranslationLayerInterface>`. The name of the
|
||||
loaded layer will appear in the plugin's configuration under the name ``primary``. Requirement values can be
|
||||
accessed within the plugin through the plugin's `config` attribute (for example ``self.config['pid']``).
|
||||
|
||||
This requirement is also a Complex Requirement and therefore will not be requested directly from the user.
|
||||
.. note:: The name itself is dynamic depending on the other layers already present in the Context. Always use the value
|
||||
from the configuration rather than attempting to guess what the layer will be called.
|
||||
|
||||
::
|
||||
Finally, this defines that the translation layer must be on the Intel Architecture. At the moment, this acts as a filter,
|
||||
failing to be satisfied by memory images that do not match the architecture required.
|
||||
|
||||
requirements.PluginRequirement(name = 'pslist',
|
||||
plugin = pslist.PsList,
|
||||
version = (1, 0, 0)),
|
||||
Most plugins will only operate on a single layer, but it is entirely possible for a plugin to request two different
|
||||
layers, for example a plugin that carries out some form of difference or statistics against multiple memory images.
|
||||
|
||||
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
|
||||
on that plugin. The version is specified in terms of Semantic Versioning, meaning that to be compatible, the major
|
||||
versions must be identical and the minor version must be equal to or higher than the one provided. This requirement
|
||||
does not make use of any data from the configuration, even if it were provided, it is merely a functional check before
|
||||
running the plugin.
|
||||
This requirement (and the next two) are known as Complex Requirements, and user interfaces will likely not directly
|
||||
request a value for this from a user. The value stored in the configuration tree for a
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement` is
|
||||
the string name of a layer present in the context's memory that satisfies the requirement.
|
||||
|
||||
::
|
||||
|
||||
requirements.SymbolTableRequirement(name = "nt_symbols",
|
||||
description = "Windows kernel symbols"),
|
||||
|
||||
This requirement specifies the need for a particular
|
||||
:py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`
|
||||
to be loaded. This gets populated by various
|
||||
:py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` as the nearest sibling to a particular
|
||||
:py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`.
|
||||
This means that if the :py:class:`~volatility3.framework.configuration.requirements.TranslationLayerRequirement`
|
||||
is satisfied and the :py:class:`Automagic <volatility3.framework.interfaces.automagic.AutoMagicInterface>` can determine
|
||||
the appropriate :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>`, the
|
||||
name of the :py:class:`SymbolTable <volatility3.framework.interfaces.symbols.SymbolTableInterface>` will be stored in the configuration.
|
||||
|
||||
This requirement is also a Complex Requirement and therefore will not be requested directly from the user.
|
||||
|
||||
::
|
||||
|
||||
requirements.ListRequirement(name = 'pid',
|
||||
description = 'Filter on specific process IDs',
|
||||
element_type = int,
|
||||
optional = True)
|
||||
optional = True),
|
||||
|
||||
The final requirement is a List Requirement, populated by integers. The description will be presented to the user to
|
||||
The next requirement is a List Requirement, populated by integers. The description will be presented to the user to
|
||||
describe what the value represents. The optional flag indicates that the plugin can function without the ``pid`` value
|
||||
being defined within the configuration tree at all.
|
||||
|
||||
::
|
||||
|
||||
requirements.PluginRequirement(name = 'pslist',
|
||||
plugin = pslist.PsList,
|
||||
version = (2, 0, 0))]
|
||||
|
||||
This requirement indicates that the plugin will make use of another plugin's code, and specifies the version requirements
|
||||
on that plugin. The version is specified in terms of Semantic Versioning meaning that, to be compatible, the major
|
||||
versions must be identical and the minor version must be equal to or higher than the one provided. This requirement
|
||||
does not make use of any data from the configuration, even if it were provided, it is merely a functional check before
|
||||
running the plugin. To define the version of a plugin, populate the `_version` class variable as a tuple of version
|
||||
numbers `(major, minor, patch)`. So for example:
|
||||
|
||||
::
|
||||
|
||||
_version = (1, 0, 0)
|
||||
|
||||
The plugin may also require a specific version of the framework, and this also uses Semantic Versioning, and can be
|
||||
set by defining the `_required_framework_version`. The major version should match the version of volatility the plugin
|
||||
is to be used with, which at the time of writing would be 2.2.0, and so would be specified as below. If only features, for example,
|
||||
from 2.0.0 are used, then the lowest applicable version number should be used to support the greatest number of
|
||||
installations:
|
||||
|
||||
::
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
|
||||
Define the `run` method
|
||||
-----------------------
|
||||
|
||||
@@ -129,6 +178,7 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces.
|
||||
def run(self):
|
||||
|
||||
filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))
|
||||
kernel = self.context.modules[self.config['kernel']]
|
||||
|
||||
return renderers.TreeGrid([("PID", int),
|
||||
("Process", str),
|
||||
@@ -137,8 +187,8 @@ that will be output as part of the :py:class:`~volatility3.framework.interfaces.
|
||||
("Name", str),
|
||||
("Path", str)],
|
||||
self._generator(pslist.PsList.list_processes(self.context,
|
||||
self.config['primary'],
|
||||
self.config['nt_symbols'],
|
||||
kernel.layer_name,
|
||||
kernel.symbol_table_name,
|
||||
filter_func = filter_func)))
|
||||
|
||||
In this instance, the plugin constructs a filter (using the PsList plugin's *classmethod* for creating filters).
|
||||
@@ -157,7 +207,8 @@ the :py:class:`~volatility3.plugins.windows.pslist.PsList` plugin. That plugin
|
||||
so that other plugins can call it. As such, it takes all the necessary parameters rather than accessing them
|
||||
from a configuration. Since it must be portable code, it takes a context, as well as the layer name,
|
||||
symbol table and optionally a filter. In this instance we unconditionally
|
||||
pass it the values from the configuration for the ``primary`` and ``nt_symbols`` requirements. This will generate a list
|
||||
pass it the values from the configuration for the layer and symbol table from the kernel module object, constructed from
|
||||
the ``kernel`` configuration requirement. This will generate a list
|
||||
of :py:class:`~volatility3.framework.symbols.windows.extensions.EPROCESS` objects, as provided by the :py:class:`~volatility.plugins.windows.pslist.PsList` plugin,
|
||||
and is not covered here but is used as an example for how to share code across plugins
|
||||
(both as the provider and the consumer of the shared code).
|
||||
@@ -208,7 +259,7 @@ The plugin then takes the process's ``BaseDllName`` value, and calls :py:meth:`~
|
||||
as defined by the symbols, are directly accessible and use the case-style of the symbol library it came from (in Windows,
|
||||
attributes are CamelCase), such as ``entry.BaseDllName`` in this instance. Any attributes not defined by the symbol but added
|
||||
by Volatility extensions cannot be properties (in case they overlap with the attributes defined in the symbol libraries)
|
||||
and are therefore always methods and pretended with ``get_``, in this example ``BaseDllName.get_string()``.
|
||||
and are therefore always methods and prepended with ``get_``, in this example ``BaseDllName.get_string()``.
|
||||
|
||||
Finally, ``FullDllName`` is populated. These operations read from memory, and as such, the memory image may be unable to
|
||||
read the data at a particular offset. This will cause an exception to be thrown. In Volatility 3, exceptions are thrown
|
||||
|
||||
@@ -12,20 +12,22 @@ Volatility will automatically decompress them on use. It will also cache their
|
||||
under the user's home directory, in :file:`.cache/volatility3`, along with other useful data. The cache directory currently
|
||||
cannot be altered.
|
||||
|
||||
Symbol table JSON files live, by default, under the :file:`volatility3/symbols`, underneath an operating system directory
|
||||
(currently one of :file:`windows`, :file:`mac` or :file:`linux`). The symbols directory is configurable within the framework and can
|
||||
usually be set within the user interface.
|
||||
Symbol table JSON files live, by default, under the :file:`volatility3/symbols` directory. The symbols directory is
|
||||
configurable within the framework and can usually be set within the user interface.
|
||||
|
||||
These files can also be compressed into ZIP files, which Volatility will process in order to locate symbol files.
|
||||
The ZIP file must be named after the appropriate operating system (such as `linux.zip`, `mac.zip` or `windows.zip`).
|
||||
Inside the ZIP file, the directory structure should match the uncompressed operating system directory.
|
||||
|
||||
Volatility maintains a cache mapping the appropriate identifier for each symbol file against its filename. This cache
|
||||
is updated by automagic called as part of the standard automagic that's run each time a plugin is run. If a large number of new
|
||||
symbols file are detected, this may take some time, but can be safely interrupted and restarted and will not need to run again
|
||||
as long as the symbol files stay in the same location.
|
||||
|
||||
Windows symbol tables
|
||||
---------------------
|
||||
|
||||
For Windows systems, Volatility accepts a string made up of the GUID and Age of the required PDB file. It then
|
||||
searches all files under the configured symbol directories under the windows subdirectory. Any that match the filename
|
||||
pattern of :file:`<pdb-name>/<GUID>-<AGE>.json` (or any compressed variant) will be used. If such a symbol table cannot be found, then
|
||||
searches all files under the configured symbol directories under the windows subdirectory. Any that contain metadata
|
||||
which matches the pdb name and GUID/age (or any compressed variant) will be used. If such a symbol table cannot be found, then
|
||||
the associated PDB file will be downloaded from Microsoft's Symbol Server and converted into the appropriate JSON
|
||||
format, and will be saved in the correct location.
|
||||
|
||||
@@ -38,14 +40,13 @@ following command:
|
||||
The :envvar:`PYTHONPATH` environment variable is not required if the Volatility library is installed in the system's library path
|
||||
or a virtual environment.
|
||||
|
||||
Mac/Linux symbol tables
|
||||
-----------------------
|
||||
Mac or Linux symbol tables
|
||||
--------------------------
|
||||
|
||||
For Mac/Linux systems, both use the same mechanism for identification. JSON files live under the symbol directories,
|
||||
under either the :file:`linux` or :file:`mac` directories. The generated files contain an identifying string (the operating system
|
||||
For Mac/Linux systems, both use the same mechanism for identification. The generated files contain an identifying string (the operating system
|
||||
banner), which Volatility's automagic can detect. Volatility caches the mapping between the strings and the symbol
|
||||
tables they come from, meaning the precise file names don't matter and can be organized under any necessary hierarchy
|
||||
under the operating system directory.
|
||||
under the symbols directory.
|
||||
|
||||
Linux and Mac symbol tables can be generated from a DWARF file using a tool called `dwarf2json <https://github.com/volatilityfoundation/dwarf2json>`_.
|
||||
Currently a kernel with debugging symbols is the only suitable means for recovering all the information required by
|
||||
@@ -93,4 +94,4 @@ file, the banners must match exactly (down to the compilation date).
|
||||
|
||||
* Copy the `.json` file to the symbols directory into `[symbols directory]/linux`
|
||||
|
||||
* For Mac change `linux` to `mac`
|
||||
* For Mac change `linux` to `mac`
|
||||
|
||||
@@ -54,6 +54,12 @@ also be included, which can be found in `volatility3.constants.PLUGINS_PATH`.
|
||||
volatility3.plugins.__path__ = <new_plugin_path> + constants.PLUGINS_PATH
|
||||
failures = framework.import_files(volatility3.plugins, True)
|
||||
|
||||
.. note::
|
||||
|
||||
Volatility uses the `volatility3.plugins` namespace for all plugins (including those in `volatility3.framework.plugins`).
|
||||
Please ensure you only use `volatility3.plugins` and only ever import plugins from this namespace.
|
||||
This ensures the ability of users to override core plugins without needing write access to the framework directory.
|
||||
|
||||
Once the plugins have been imported, we can interrogate which plugins are available. The
|
||||
:py:func:`~volatility3.framework.list_plugins` call will
|
||||
return a dictionary of plugin names and the plugin classes.
|
||||
@@ -67,9 +73,10 @@ return a dictionary of plugin names and the plugin classes.
|
||||
Determine what configuration options a plugin requires
|
||||
------------------------------------------------------
|
||||
|
||||
For each plugin class, we can call the classmethod `requirements` on it, which will return a list of objects that
|
||||
adhere to the :py:class:`~volatility3.framework.interfaces.configuration.RequirementInterface` method. The various
|
||||
types of Requirement are split roughly in two,
|
||||
For each plugin class, we can call the classmethod
|
||||
:py:func:`~volatility3.framework.interfaces.configuration.ConfigurableInterface.get_requirements` on it, which will
|
||||
return a list of objects that adhere to the :py:class:`~volatility3.framework.interfaces.configuration.RequirementInterface`
|
||||
method. The various types of Requirement are split roughly in two,
|
||||
:py:class:`~volatility3.framework.interfaces.configuration.SimpleTypeRequirement` (such as integers, booleans, floats
|
||||
and strings) and more complex requirements (such as lists, choices, multiple requirements, translation layer
|
||||
requirements or symbol table requirements). A requirement just specifies a type of data and a name, and must be
|
||||
|
||||
+23
-1
@@ -9,7 +9,11 @@ Synopsis
|
||||
**volatility** [-h] [-c CONFIG] [--parallelism [{processes,threads,off}]]
|
||||
[-e EXTEND] [-p PLUGIN_DIRS] [-s SYMBOL_DIRS] [-v] [-l LOG]
|
||||
[-o OUTPUT_DIR] [-q] [-r RENDERER] [-f FILE]
|
||||
[--write-config] [--single-location SINGLE_LOCATION]
|
||||
[--write-config] [--save-config SAVE_CONFIG]
|
||||
[--clear-cache] [--cache-path CACHE_PATH]
|
||||
[--offline]
|
||||
[--single-location SINGLE_LOCATION]
|
||||
[--stackers [STACKERS ...]]
|
||||
[--single-swap-locations SINGLE_SWAP_LOCATIONS]
|
||||
<plugin> ...
|
||||
|
||||
@@ -98,6 +102,10 @@ Options
|
||||
attempt to build upon, and can be considered the input for the program.
|
||||
|
||||
--write-config
|
||||
*Deprecated*
|
||||
Use of `--write-config` has been deprecated, replaced by `--save-config`
|
||||
|
||||
--save-config
|
||||
This flag specifies that volatility should write or overwrite a file
|
||||
called config.json in the current directory. The file will contain
|
||||
the necessary JSON configuration to recreate the environment that the
|
||||
@@ -105,11 +113,25 @@ Options
|
||||
other plugins, but there's no guarantee that plugins use the same
|
||||
configuration options.
|
||||
|
||||
--clear-cache
|
||||
Clears out all short-term cached items.
|
||||
|
||||
--cache-path
|
||||
Change the default path used to store the cache.
|
||||
|
||||
--offline
|
||||
Do not search online for additional JSON files.
|
||||
Run offline mode (defaults to false) and for
|
||||
remote windows symbol tables, linux/mac banner repositories.
|
||||
|
||||
--single-location SINGLE_LOCATION
|
||||
This specifies a URL which will be downloaded if necessary, and built
|
||||
upon by the automagic and, since most plugins require a single memory
|
||||
image, can be considered the input for the program.
|
||||
|
||||
--stackers STACKERS
|
||||
Creates the list of stackers to use based on the config option.
|
||||
|
||||
--single-swap-locations SINGLE_SWAP_LOCATIONS
|
||||
A comma-separated list of swap files to be considered as part of the
|
||||
memory image specified by the single-location or file parameters.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# The following packages are required for core functionality.
|
||||
pefile>=2023.2.7
|
||||
|
||||
# The following packages are optional.
|
||||
# If certain packages are not necessary, place a comment (#) at the start of the line.
|
||||
|
||||
# This is required for the yara plugins
|
||||
yara-python>=3.8.0
|
||||
|
||||
# This is required for several plugins that perform malware analysis and disassemble code.
|
||||
# It can also improve accuracy of Windows 8 and later memory samples.
|
||||
capstone>=3.0.5
|
||||
|
||||
# This is required by plugins that decrypt passwords, password hashes, etc.
|
||||
pycryptodome
|
||||
|
||||
# This can improve error messages regarding improperly configured ISF files,
|
||||
# but is only recommended for development
|
||||
jsonschema>=2.3.0
|
||||
|
||||
# This is required for memory acquisition via leechcore/pcileech.
|
||||
leechcorepyc>=2.4.0
|
||||
@@ -1,2 +1,2 @@
|
||||
# These packages are required for core functionality.
|
||||
pefile>=2017.8.1 #foo
|
||||
pefile>=2023.2.7 #foo
|
||||
+4
-7
@@ -1,5 +1,5 @@
|
||||
# The following packages are required for core functionality.
|
||||
pefile>=2017.8.1
|
||||
pefile>=2023.2.7
|
||||
|
||||
# The following packages are optional.
|
||||
# If certain packages are not necessary, place a comment (#) at the start of the line.
|
||||
@@ -14,12 +14,9 @@ capstone>=3.0.5
|
||||
# This is required by plugins that decrypt passwords, password hashes, etc.
|
||||
pycryptodome
|
||||
|
||||
# This can improve error messages regarding improperly configured ISF files.
|
||||
jsonschema>=2.3.0
|
||||
|
||||
# This is required for memory acquisition via leechcore/pcileech.
|
||||
leechcorepyc>=2.4.0
|
||||
|
||||
# This is required for analyzing Linux samples compressed using AVMLs native
|
||||
# compression format. It is not required for AVML's standard LiME compression.
|
||||
python-snappy==0.6.0
|
||||
# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage
|
||||
gcsfs>=2023.1.0
|
||||
s3fs>=2023.1.0
|
||||
@@ -6,9 +6,10 @@ 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:
|
||||
@@ -19,32 +20,34 @@ 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_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",
|
||||
},
|
||||
packages=setuptools.find_namespace_packages(
|
||||
include=["volatility3", "volatility3.*"]
|
||||
),
|
||||
package_dir={"volatility3": "volatility3"},
|
||||
python_requires=">=3.7.0",
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"vol = volatility3.cli:main",
|
||||
"volshell = volatility3.cli.volshell:main",
|
||||
],
|
||||
},
|
||||
install_requires=get_install_requires(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Volatility 3 Testing Framework
|
||||
|
||||
## Requirements
|
||||
|
||||
The Volatility 3 Testing Framework requires the same version of Python as Volatility3 itself. To install the current set of dependencies that the framework requires, use a command like this:
|
||||
|
||||
```shell
|
||||
pip3 install -r requirements-testing.txt
|
||||
```
|
||||
|
||||
NOTE: `requirements-testing.txt` can be found in this current `test/` directory.
|
||||
|
||||
## Quick Start: Manual Testing
|
||||
|
||||
1. To test Volatility 3 on an image, first download one with a command such as:
|
||||
|
||||
```shell
|
||||
curl -sLO "https://downloads.volatilityfoundation.org/volatility3/images/win-xp-laptop-2005-06-25.img.gz"
|
||||
gunzip win-xp-laptop-2005-06-25.img.gz
|
||||
```
|
||||
|
||||
2. In many cases, more symbols are required to be downloaded to the `./volatility3/symbols` directory.
|
||||
|
||||
3. To manually run the tests, run a command, such as:
|
||||
|
||||
```shell
|
||||
py.test ./test/test_volatility.py --volatility=vol.py --image win-xp-laptop-2005-06-25.img -k test_windows
|
||||
```
|
||||
|
||||
The above command runs all available tests for windows on the `win-xp-laptop-2005-06-25.img` image. To choose a more specific set of tests, change the phrase after `-k` in this command.
|
||||
|
||||
## Github Actions
|
||||
|
||||
This framework currently tests two images (one linux image and one windows image) after every push on any branch. For more information/context, find the actions setup in `./github/workflows/test.yaml`
|
||||
@@ -0,0 +1,59 @@
|
||||
# This file is used to augment the test configuration
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--volatility",
|
||||
action="store",
|
||||
default=None,
|
||||
required=True,
|
||||
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(
|
||||
"--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",
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
# 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]
|
||||
)
|
||||
|
||||
|
||||
# Fixtures
|
||||
@pytest.fixture
|
||||
def volatility(request):
|
||||
return request.config.getoption("--volatility")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def python(request):
|
||||
return request.config.getoption("--python")
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"windows_dumpfiles": {
|
||||
"win-xp-laptop-2005-06-25.img": {
|
||||
"0x82220e78": [
|
||||
"9bdd5532286f1660f3778e68bc36efe6",
|
||||
"e3bc1e9e7370e3b5a661ebe591ecf4ec"
|
||||
],
|
||||
"0x82350bf8": [
|
||||
"e5c5e8d97b6280745b41f6572c85d1f0",
|
||||
"8589f1463422884dbf1411aaad278465"
|
||||
],
|
||||
"0x81eaf418": [
|
||||
"f7a1ae2060a58f8470b97affdb46dccf",
|
||||
"54fd611021fa784912530b8007545986"
|
||||
],
|
||||
"0x820588e8": "458efbc8fdb859488a6ab2b200cce809"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# These packages are required for core functionality.
|
||||
pefile>=2017.8.1 #foo
|
||||
|
||||
# The following packages are optional.
|
||||
# If certain packages are not necessary, place a comment (#) at the start of the line.
|
||||
|
||||
# This is required for the yara plugins
|
||||
yara-python>=3.8.0
|
||||
|
||||
pytest>=7.0.0
|
||||
@@ -0,0 +1,476 @@
|
||||
# volatility3 tests
|
||||
#
|
||||
|
||||
#
|
||||
# IMPORTS
|
||||
#
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import hashlib
|
||||
import ntpath
|
||||
import json
|
||||
|
||||
#
|
||||
# HELPER FUNCTIONS
|
||||
#
|
||||
|
||||
|
||||
def runvol(args, volatility, python):
|
||||
volpy = volatility
|
||||
python_cmd = python
|
||||
|
||||
cmd = [python_cmd, volpy] + args
|
||||
print(" ".join(cmd))
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
print("stdout:")
|
||||
sys.stdout.write(str(stdout))
|
||||
print("")
|
||||
print("stderr:")
|
||||
sys.stdout.write(str(stderr))
|
||||
print("")
|
||||
|
||||
return p.returncode, stdout, stderr
|
||||
|
||||
|
||||
def runvol_plugin(plugin, img, volatility, python, pluginargs=[], globalargs=[]):
|
||||
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()
|
||||
assert out.find(b"system") != -1
|
||||
assert out.find(b"csrss.exe") != -1
|
||||
assert out.find(b"svchost.exe") != -1
|
||||
assert out.count(b"\n") > 10
|
||||
assert rc == 0
|
||||
|
||||
rc, out, err = runvol_plugin(
|
||||
"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()
|
||||
assert out.find(b"system") != -1
|
||||
assert out.find(b"csrss.exe") != -1
|
||||
assert out.find(b"svchost.exe") != -1
|
||||
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
|
||||
)
|
||||
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.count(b"\n") > 10
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_windows_dumpfiles(image, volatility, python):
|
||||
|
||||
with open("./test/known_files.json") as json_file:
|
||||
known_files = json.load(json_file)
|
||||
|
||||
failed_chksms = 0
|
||||
|
||||
if sys.platform == "win32":
|
||||
file_name = ntpath.basename(image)
|
||||
else:
|
||||
file_name = os.path.basename(image)
|
||||
|
||||
try:
|
||||
for addr in known_files["windows_dumpfiles"][file_name]:
|
||||
|
||||
path = tempfile.mkdtemp()
|
||||
|
||||
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]
|
||||
):
|
||||
failed_chksms += 1
|
||||
|
||||
shutil.rmtree(path)
|
||||
|
||||
json_file.close()
|
||||
|
||||
assert failed_chksms == 0
|
||||
assert rc == 0
|
||||
except Exception as e:
|
||||
json_file.close()
|
||||
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"]
|
||||
)
|
||||
|
||||
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\\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)
|
||||
|
||||
assert out.find(b"Microsoft ACPI Driver") != -1
|
||||
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"]
|
||||
)
|
||||
|
||||
assert out.find(b"SeCreateTokenPrivilege") != -1
|
||||
assert out.find(b"SeCreateGlobalPrivilege") != -1
|
||||
assert out.find(b"SeAssignPrimaryTokenPrivilege") != -1
|
||||
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"]
|
||||
)
|
||||
|
||||
assert out.find(b"Local System") != -1
|
||||
assert out.find(b"Administrators") != -1
|
||||
assert out.find(b"Everyone") != -1
|
||||
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)
|
||||
|
||||
assert out.find(b"PATH") != -1
|
||||
assert out.find(b"PROCESSOR_ARCHITECTURE") != -1
|
||||
assert out.find(b"USERNAME") != -1
|
||||
assert out.find(b"SystemRoot") != -1
|
||||
assert out.find(b"CommonProgramFiles") != -1
|
||||
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
|
||||
)
|
||||
|
||||
assert out.find(b"PspCreateProcessNotifyRoutine") != -1
|
||||
assert out.find(b"KeBugCheckCallbackListHead") != -1
|
||||
assert out.find(b"KeBugCheckReasonCallbackListHead") != -1
|
||||
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
|
||||
)
|
||||
|
||||
assert out.find(b"DEV") != -1
|
||||
assert out.find(b"DRV") != -1
|
||||
assert out.find(b"ATT") != -1
|
||||
assert out.find(b"FILE_DEVICE_CONTROLLER") != -1
|
||||
assert out.find(b"FILE_DEVICE_DISK") != -1
|
||||
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"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()
|
||||
|
||||
assert out.count(b"__kernel__") >= 10
|
||||
assert out.count(b"\n") > 10
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_check_syscall(image, volatility, python):
|
||||
rc, out, err = runvol_plugin(
|
||||
"linux.check_syscall.Check_syscall", image, volatility, python
|
||||
)
|
||||
out = out.lower()
|
||||
|
||||
assert out.find(b"sys_close") != -1
|
||||
assert out.find(b"sys_open") != -1
|
||||
assert out.count(b"\n") > 100
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_lsmod(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("linux.lsmod.Lsmod", image, volatility, python)
|
||||
out = out.lower()
|
||||
|
||||
assert out.count(b"\n") > 10
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_lsof(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("linux.lsof.Lsof", image, volatility, python)
|
||||
out = out.lower()
|
||||
|
||||
assert out.count(b"socket:") >= 10
|
||||
assert out.count(b"\n") > 35
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_proc_maps(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("linux.proc.Maps", image, volatility, python)
|
||||
out = out.lower()
|
||||
|
||||
assert out.count(b"anonymous mapping") >= 10
|
||||
assert out.count(b"\n") > 100
|
||||
assert rc == 0
|
||||
|
||||
|
||||
def test_linux_tty_check(image, volatility, python):
|
||||
rc, out, err = runvol_plugin("linux.tty_check.tty_check", image, volatility, python)
|
||||
out = out.lower()
|
||||
|
||||
assert out.find(b"__kernel__") != -1
|
||||
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.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
|
||||
)
|
||||
out = out.lower()
|
||||
|
||||
assert out.find(b"chmod") != -1
|
||||
assert out.find(b"chown") != -1
|
||||
assert out.find(b"nosys") != -1
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
||||
assert out.find(b"127.0.0.1") != -1
|
||||
assert out.find(b"false") != -1
|
||||
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()
|
||||
|
||||
assert out.find(b"com.apple") != -1
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
assert out.find(b"/dev") != -1
|
||||
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)
|
||||
|
||||
assert out.find(b"TCP") != -1
|
||||
assert out.find(b"UDP") != -1
|
||||
assert out.find(b"UNIX") != -1
|
||||
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()
|
||||
|
||||
assert out.find(b"[heap]") != -1
|
||||
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()
|
||||
|
||||
assert out.find(b"executable_path") != -1
|
||||
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
|
||||
)
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
assert out.count(b"\n") > 10
|
||||
assert rc == 0
|
||||
@@ -6,5 +6,5 @@
|
||||
|
||||
import volatility3.cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
volatility3.cli.main()
|
||||
|
||||
@@ -26,7 +26,7 @@ except ImportError:
|
||||
|
||||
# Volatility must be findable in sys.path in order for collect_submodules to work
|
||||
# This adds the current working directory, which should usually do the trick
|
||||
sys.path.append(os.getcwd())
|
||||
sys.path.append(os.path.dirname(os.path.abspath(SPEC)))
|
||||
|
||||
vol_analysis = Analysis(['vol.py'],
|
||||
pathex = [],
|
||||
|
||||
+10
-5
@@ -32,14 +32,19 @@ 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 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
|
||||
if inspect.stack()[-2].function != 'walk_packages':
|
||||
warning = f"Import {fullname}: 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 not in [
|
||||
"walk_packages",
|
||||
"_collect_submodules",
|
||||
] and inspect.stack()[-3].function not in ["_collect_submodules"]:
|
||||
raise Warning(warning)
|
||||
|
||||
|
||||
|
||||
+345
-191
@@ -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,98 +98,147 @@ 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)
|
||||
]
|
||||
)
|
||||
|
||||
# Load up system defaults
|
||||
delayed_logs, default_config = self.load_system_defaults('vol.json')
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
parser.set_defaults(**default_config)
|
||||
|
||||
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
|
||||
# processed the plugin choice or had the plugin subparser added.
|
||||
known_args = [arg for arg in sys.argv if arg != '--help' and arg != '-h']
|
||||
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
|
||||
@@ -193,8 +250,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")
|
||||
@@ -210,23 +269,25 @@ class CommandLine:
|
||||
|
||||
### Alter constants if necessary
|
||||
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
|
||||
|
||||
|
||||
vollog.info(f"Volatility plugins path: {volatility3.plugins.__path__}")
|
||||
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
|
||||
@@ -239,11 +300,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)
|
||||
|
||||
@@ -258,13 +322,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])
|
||||
|
||||
###
|
||||
@@ -277,12 +346,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
|
||||
@@ -290,8 +363,10 @@ class CommandLine:
|
||||
###
|
||||
if args.file:
|
||||
try:
|
||||
single_location = self.location_from_file(args.file)
|
||||
ctx.config['automagic.LayerStacker.single_location'] = single_location
|
||||
single_location = requirements.URIRequirement.location_from_file(
|
||||
args.file
|
||||
)
|
||||
ctx.config["automagic.LayerStacker.single_location"] = single_location
|
||||
except ValueError as excp:
|
||||
parser.error(str(excp))
|
||||
|
||||
@@ -299,26 +374,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
|
||||
|
||||
###
|
||||
@@ -330,28 +416,46 @@ 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
|
||||
if constructed:
|
||||
renderers[args.renderer]().render(constructed.run())
|
||||
except (exceptions.VolatilityException) as excp:
|
||||
except exceptions.VolatilityException as excp:
|
||||
self.process_exceptions(excp)
|
||||
|
||||
@classmethod
|
||||
@@ -364,17 +468,10 @@ class CommandLine:
|
||||
Returns:
|
||||
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':
|
||||
if not os.path.exists(request.url2pathname(single_location.path)):
|
||||
filename = request.url2pathname(single_location.path)
|
||||
if not filename:
|
||||
raise ValueError("File URL looks incorrect (potentially missing /)")
|
||||
raise ValueError(f"File does not exist: {filename}")
|
||||
return parse.urlunparse(single_location)
|
||||
vollog.debug(
|
||||
f"{__name__}.location_from_file has been deprecated and moved to requirements.URIRequirement.location_from_file"
|
||||
)
|
||||
return requirements.URIRequirement.location_from_file(filename)
|
||||
|
||||
def load_system_defaults(self, filename: str) -> Tuple[List[Tuple[int, str]], Dict[str, Any]]:
|
||||
"""Modify the main configuration based on the default configuration override"""
|
||||
@@ -409,7 +506,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):
|
||||
@@ -418,22 +515,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:"
|
||||
@@ -447,22 +546,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
|
||||
@@ -482,27 +587,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
|
||||
@@ -524,34 +645,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
|
||||
@@ -561,7 +690,6 @@ class CommandLine:
|
||||
return output_filename
|
||||
|
||||
class CLIMemFileHandler(io.BytesIO, CLIFileHandler):
|
||||
|
||||
def __init__(self, filename: str):
|
||||
io.BytesIO.__init__(self)
|
||||
CLIFileHandler.__init__(self, filename)
|
||||
@@ -569,7 +697,7 @@ class CommandLine:
|
||||
def close(self):
|
||||
# Don't overcommit
|
||||
if self.closed:
|
||||
return
|
||||
return None
|
||||
|
||||
self.seek(0)
|
||||
|
||||
@@ -578,18 +706,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):
|
||||
@@ -611,7 +747,7 @@ class CommandLine:
|
||||
"""Closes and commits the file (by moving the temporary file to the correct name"""
|
||||
# Don't overcommit
|
||||
if self._file.closed:
|
||||
return
|
||||
return None
|
||||
|
||||
self._file.close()
|
||||
output_filename = self._get_final_filename()
|
||||
@@ -622,8 +758,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:
|
||||
@@ -631,15 +770,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):
|
||||
@@ -648,21 +794,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():
|
||||
|
||||
@@ -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,32 @@ 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)
|
||||
writer = csv.DictWriter(
|
||||
outfd, header_list, lineterminator="\n", escapechar="\\"
|
||||
)
|
||||
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 +291,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,33 +327,57 @@ 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"
|
||||
|
||||
column_titles = [""] + [column.name for column in grid.columns]
|
||||
outfd.write(format_string.format(*column_titles))
|
||||
for (depth, line) in final_output:
|
||||
for depth, line in final_output:
|
||||
nums_line = max([len(line[column]) for column in line])
|
||||
for column in line:
|
||||
line[column] = line[column] + ([""] * (nums_line - len(line[column])))
|
||||
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 +389,15 @@ 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 +405,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 +443,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")
|
||||
|
||||
@@ -24,13 +24,14 @@ 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:
|
||||
|
||||
parser_name = ''
|
||||
def __call__(
|
||||
self,
|
||||
parser: argparse.ArgumentParser,
|
||||
namespace: argparse.Namespace,
|
||||
values: Union[str, Sequence[Any], None],
|
||||
option_string: Optional[str] = None,
|
||||
) -> None:
|
||||
parser_name = ""
|
||||
arg_strings = [] # type: List[str]
|
||||
if values is not None:
|
||||
for value in values:
|
||||
@@ -43,7 +44,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 +55,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 +74,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 +82,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,7 +28,7 @@ vollog = logging.getLogger()
|
||||
vollog.setLevel(0)
|
||||
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)
|
||||
vollog.addHandler(console)
|
||||
@@ -38,107 +45,157 @@ 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)
|
||||
|
||||
# Load up system defaults
|
||||
delayed_logs, default_config = self.load_system_defaults('volshell.json')
|
||||
|
||||
parser = argparse.ArgumentParser(prog = self.CLI_NAME,
|
||||
description = "A tool for interactivate forensic analysis of memory images")
|
||||
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("--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.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("-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,
|
||||
)
|
||||
parser.add_argument("--offline",
|
||||
help = "Do not search online for additional JSON files",
|
||||
default = False,
|
||||
action = 'store_true')
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
parser.set_defaults(**default_config)
|
||||
|
||||
# We have to filter out help, otherwise parse_known_args will trigger the help message before having
|
||||
# processed the plugin choice or had the plugin subparser added.
|
||||
known_args = [arg for arg in sys.argv if arg != '--help' and arg != '-h']
|
||||
known_args = [arg for arg in sys.argv if arg != "--help" and arg != "-h"]
|
||||
partial_args, _ = parser.parse_known_args(known_args)
|
||||
|
||||
### Start up logging
|
||||
if partial_args.log:
|
||||
file_logger = logging.FileHandler(partial_args.log)
|
||||
file_logger.setLevel(1)
|
||||
file_formatter = logging.Formatter(datefmt = '%y-%m-%d %H:%M:%S',
|
||||
fmt = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
|
||||
file_logger.setLevel(0)
|
||||
file_formatter = logging.Formatter(
|
||||
datefmt="%y-%m-%d %H:%M:%S",
|
||||
fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
|
||||
)
|
||||
file_logger.setFormatter(file_formatter)
|
||||
rootlog.addHandler(file_logger)
|
||||
vollog.addHandler(file_logger)
|
||||
vollog.info("Logging started")
|
||||
|
||||
if partial_args.verbosity < 3:
|
||||
if partial_args.verbosity < 1:
|
||||
sys.tracebacklimit = None
|
||||
console.setLevel(30 - (partial_args.verbosity * 10))
|
||||
else:
|
||||
console.setLevel(10 - (partial_args.verbosity - 2))
|
||||
@@ -148,12 +205,14 @@ class VolShell(cli.CommandLine):
|
||||
|
||||
### Alter constants if necessary
|
||||
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
|
||||
@@ -161,7 +220,6 @@ class VolShell(cli.CommandLine):
|
||||
vollog.info(f"Volatility plugins path: {volatility3.plugins.__path__}")
|
||||
vollog.info(f"Volatility symbols path: {volatility3.symbols.__path__}")
|
||||
|
||||
|
||||
if partial_args.clear_cache:
|
||||
framework.clear_cache()
|
||||
|
||||
@@ -170,11 +228,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)
|
||||
|
||||
@@ -192,11 +253,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]
|
||||
|
||||
@@ -208,7 +275,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:
|
||||
@@ -219,7 +288,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
|
||||
@@ -228,7 +299,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))
|
||||
|
||||
@@ -236,15 +307,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
|
||||
@@ -259,22 +337,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
|
||||
@@ -282,7 +378,6 @@ class VolShell(cli.CommandLine):
|
||||
constructed.run()
|
||||
except exceptions.VolatilityException as excp:
|
||||
self.process_exceptions(excp)
|
||||
parser.exit(1, f"Unable to validate the plugin requirements: {[x for x in excp.unsatisfied]}\n")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+215
-108
@@ -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)
|
||||
|
||||
@@ -100,7 +108,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
"""Describes the available commands"""
|
||||
if args:
|
||||
help(*args)
|
||||
return
|
||||
return None
|
||||
|
||||
variables = []
|
||||
print("\nMethods:")
|
||||
@@ -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,53 +266,66 @@ 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
|
||||
return None
|
||||
|
||||
if not isinstance(object, str):
|
||||
# Mypy requires us to order things this way
|
||||
@@ -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:
|
||||
@@ -370,7 +453,7 @@ class Volshell(interfaces.plugins.PluginInterface):
|
||||
"""Prints an alphabetical list of symbols for a symbol table"""
|
||||
if symbol_table is None:
|
||||
print("No symbol table provided")
|
||||
return
|
||||
return None
|
||||
longest_offset = longest_name = 0
|
||||
|
||||
table = self.context.symbol_space[symbol_table]
|
||||
@@ -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, encoding="utf-8").read(), 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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -29,9 +35,9 @@ class Volshell(generic.Volshell):
|
||||
process_layer = task.add_process_layer()
|
||||
if process_layer is not None:
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"Layer for task ID {pid} could not be constructed")
|
||||
return
|
||||
return None
|
||||
print(f"No task with task ID {pid} found")
|
||||
|
||||
def list_tasks(self):
|
||||
@@ -42,27 +48,31 @@ 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:
|
||||
object = self.config['vmlinux'] + constants.BANG + object
|
||||
object = self.current_symbol_table + constants.BANG + object
|
||||
return super().display_type(object, offset)
|
||||
|
||||
def display_symbols(self, symbol_table: str = None):
|
||||
"""Prints an alphabetical list of symbols for a symbol table"""
|
||||
if symbol_table is None:
|
||||
symbol_table = self.config['vmlinux']
|
||||
symbol_table = self.current_symbol_table
|
||||
return super().display_symbols(symbol_table)
|
||||
|
||||
@property
|
||||
|
||||
@@ -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:
|
||||
@@ -29,34 +35,40 @@ class Volshell(generic.Volshell):
|
||||
process_layer = task.add_process_layer()
|
||||
if process_layer is not None:
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"Layer for task ID {pid} could not be constructed")
|
||||
return
|
||||
return None
|
||||
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:
|
||||
object = self.config['darwin'] + constants.BANG + object
|
||||
object = self.current_symbol_table + constants.BANG + object
|
||||
return super().display_type(object, offset)
|
||||
|
||||
def display_symbols(self, symbol_table: str = None):
|
||||
|
||||
@@ -15,41 +15,53 @@ 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:
|
||||
if process.UniqueProcessId == pid:
|
||||
process_layer = process.add_process_layer()
|
||||
self.change_layer(process_layer)
|
||||
return
|
||||
return None
|
||||
print(f"No process with process ID {pid} found")
|
||||
|
||||
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:
|
||||
|
||||
@@ -7,16 +7,26 @@ 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
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from typing import Any, Dict, Generator, List, Tuple, Type, TypeVar
|
||||
|
||||
from volatility3.framework import constants, interfaces
|
||||
@@ -45,24 +55,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 +88,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 +97,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 +108,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 +121,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]:
|
||||
@@ -151,8 +184,14 @@ def import_file(module: str, path: str, ignore_errors: bool = False) -> List[str
|
||||
try:
|
||||
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(
|
||||
"".join(
|
||||
traceback.TracebackException.from_exception(e).format(chain=True)
|
||||
)
|
||||
)
|
||||
vollog.debug(
|
||||
"Failed to import module {} based on file: {}".format(module, path)
|
||||
)
|
||||
failures.append(module)
|
||||
if not ignore_errors:
|
||||
raise
|
||||
@@ -167,7 +206,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 +218,13 @@ 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'
|
||||
if not complete:
|
||||
glob_pattern = 'data_' + glob_pattern
|
||||
for cache_filename in glob.glob(os.path.join(constants.CACHE_PATH, glob_pattern)):
|
||||
os.unlink(cache_filename)
|
||||
def clear_cache(complete=False):
|
||||
try:
|
||||
os.unlink(os.path.join(constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME))
|
||||
except FileNotFoundError:
|
||||
vollog.log(constants.LOGLEVEL_VVVV, "Attempting to clear a non-existant cache")
|
||||
|
||||
@@ -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,59 @@ 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)
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional, Tuple, Type
|
||||
|
||||
from volatility3.framework import interfaces, constants
|
||||
from volatility3.framework import constants, interfaces
|
||||
from volatility3.framework.automagic import symbol_cache, symbol_finder
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import intel, scanners
|
||||
from volatility3.framework.symbols import linux
|
||||
|
||||
@@ -15,14 +17,26 @@ 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
|
||||
):
|
||||
vollog.info(
|
||||
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Bail out by default unless we can stack properly
|
||||
layer = context.layers[layer_name]
|
||||
join = interfaces.configuration.path_join
|
||||
@@ -32,61 +46,70 @@ class LinuxIntelStacker(interfaces.automagic.StackerLayerInterface):
|
||||
if isinstance(layer, intel.Intel):
|
||||
return None
|
||||
|
||||
linux_banners = LinuxBannerCache.load_banners()
|
||||
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)}")
|
||||
|
||||
symbol_files = linux_banners.get(banner, None)
|
||||
if symbol_files:
|
||||
if len(symbol_files) > 1:
|
||||
using = "*"
|
||||
vollog.warning(f"Multiple symbol files identified (using {using}):")
|
||||
for symbol_file in symbol_files:
|
||||
vollog.warning(f" {using} {symbol_file}")
|
||||
using = " "
|
||||
isf_path = symbol_files[0]
|
||||
table_name = context.symbol_space.free_table_name('LintelStacker')
|
||||
table = linux.LinuxKernelIntermedSymbols(context,
|
||||
'temporary.' + table_name,
|
||||
name = table_name,
|
||||
isf_url = isf_path)
|
||||
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,
|
||||
)
|
||||
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}")
|
||||
@@ -95,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
|
||||
@@ -142,25 +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
|
||||
|
||||
|
||||
class LinuxBannerCache(symbol_cache.SymbolBannerCache):
|
||||
"""Caches the banners found in the Linux symbol files."""
|
||||
|
||||
os = "linux"
|
||||
symbol_name = "linux_banner"
|
||||
banner_path = constants.LINUX_BANNERS_PATH
|
||||
exclusion_list = ['mac', 'windows']
|
||||
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"
|
||||
banner_cache = LinuxBannerCache
|
||||
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"]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from volatility3.framework import interfaces, constants, layers, exceptions
|
||||
from volatility3.framework import constants, exceptions, interfaces, layers
|
||||
from volatility3.framework.automagic import symbol_cache, symbol_finder
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import intel, scanners
|
||||
from volatility3.framework.symbols import mac
|
||||
|
||||
@@ -16,14 +18,26 @@ 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
|
||||
):
|
||||
vollog.info(
|
||||
f"SQLiteCache version not suitable: required {required} found {symbol_cache.SqliteCache.version}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Bail out by default unless we can stack properly
|
||||
layer = context.layers[layer_name]
|
||||
new_layer = None
|
||||
@@ -34,55 +48,76 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
|
||||
if isinstance(layer, intel.Intel):
|
||||
return None
|
||||
|
||||
mac_banners = MacBannerCache.load_banners()
|
||||
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)}")
|
||||
|
||||
symbol_files = mac_banners.get(banner, None)
|
||||
if symbol_files:
|
||||
isf_path = symbol_files[0]
|
||||
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)
|
||||
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,
|
||||
)
|
||||
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]
|
||||
@@ -90,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
|
||||
@@ -100,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}")
|
||||
@@ -115,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)]
|
||||
|
||||
@@ -145,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}")
|
||||
@@ -173,21 +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"
|
||||
|
||||
for offset in context.layers[layer_name].scan(scanner = scanners.RegExScanner(darwin_signature),
|
||||
context = context,
|
||||
progress_callback = progress_callback):
|
||||
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,
|
||||
):
|
||||
banner = context.layers[layer_name].read(offset, 128)
|
||||
|
||||
idx = banner.find(b"\x00")
|
||||
@@ -197,19 +260,11 @@ class MacIntelStacker(interfaces.automagic.StackerLayerInterface):
|
||||
yield offset, banner
|
||||
|
||||
|
||||
class MacBannerCache(symbol_cache.SymbolBannerCache):
|
||||
"""Caches the banners found in the Mac symbol files."""
|
||||
os = "mac"
|
||||
symbol_name = "version"
|
||||
banner_path = constants.MAC_BANNERS_PATH
|
||||
exclusion_list = ['windows', 'linux']
|
||||
|
||||
|
||||
class MacSymbolFinder(symbol_finder.SymbolFinder):
|
||||
"""Mac symbol loader based on uname signature strings."""
|
||||
|
||||
banner_config_key = 'kernel_banner'
|
||||
banner_cache = MacBannerCache
|
||||
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"]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
from volatility3.framework import interfaces, constants, configuration
|
||||
|
||||
|
||||
@@ -6,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)
|
||||
return
|
||||
self(
|
||||
context,
|
||||
new_config_path,
|
||||
requirement.requirements[req],
|
||||
progress_callback,
|
||||
)
|
||||
return None
|
||||
if not requirement.unsatisfied(context, config_path):
|
||||
return
|
||||
return None
|
||||
# The requirement is unfulfilled and is a ModuleRequirement
|
||||
|
||||
context.config[interfaces.configuration.path_join(
|
||||
new_config_path, 'class')] = 'volatility3.framework.contexts.Module'
|
||||
context.config[interfaces.configuration.path_join(new_config_path, "class")] = (
|
||||
"volatility3.framework.contexts.Module"
|
||||
)
|
||||
|
||||
for req in requirement.requirements:
|
||||
if requirement.requirements[req].unsatisfied(context, new_config_path) and req != 'offset':
|
||||
return
|
||||
if (
|
||||
requirement.requirements[req].unsatisfied(context, new_config_path)
|
||||
and req != "offset"
|
||||
):
|
||||
return None
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ from loaded PE files.
|
||||
This module contains a standalone scanner, and also a :class:`~volatility3.framework.interfaces.layers.ScannerInterface`
|
||||
based scanner for use within the framework by calling :func:`~volatility3.framework.interfaces.layers.DataLayerInterface.scan`.
|
||||
"""
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, Callable
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
||||
|
||||
from volatility3.framework import constants, exceptions, interfaces, layers
|
||||
from volatility3.framework.configuration import requirements
|
||||
@@ -21,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__)
|
||||
|
||||
@@ -42,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.
|
||||
|
||||
@@ -61,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`.
|
||||
|
||||
@@ -98,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`.
|
||||
@@ -126,69 +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)
|
||||
if valid_kernel != None:
|
||||
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
|
||||
@@ -199,104 +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
|
||||
try:
|
||||
if vlayer.read(address, 0x2) == b'MZ':
|
||||
with contextlib.suppress(exceptions.InvalidAddressException):
|
||||
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])
|
||||
except exceptions.InvalidAddressException:
|
||||
pass
|
||||
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.
|
||||
|
||||
@@ -325,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")
|
||||
|
||||
@@ -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,52 +96,79 @@ 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)
|
||||
context.config[appropriate_config_path] = top_layer_name
|
||||
return
|
||||
return None
|
||||
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"physical_layer maximum_address: {physical_layer.maximum_address}"
|
||||
)
|
||||
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 +192,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 +209,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 +240,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 +258,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 +273,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 +291,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 +302,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__)
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import base64
|
||||
import gc
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import sqlite3
|
||||
import urllib
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from typing import Dict, List, Optional
|
||||
from abc import abstractmethod
|
||||
from typing import Dict, Generator, Iterable, List, Optional, Tuple
|
||||
|
||||
from volatility3.framework import constants, exceptions, interfaces
|
||||
from volatility3 import framework, schemas
|
||||
from volatility3.framework import constants, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import resources
|
||||
from volatility3.framework.symbols import intermed
|
||||
|
||||
@@ -22,189 +24,539 @@ vollog = logging.getLogger(__name__)
|
||||
BannersType = Dict[bytes, List[str]]
|
||||
|
||||
|
||||
class SymbolBannerCache(interfaces.automagic.AutomagicInterface):
|
||||
"""Runs through all symbols tables and caches their banners."""
|
||||
### Identifiers
|
||||
|
||||
|
||||
class IdentifierProcessor:
|
||||
operating_system = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_identifier(cls, json) -> Optional[bytes]:
|
||||
"""Method to extract the identifier from a particular operating system's JSON
|
||||
|
||||
Returns:
|
||||
identifier is valid or None if not found
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"This base class has no get_identifier method defined"
|
||||
)
|
||||
|
||||
|
||||
class WindowsIdentifier(IdentifierProcessor):
|
||||
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", {})
|
||||
if windows_metadata:
|
||||
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")
|
||||
|
||||
|
||||
class MacIdentifier(IdentifierProcessor):
|
||||
operating_system = "mac"
|
||||
|
||||
@classmethod
|
||||
def get_identifier(cls, json) -> Optional[bytes]:
|
||||
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"
|
||||
|
||||
@classmethod
|
||||
def get_identifier(cls, json) -> Optional[bytes]:
|
||||
linux_banner = (
|
||||
json.get("symbols", {}).get("linux_banner", {}).get("constant_data", None)
|
||||
)
|
||||
if linux_banner:
|
||||
return base64.b64decode(linux_banner)
|
||||
return None
|
||||
|
||||
|
||||
### CacheManagers
|
||||
|
||||
|
||||
class CacheManagerInterface(interfaces.configuration.VersionableInterface):
|
||||
def __init__(self, filename: str):
|
||||
super().__init__()
|
||||
self._filename = filename
|
||||
self._classifiers = {}
|
||||
for subclazz in framework.class_subclasses(IdentifierProcessor):
|
||||
self._classifiers[subclazz.operating_system] = subclazz
|
||||
|
||||
def add_identifier(self, location: str, operating_system: str, identifier: str):
|
||||
"""Adds an identifier to the store"""
|
||||
pass
|
||||
|
||||
def find_location(
|
||||
self, identifier: bytes, operating_system: Optional[str]
|
||||
) -> Optional[str]:
|
||||
"""Returns the location of the symbol file given the identifier
|
||||
|
||||
Args:
|
||||
identifier: string that uniquely identifies a particular symbol table
|
||||
operating_system: optional string to restrict identifiers to just those for a particular operating system
|
||||
|
||||
Returns:
|
||||
The location of the symbols file that matches the identifier
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_local_locations(self) -> Iterable[str]:
|
||||
"""Returns a list of all the local locations"""
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
"""Locates all files under the symbol directories. Updates the cache with additions, modifications and removals.
|
||||
This also updates remote locations based on a cache timeout.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
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:
|
||||
operating_system: If set, limits responses to a specific operating system
|
||||
local_only: Returns only local locations
|
||||
|
||||
Returns:
|
||||
A dictionary of identifiers mapped to a location
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_identifier(self, location: str) -> Optional[bytes]:
|
||||
"""Returns an identifier based on a specific location or None"""
|
||||
pass
|
||||
|
||||
def get_identifiers(self, operating_system: Optional[str]) -> List[bytes]:
|
||||
"""Returns all identifiers for a particular operating system"""
|
||||
pass
|
||||
|
||||
def get_location_statistics(
|
||||
self, location: str
|
||||
) -> Optional[Tuple[int, int, int, int]]:
|
||||
"""Returns ISF statistics based on the location
|
||||
|
||||
Returns:
|
||||
A tuple of base_types, types, enums, symbols, or None is location not found
|
||||
"""
|
||||
|
||||
def get_hash(self, location: str) -> Optional[str]:
|
||||
"""Returns the hash of the JSON from within a location ISF"""
|
||||
|
||||
|
||||
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
|
||||
try:
|
||||
self._database = self._connect_storage(filename)
|
||||
except sqlite3.DatabaseError:
|
||||
os.unlink(filename)
|
||||
self._database = self._connect_storage(filename)
|
||||
|
||||
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()
|
||||
)
|
||||
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:
|
||||
# All good, so pass and move on
|
||||
pass
|
||||
else:
|
||||
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)"
|
||||
)
|
||||
database.commit()
|
||||
return database
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
identifier: string that uniquely identifies a particular symbol table
|
||||
operating_system: optional string to restrict identifiers to just those for a particular operating system
|
||||
|
||||
Returns:
|
||||
The location of the symbols file that matches the identifier or None
|
||||
"""
|
||||
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 = ?"
|
||||
parameters = (identifier, operating_system)
|
||||
results = self._database.cursor().execute(statement, parameters).fetchall()
|
||||
result = None
|
||||
for row in results:
|
||||
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()
|
||||
)
|
||||
for row in result:
|
||||
yield row["location"]
|
||||
|
||||
def is_url_local(self, url: str) -> bool:
|
||||
"""Determines whether an url is local or not"""
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
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()
|
||||
)
|
||||
for row in results:
|
||||
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()
|
||||
)
|
||||
for row in results:
|
||||
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()
|
||||
)
|
||||
for row in results:
|
||||
return row["hash"]
|
||||
return 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("")
|
||||
]
|
||||
)
|
||||
cached_locations = set(self.get_local_locations())
|
||||
|
||||
new_locations = on_disk_locations.difference(cached_locations)
|
||||
missing_locations = cached_locations.difference(on_disk_locations)
|
||||
|
||||
# Missing entries
|
||||
if missing_locations:
|
||||
self._database.cursor().execute(
|
||||
f"DELETE FROM cache WHERE location IN ({','.join(['?'] * len(missing_locations))})",
|
||||
[x for x in missing_locations],
|
||||
)
|
||||
self._database.commit()
|
||||
|
||||
cache_update = set()
|
||||
files_to_timestamp = on_disk_locations.intersection(cached_locations)
|
||||
if files_to_timestamp:
|
||||
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:
|
||||
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 and os.path.exists(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))
|
||||
|
||||
# New or not recently updated
|
||||
|
||||
files_to_process = new_locations.union(cache_update)
|
||||
number_files_to_process = len(files_to_process)
|
||||
cursor = self._database.cursor()
|
||||
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...",
|
||||
)
|
||||
try:
|
||||
with resources.ResourceAccessor().open(location) as fp:
|
||||
json_obj = json.load(fp)
|
||||
hash = schemas.create_json_hash(json_obj)
|
||||
identifier = None
|
||||
|
||||
# Get stats
|
||||
stats_base_types = len(json_obj.get("base_types", {}))
|
||||
stats_types = len(json_obj.get("user_types", {}))
|
||||
stats_enums = len(json_obj.get("enums", {}))
|
||||
stats_symbols = len(json_obj.get("symbols", {}))
|
||||
|
||||
operating_system = None
|
||||
for idextractor in idextractors:
|
||||
identifier = idextractor.get_identifier(json_obj)
|
||||
if identifier is not None:
|
||||
operating_system = idextractor.operating_system
|
||||
break
|
||||
|
||||
# We don't try to validate schemas here, we do that on first use
|
||||
# Store in database
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO cache (location, identifier, operating_system, hash,"
|
||||
"stats_base_types, stats_types, stats_enums, stats_symbols, "
|
||||
"local, cached) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))",
|
||||
(
|
||||
location,
|
||||
identifier,
|
||||
operating_system,
|
||||
hash,
|
||||
stats_base_types,
|
||||
stats_types,
|
||||
stats_enums,
|
||||
stats_symbols,
|
||||
self.is_url_local(location),
|
||||
),
|
||||
)
|
||||
if identifier is not None:
|
||||
vollog.log(
|
||||
constants.LOGLEVEL_VV,
|
||||
f"Identified {location} as {identifier}",
|
||||
)
|
||||
else:
|
||||
vollog.log(
|
||||
constants.LOGLEVEL_VVVV,
|
||||
f"No identifier found for {location}",
|
||||
)
|
||||
except Exception as excp:
|
||||
vollog.log(constants.LOGLEVEL_VVVV, excp)
|
||||
finally:
|
||||
self._database.commit()
|
||||
|
||||
# Remote Entries
|
||||
|
||||
if not constants.OFFLINE and constants.REMOTE_ISF_URL:
|
||||
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}')"
|
||||
)
|
||||
remote_identifiers = RemoteIdentifierFormat(constants.REMOTE_ISF_URL)
|
||||
progress_callback(50, "Reading remote ISF list")
|
||||
for operating_system in constants.OS_CATEGORIES:
|
||||
identifiers = remote_identifiers.process(
|
||||
{}, operating_system=operating_system
|
||||
)
|
||||
for identifier, location in identifiers:
|
||||
identifier = identifier.rstrip()
|
||||
identifier = (
|
||||
identifier[:-1] if identifier.endswith(b"\x00") else identifier
|
||||
) # Linux banners dumped by dwarf2json end with "\x00\n". If not stripped, the banner cannot match.
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO cache(identifier, location, operating_system, local, cached) VALUES (?, ?, ?, ?, datetime('now'))",
|
||||
(identifier, location, operating_system, False),
|
||||
)
|
||||
progress_callback(100, "Reading remote ISF list")
|
||||
self._database.commit()
|
||||
|
||||
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"
|
||||
if local_only:
|
||||
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"]:
|
||||
vollog.debug(
|
||||
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()
|
||||
)
|
||||
else:
|
||||
results = (
|
||||
self._database.cursor()
|
||||
.execute("SELECT identifier FROM cache")
|
||||
.fetchall()
|
||||
)
|
||||
output = []
|
||||
for row in results:
|
||||
output.append(row["identifier"])
|
||||
return output
|
||||
|
||||
|
||||
### Automagic
|
||||
|
||||
|
||||
class SymbolCacheMagic(interfaces.automagic.AutomagicInterface):
|
||||
"""Runs through all symbol tables and caches their identifiers"""
|
||||
|
||||
# Since this is necessary for ConstructionMagic, we set a lower priority
|
||||
# The user would run it eventually either way, but running it first means it can be used that run
|
||||
priority = 0
|
||||
|
||||
os: Optional[str] = None
|
||||
symbol_name: str = "banner_name"
|
||||
banner_path: Optional[str] = None
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
identifiers_path = os.path.join(
|
||||
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
|
||||
)
|
||||
self._cache = SqliteCache(identifiers_path)
|
||||
|
||||
@classmethod
|
||||
def load_banners(cls) -> BannersType:
|
||||
if not cls.banner_path:
|
||||
raise ValueError("Banner_path not appropriately set")
|
||||
banners: BannersType = {}
|
||||
if os.path.exists(cls.banner_path):
|
||||
with open(cls.banner_path, "rb") as f:
|
||||
# We use pickle over JSON because we're dealing with bytes objects
|
||||
banners.update(pickle.load(f))
|
||||
|
||||
# Remove possibilities that can't exist locally.
|
||||
remove_banners = []
|
||||
for banner in banners:
|
||||
for path in banners[banner]:
|
||||
url = urllib.parse.urlparse(path)
|
||||
if url.scheme == 'file' and not os.path.exists(urllib.request.url2pathname(url.path)):
|
||||
vollog.log(
|
||||
constants.LOGLEVEL_VV, "Removing cached path {} for banner {}: file does not exist".format(
|
||||
path, str(banner or b'', 'latin-1')))
|
||||
banners[banner].remove(path)
|
||||
# This is probably excessive, but it's here if we need it
|
||||
if url.scheme == 'jar':
|
||||
zip_file, zip_path = url.path.split("!")
|
||||
zip_file = urllib.parse.urlparse(zip_file).path
|
||||
if ((not os.path.exists(zip_file)) or (zip_path not in zipfile.ZipFile(zip_file).namelist())):
|
||||
vollog.log(constants.LOGLEVEL_VV,
|
||||
"Removing cached path {} for banner {}: file does not exist".format(path, banner))
|
||||
banners[banner].remove(path)
|
||||
|
||||
if not banners[banner]:
|
||||
remove_banners.append(banner)
|
||||
for remove_banner in remove_banners:
|
||||
del banners[remove_banner]
|
||||
return banners
|
||||
|
||||
@classmethod
|
||||
def save_banners(cls, banners):
|
||||
|
||||
with open(cls.banner_path, "wb") as f:
|
||||
pickle.dump(banners, f)
|
||||
|
||||
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."""
|
||||
|
||||
# Bomb out if we're just the generic interface
|
||||
if self.os is None:
|
||||
return
|
||||
|
||||
# We only need to be called once, so no recursion necessary
|
||||
banners = self.load_banners()
|
||||
|
||||
cacheables = self.find_new_banner_files(banners, self.os)
|
||||
|
||||
new_banners = self.read_new_banners(context, config_path, cacheables, self.symbol_name, self.os,
|
||||
progress_callback)
|
||||
|
||||
# Add in any new banners to the existing list
|
||||
for new_banner in new_banners:
|
||||
banner_list = banners.get(new_banner, [])
|
||||
banners[new_banner] = list(set(banner_list + new_banners[new_banner]))
|
||||
|
||||
# Do remote banners *after* the JSON loading, so that it doesn't pull down all the remote JSON
|
||||
self.remote_banners(banners, self.os)
|
||||
|
||||
# Rewrite the cached banners each run, since writing is faster than the banner_cache validation portion
|
||||
self.save_banners(banners)
|
||||
|
||||
if progress_callback is not None:
|
||||
progress_callback(100, f"Built {self.os} caches")
|
||||
self._cache.update(progress_callback)
|
||||
|
||||
@classmethod
|
||||
def read_new_banners(cls, context: interfaces.context.ContextInterface, config_path: str, new_urls: List[str],
|
||||
symbol_name: str, operating_system: str = None,
|
||||
progress_callback = None) -> Optional[Dict[bytes, List[str]]]:
|
||||
"""Reads the any new banners for the OS in question"""
|
||||
if operating_system is None:
|
||||
return None
|
||||
|
||||
banners = {}
|
||||
|
||||
total = len(new_urls)
|
||||
if total > 0:
|
||||
vollog.info(f"Building {operating_system} caches...")
|
||||
for current in range(total):
|
||||
if progress_callback is not None:
|
||||
progress_callback(current * 100 / total, f"Building {operating_system} caches")
|
||||
isf_url = new_urls[current]
|
||||
|
||||
isf = None
|
||||
try:
|
||||
# Loading the symbol table will be very slow until it's been validated
|
||||
isf = intermed.IntermediateSymbolTable(context, config_path, "temp", isf_url, validate = False)
|
||||
|
||||
# We should store the banner against the filename
|
||||
# We don't bother with the hash (it'll likely take too long to validate)
|
||||
# but we should check at least that the banner matches on load.
|
||||
banner = isf.get_symbol(symbol_name).constant_data
|
||||
vollog.log(constants.LOGLEVEL_VV, f"Caching banner {banner} for file {isf_url}")
|
||||
|
||||
bannerlist = banners.get(banner, [])
|
||||
bannerlist.append(isf_url)
|
||||
banners[banner] = bannerlist
|
||||
except exceptions.SymbolError:
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
vollog.log(constants.LOGLEVEL_VV, f"Caching file {isf_url} failed due to JSON error")
|
||||
finally:
|
||||
# Get rid of the loaded file, in case it sits in memory
|
||||
if isf:
|
||||
del isf
|
||||
gc.collect()
|
||||
return banners
|
||||
|
||||
@classmethod
|
||||
def find_new_banner_files(cls, banners: Dict[bytes, List[str]], operating_system: str) -> List[str]:
|
||||
"""Gathers all files and remove existing banners"""
|
||||
cacheables = list(intermed.IntermediateSymbolTable.file_symbol_url(operating_system))
|
||||
for banner in banners:
|
||||
for json_file in banners[banner]:
|
||||
if json_file in cacheables:
|
||||
cacheables.remove(json_file)
|
||||
return cacheables
|
||||
|
||||
@classmethod
|
||||
def remote_banners(cls, banners: Dict[bytes, List[str]], operating_system = None, banner_location = None):
|
||||
"""Adds remote URLs to the banner list"""
|
||||
if operating_system is None:
|
||||
return None
|
||||
|
||||
if banner_location is None:
|
||||
banner_location = constants.REMOTE_ISF_URL
|
||||
|
||||
if not constants.OFFLINE and banner_location is not None:
|
||||
try:
|
||||
rbf = RemoteBannerFormat(banner_location)
|
||||
rbf.process(banners, operating_system)
|
||||
except urllib.error.URLError:
|
||||
vollog.debug(f"Unable to download remote banner list from {banner_location}")
|
||||
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)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class RemoteBannerFormat:
|
||||
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 banner list format")
|
||||
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, banners: Dict[bytes, List[str]], operating_system: Optional[str]):
|
||||
raise ValueError("Banner List version not verified")
|
||||
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, banners: Dict[bytes, List[str]], operating_system: Optional[str]):
|
||||
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 banner in self._data[operating_system]:
|
||||
binary_banner = base64.b64decode(banner)
|
||||
file_list = banners.get(binary_banner, [])
|
||||
for value in self._data[operating_system][banner]:
|
||||
if value not in file_list:
|
||||
file_list = file_list + [value]
|
||||
banners[binary_banner] = file_list
|
||||
if 'additional' in self._data:
|
||||
for location in self._data['additional']:
|
||||
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"]:
|
||||
try:
|
||||
subrbf = RemoteBannerFormat(location)
|
||||
subrbf.process(banners, operating_system)
|
||||
subrbf = RemoteIdentifierFormat(location)
|
||||
yield from subrbf.process(identifiers, operating_system)
|
||||
except IOError:
|
||||
vollog.debug(f"Remote file not found: {location}")
|
||||
return banners
|
||||
return identifiers
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
from typing import Any, Iterable, List, Tuple, Type, Optional, Callable
|
||||
import os
|
||||
from typing import Any, Callable, Iterable, List, Optional, Tuple
|
||||
|
||||
from volatility3.framework import interfaces, constants, layers
|
||||
from volatility3.framework import constants, interfaces, layers
|
||||
from volatility3.framework.automagic import symbol_cache
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import scanners
|
||||
@@ -15,73 +16,111 @@ vollog = logging.getLogger(__name__)
|
||||
|
||||
class SymbolFinder(interfaces.automagic.AutomagicInterface):
|
||||
"""Symbol loader based on signature strings."""
|
||||
|
||||
priority = 40
|
||||
|
||||
banner_config_key: str = "banner"
|
||||
banner_cache: Optional[Type[symbol_cache.SymbolBannerCache]] = None
|
||||
operating_system: Optional[str] = None
|
||||
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),
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def banners(self) -> symbol_cache.BannersType:
|
||||
"""Creates a cached copy of the results, but only it's been
|
||||
requested."""
|
||||
if not self._banners:
|
||||
if not self.banner_cache:
|
||||
raise RuntimeError(f"Cache has not been properly defined for {self.__class__.__name__}")
|
||||
self._banners = self.banner_cache.load_banners()
|
||||
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
|
||||
)
|
||||
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."""
|
||||
|
||||
# Bomb out early if our details haven't been configured
|
||||
if self.symbol_class is None:
|
||||
return
|
||||
return None
|
||||
|
||||
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:
|
||||
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)):
|
||||
for (tl_sub_path, tl_requirement) in self._requirements:
|
||||
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."""
|
||||
|
||||
# Bomb out early if there's no banners
|
||||
if not self.banners:
|
||||
return
|
||||
return None
|
||||
|
||||
mss = scanners.MultiStringScanner([x for x in self.banners if x is not None])
|
||||
|
||||
@@ -89,36 +128,44 @@ 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)}")
|
||||
symbol_files = self.banners.get(banner, None)
|
||||
if symbol_files:
|
||||
isf_path = symbol_files[0]
|
||||
vollog.debug(f"Using symbol library: {symbol_files[0]}")
|
||||
isf_path = symbol_files
|
||||
vollog.debug(f"Using symbol library: {symbol_files}")
|
||||
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)
|
||||
break
|
||||
else:
|
||||
if symbol_files:
|
||||
vollog.debug(f"Symbol library path not found: {symbol_files[0]}")
|
||||
# print("Kernel", banner, hex(banner_offset))
|
||||
vollog.debug(f"Symbol library path not found for: {banner}")
|
||||
# print("Kernel", banner, hex(banner_offset))
|
||||
else:
|
||||
vollog.debug("No existing banners found")
|
||||
# TODO: Fallback to generic regex search?
|
||||
|
||||
@@ -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,37 +230,56 @@ 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'})
|
||||
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
|
||||
|
||||
# Self Referential finder
|
||||
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):
|
||||
@@ -233,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
|
||||
@@ -248,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
|
||||
|
||||
|
||||
@@ -275,31 +357,40 @@ 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)
|
||||
@@ -310,33 +401,52 @@ class WinSwapLayers(interfaces.automagic.AutomagicInterface):
|
||||
# Fill in the config
|
||||
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'
|
||||
try:
|
||||
context.config[layer_loc_path] = (
|
||||
requirements.URIRequirement.location_from_file(
|
||||
swap_location
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
vollog.warning(
|
||||
f"Volatility swap_location {swap_location} could not be validated - swap layer disabled"
|
||||
)
|
||||
continue
|
||||
context.config[layer_class_path] = (
|
||||
"volatility3.framework.layers.physical.FileLayer"
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -344,10 +454,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,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -10,7 +10,9 @@ expect to be in the context (such as particular layers or symboltables).
|
||||
"""
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
|
||||
from urllib import parse, request
|
||||
|
||||
from volatility3.framework import constants, interfaces
|
||||
|
||||
@@ -24,23 +26,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 +54,37 @@ 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?
|
||||
|
||||
@classmethod
|
||||
def location_from_file(cls, filename: str) -> str:
|
||||
"""Returns the URL location from a file parameter (which may be a URL)
|
||||
|
||||
Args:
|
||||
filename: The path to the file (either an absolute, relative, or URL path)
|
||||
|
||||
Returns:
|
||||
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":
|
||||
if not os.path.exists(request.url2pathname(single_location.path)):
|
||||
filename = request.url2pathname(single_location.path)
|
||||
if not filename:
|
||||
raise ValueError("File URL looks incorrect (potentially missing /)")
|
||||
raise ValueError(f"File does not exist: {filename}")
|
||||
return parse.urlunparse(single_location)
|
||||
|
||||
|
||||
class BytesRequirement(interfaces.configuration.SimpleTypeRequirement):
|
||||
"""A requirement type that contains a byte string."""
|
||||
|
||||
instance_type: ClassVar[Type] = bytes
|
||||
|
||||
|
||||
@@ -67,12 +99,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 +116,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 +150,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 +180,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 +229,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 +247,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 +273,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 +290,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 +340,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 +387,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 +410,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 +425,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 +485,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 +515,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,73 +543,111 @@ 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 len(self._version) > 0 and self._component.version[0] != self._version[0]:
|
||||
if not self.matches_required(self._version, self._component.version):
|
||||
return {config_path: self}
|
||||
if len(self._version) > 1 and self._component.version[1] < self._version[1]:
|
||||
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:
|
||||
if len(required) > 0 and version[0] != required[0]:
|
||||
return False
|
||||
if len(required) > 1 and version[1] < required[1]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
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)!!!
|
||||
@@ -479,7 +656,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)
|
||||
|
||||
@@ -493,8 +672,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)
|
||||
@@ -504,8 +687,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()
|
||||
|
||||
@@ -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,39 +17,46 @@ 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"""
|
||||
|
||||
# We use the SemVer 2.0.0 versioning scheme
|
||||
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
|
||||
VERSION_MINOR = 2 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 0 # Number of changes that do not change the interface
|
||||
VERSION_MINOR = 5 # Number of changes that only add to the interface
|
||||
VERSION_PATCH = 2 # Number of changes that do not change the interface
|
||||
VERSION_SUFFIX = ""
|
||||
|
||||
# TODO: At version 2.0.0, remove the symbol_shift feature
|
||||
|
||||
PACKAGE_VERSION = ".".join([str(x) for x in [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH]]) + VERSION_SUFFIX
|
||||
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,27 +71,33 @@ LOGLEVEL_VVVV = 6
|
||||
CACHE_PATH = os.path.join(os.path.expanduser("~"), ".cache", "volatility3")
|
||||
"""Default path to store cached data"""
|
||||
|
||||
if sys.platform == 'win32':
|
||||
CACHE_PATH = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "volatility3")
|
||||
os.makedirs(CACHE_PATH, exist_ok = True)
|
||||
SQLITE_CACHE_PERIOD = "-3 days"
|
||||
"""SQLite time modifier for how long each item is valid in the cache for"""
|
||||
|
||||
LINUX_BANNERS_PATH = os.path.join(CACHE_PATH, "linux_banners.cache")
|
||||
""""Default location to record information about available linux banners"""
|
||||
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)
|
||||
|
||||
MAC_BANNERS_PATH = os.path.join(CACHE_PATH, "mac_banners.cache")
|
||||
""""Default location to record information about available mac banners"""
|
||||
IDENTIFIERS_FILENAME = "identifier.cache"
|
||||
"""Default location to record information about available identifiers"""
|
||||
|
||||
CACHE_SQLITE_SCHEMA_VERSION = 1
|
||||
"""Version for the sqlite3 cache schema"""
|
||||
|
||||
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
|
||||
@@ -101,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
|
||||
|
||||
@@ -13,4 +13,271 @@ PAGE_SHIFT = 12
|
||||
"""The value hard coded from the Linux Kernel (hence not extracted from the layer itself)"""
|
||||
|
||||
# include/linux/sched.h
|
||||
PF_KTHREAD = 0x00200000 # I'm a kernel thread
|
||||
PF_KTHREAD = 0x00200000 # I'm a kernel thread
|
||||
|
||||
# Standard well-defined IP protocols.
|
||||
# ref: include/uapi/linux/in.h
|
||||
IP_PROTOCOLS = {
|
||||
0: "IP",
|
||||
1: "ICMP",
|
||||
2: "IGMP",
|
||||
4: "IPIP",
|
||||
6: "TCP",
|
||||
8: "EGP",
|
||||
12: "PUP",
|
||||
17: "UDP",
|
||||
22: "IDP",
|
||||
29: "TP",
|
||||
33: "DCCP",
|
||||
41: "IPV6",
|
||||
46: "RSVP",
|
||||
47: "GRE",
|
||||
50: "ESP",
|
||||
51: "AH",
|
||||
92: "MTP",
|
||||
94: "BEETPH",
|
||||
98: "ENCAP",
|
||||
103: "PIM",
|
||||
108: "COMP",
|
||||
132: "SCTP",
|
||||
136: "UDPLITE",
|
||||
137: "MPLS",
|
||||
143: "ETHERNET",
|
||||
255: "RAW",
|
||||
262: "MPTCP",
|
||||
}
|
||||
|
||||
# IPV6 extension headers
|
||||
# ref: include/uapi/linux/in6.h
|
||||
IPV6_PROTOCOLS = {
|
||||
0: "HOPBYHOP_OPTS",
|
||||
43: "ROUTING",
|
||||
44: "FRAGMENT",
|
||||
58: "ICMPv6",
|
||||
59: "NO_NEXT",
|
||||
60: "DESTINATION_OPTS",
|
||||
135: "MOBILITY",
|
||||
}
|
||||
|
||||
# ref: include/net/tcp_states.h
|
||||
TCP_STATES = (
|
||||
"",
|
||||
"ESTABLISHED",
|
||||
"SYN_SENT",
|
||||
"SYN_RECV",
|
||||
"FIN_WAIT1",
|
||||
"FIN_WAIT2",
|
||||
"TIME_WAIT",
|
||||
"CLOSE",
|
||||
"CLOSE_WAIT",
|
||||
"LAST_ACK",
|
||||
"LISTEN",
|
||||
"CLOSING",
|
||||
"TCP_NEW_SYN_RECV",
|
||||
)
|
||||
|
||||
# ref: include/linux/net.h (socket_type enum)
|
||||
SOCK_TYPES = {
|
||||
1: "STREAM",
|
||||
2: "DGRAM",
|
||||
3: "RAW",
|
||||
4: "RDM",
|
||||
5: "SEQPACKET",
|
||||
6: "DCCP",
|
||||
10: "PACKET",
|
||||
}
|
||||
|
||||
# Address families
|
||||
# ref: include/linux/socket.h
|
||||
SOCK_FAMILY = (
|
||||
"AF_UNSPEC",
|
||||
"AF_UNIX",
|
||||
"AF_INET",
|
||||
"AF_AX25",
|
||||
"AF_IPX",
|
||||
"AF_APPLETALK",
|
||||
"AF_NETROM",
|
||||
"AF_BRIDGE",
|
||||
"AF_ATMPVC",
|
||||
"AF_X25",
|
||||
"AF_INET6",
|
||||
"AF_ROSE",
|
||||
"AF_DECnet",
|
||||
"AF_NETBEUI",
|
||||
"AF_SECURITY",
|
||||
"AF_KEY",
|
||||
"AF_NETLINK",
|
||||
"AF_PACKET",
|
||||
"AF_ASH",
|
||||
"AF_ECONET",
|
||||
"AF_ATMSVC",
|
||||
"AF_RDS",
|
||||
"AF_SNA",
|
||||
"AF_IRDA",
|
||||
"AF_PPPOX",
|
||||
"AF_WANPIPE",
|
||||
"AF_LLC",
|
||||
"AF_IB",
|
||||
"AF_MPLS",
|
||||
"AF_CAN",
|
||||
"AF_TIPC",
|
||||
"AF_BLUETOOTH",
|
||||
"AF_IUCV",
|
||||
"AF_RXRPC",
|
||||
"AF_ISDN",
|
||||
"AF_PHONET",
|
||||
"AF_IEEE802154",
|
||||
"AF_CAIF",
|
||||
"AF_ALG",
|
||||
"AF_NFC",
|
||||
"AF_VSOCK",
|
||||
"AF_KCM",
|
||||
"AF_QIPCRTR",
|
||||
"AF_SMC",
|
||||
"AF_XDP",
|
||||
)
|
||||
|
||||
# Socket states
|
||||
# ref: include/uapi/linux/net.h
|
||||
SOCKET_STATES = ("FREE", "UNCONNECTED", "CONNECTING", "CONNECTED", "DISCONNECTING")
|
||||
|
||||
# Netlink protocols
|
||||
# ref: include/uapi/linux/netlink.h
|
||||
NETLINK_PROTOCOLS = (
|
||||
"NETLINK_ROUTE",
|
||||
"NETLINK_UNUSED",
|
||||
"NETLINK_USERSOCK",
|
||||
"NETLINK_FIREWALL",
|
||||
"NETLINK_SOCK_DIAG",
|
||||
"NETLINK_NFLOG",
|
||||
"NETLINK_XFRM",
|
||||
"NETLINK_SELINUX",
|
||||
"NETLINK_ISCSI",
|
||||
"NETLINK_AUDIT",
|
||||
"NETLINK_FIB_LOOKUP",
|
||||
"NETLINK_CONNECTOR",
|
||||
"NETLINK_NETFILTER",
|
||||
"NETLINK_IP6_FW",
|
||||
"NETLINK_DNRTMSG",
|
||||
"NETLINK_KOBJECT_UEVENT",
|
||||
"NETLINK_GENERIC",
|
||||
"NETLINK_DM",
|
||||
"NETLINK_SCSITRANSPORT",
|
||||
"NETLINK_ECRYPTFS",
|
||||
"NETLINK_RDMA",
|
||||
"NETLINK_CRYPTO",
|
||||
"NETLINK_SMC",
|
||||
)
|
||||
|
||||
# Short list of Ethernet Protocol ID's.
|
||||
# ref: include/uapi/linux/if_ether.h
|
||||
# Used in AF_PACKET socket family
|
||||
ETH_PROTOCOLS = {
|
||||
0x0001: "ETH_P_802_3",
|
||||
0x0002: "ETH_P_AX25",
|
||||
0x0003: "ETH_P_ALL",
|
||||
0x0004: "ETH_P_802_2",
|
||||
0x0005: "ETH_P_SNAP",
|
||||
0x0006: "ETH_P_DDCMP",
|
||||
0x0007: "ETH_P_WAN_PPP",
|
||||
0x0008: "ETH_P_PPP_MP",
|
||||
0x0009: "ETH_P_LOCALTALK",
|
||||
0x000C: "ETH_P_CAN",
|
||||
0x000F: "ETH_P_CANFD",
|
||||
0x0010: "ETH_P_PPPTALK",
|
||||
0x0011: "ETH_P_TR_802_2",
|
||||
0x0016: "ETH_P_CONTROL",
|
||||
0x0017: "ETH_P_IRDA",
|
||||
0x0018: "ETH_P_ECONET",
|
||||
0x0019: "ETH_P_HDLC",
|
||||
0x001A: "ETH_P_ARCNET",
|
||||
0x001B: "ETH_P_DSA",
|
||||
0x001C: "ETH_P_TRAILER",
|
||||
0x0060: "ETH_P_LOOP",
|
||||
0x00F6: "ETH_P_IEEE802154",
|
||||
0x00F7: "ETH_P_CAIF",
|
||||
0x00F8: "ETH_P_XDSA",
|
||||
0x00F9: "ETH_P_MAP",
|
||||
0x0800: "ETH_P_IP",
|
||||
0x0805: "ETH_P_X25",
|
||||
0x0806: "ETH_P_ARP",
|
||||
0x8035: "ETH_P_RARP",
|
||||
0x809B: "ETH_P_ATALK",
|
||||
0x80F3: "ETH_P_AARP",
|
||||
0x8100: "ETH_P_8021Q",
|
||||
}
|
||||
|
||||
# Connection and socket states
|
||||
# ref: include/net/bluetooth/bluetooth.h
|
||||
BLUETOOTH_STATES = (
|
||||
"",
|
||||
"CONNECTED",
|
||||
"OPEN",
|
||||
"BOUND",
|
||||
"LISTEN",
|
||||
"CONNECT",
|
||||
"CONNECT2",
|
||||
"CONFIG",
|
||||
"DISCONN",
|
||||
"CLOSED",
|
||||
)
|
||||
|
||||
# Bluetooth protocols
|
||||
# ref: include/net/bluetooth/bluetooth.h
|
||||
BLUETOOTH_PROTOCOLS = (
|
||||
"L2CAP",
|
||||
"HCI",
|
||||
"SCO",
|
||||
"RFCOMM",
|
||||
"BNEP",
|
||||
"CMTP",
|
||||
"HIDP",
|
||||
"AVDTP",
|
||||
)
|
||||
|
||||
# Ref: include/uapi/linux/capability.h
|
||||
CAPABILITIES = (
|
||||
"chown",
|
||||
"dac_override",
|
||||
"dac_read_search",
|
||||
"fowner",
|
||||
"fsetid",
|
||||
"kill",
|
||||
"setgid",
|
||||
"setuid",
|
||||
"setpcap",
|
||||
"linux_immutable",
|
||||
"net_bind_service",
|
||||
"net_broadcast",
|
||||
"net_admin",
|
||||
"net_raw",
|
||||
"ipc_lock",
|
||||
"ipc_owner",
|
||||
"sys_module",
|
||||
"sys_rawio",
|
||||
"sys_chroot",
|
||||
"sys_ptrace",
|
||||
"sys_pacct",
|
||||
"sys_admin",
|
||||
"sys_boot",
|
||||
"sys_nice",
|
||||
"sys_resource",
|
||||
"sys_time",
|
||||
"sys_tty_config",
|
||||
"mknod",
|
||||
"lease",
|
||||
"audit_write",
|
||||
"audit_control",
|
||||
"setfcap",
|
||||
"mac_override",
|
||||
"mac_admin",
|
||||
"syslog",
|
||||
"wake_alarm",
|
||||
"block_suspend",
|
||||
"audit_read",
|
||||
"perfmon",
|
||||
"bpf",
|
||||
"checkpoint_restore",
|
||||
)
|
||||
|
||||
ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1
|
||||
|
||||
@@ -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,24 @@ 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,
|
||||
object_type: Optional[Union[str, "interfaces.objects.ObjectInterface"]] = None,
|
||||
**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
|
||||
@@ -257,11 +285,14 @@ class Module(interfaces.context.ModuleInterface):
|
||||
symbol_name: Name of the symbol (within the module) to construct
|
||||
native_layer_name: Name of the layer in which constructed objects are made (for pointers)
|
||||
absolute: whether the symbol's address is absolute or relative to the module
|
||||
object_type: Override for the type from the symobl to use (or if the symbol type is missing)
|
||||
"""
|
||||
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)
|
||||
@@ -270,19 +301,26 @@ class Module(interfaces.context.ModuleInterface):
|
||||
if not absolute:
|
||||
offset += self._offset
|
||||
|
||||
if symbol_val.type is None:
|
||||
raise TypeError(f"Symbol {symbol_val.name} has no associated type")
|
||||
if object_type is None:
|
||||
if symbol_val.type is None:
|
||||
raise TypeError(
|
||||
f"Symbol {symbol_val.name} has no associated type and no object_type specified"
|
||||
)
|
||||
else:
|
||||
object_type = symbol_val.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=object_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 +328,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 +366,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 +385,13 @@ 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:
|
||||
self._prefix_count = {}
|
||||
super().__init__(modules)
|
||||
|
||||
def deduplicate(self) -> 'ModuleCollection':
|
||||
def deduplicate(self) -> "ModuleCollection":
|
||||
"""Returns a new deduplicated ModuleCollection featuring no repeated
|
||||
modules (based on data hash)
|
||||
|
||||
@@ -361,20 +408,27 @@ class ModuleCollection(interfaces.context.ModuleContainer):
|
||||
|
||||
def free_module_name(self, prefix: str = "module") -> str:
|
||||
"""Returns an unused module name"""
|
||||
count = 1
|
||||
if prefix not in self._prefix_count:
|
||||
self._prefix_count[prefix] = 1
|
||||
return prefix
|
||||
count = self._prefix_count[prefix]
|
||||
while prefix + str(count) in self:
|
||||
count += 1
|
||||
self._prefix_count[prefix] = count
|
||||
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 +437,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
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -9,15 +9,17 @@ that a user has not filled.
|
||||
"""
|
||||
import logging
|
||||
from abc import ABCMeta
|
||||
from typing import Any, List, Optional, Tuple, Union, Type
|
||||
from typing import Any, List, Optional, Tuple, Type, Union
|
||||
|
||||
from volatility3.framework import interfaces, constants
|
||||
from volatility3.framework import constants, interfaces
|
||||
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,32 +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)):
|
||||
if not isinstance(
|
||||
requirement,
|
||||
(
|
||||
interfaces.configuration.SimpleTypeRequirement,
|
||||
requirements.ChoiceRequirement,
|
||||
requirements.ListRequirement,
|
||||
requirements.VersionRequirement,
|
||||
),
|
||||
):
|
||||
raise TypeError(
|
||||
"Automagic requirements must be a SimpleTypeRequirement, ChoiceRequirement or ListRequirement")
|
||||
"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.
|
||||
|
||||
@@ -84,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):
|
||||
@@ -94,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
|
||||
@@ -112,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.
|
||||
|
||||
@@ -134,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"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
@@ -523,12 +565,14 @@ class ConstructableRequirementInterface(RequirementInterface):
|
||||
must happen after the class configuration value has been provided).
|
||||
These values are then provided to the object's constructor by name
|
||||
as arguments (as well as the standard `context` and `config_path`
|
||||
arguments.
|
||||
arguments).
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
) -> "interfaces.objects.ObjectInterface":
|
||||
"""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,14 @@ 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,
|
||||
object_type: Optional[Union[str, "interfaces.objects.ObjectInterface"]] = None,
|
||||
**kwargs,
|
||||
) -> "interfaces.objects.ObjectInterface":
|
||||
"""Returns an object created using the symbol_table_name and layer_name
|
||||
of the Module.
|
||||
|
||||
@@ -249,6 +263,7 @@ class ModuleInterface(interfaces.configuration.ConfigurableInterface):
|
||||
symbol_name: The name of a symbol (that must be present in the module's symbol table). The symbol's associated type will be used to construct an object at the symbol's offset.
|
||||
native_layer_name: The native layer for objects that reference a different layer (if not the default provided during module construction)
|
||||
absolute: A boolean specifying whether the offset is absolute within the layer, or relative to the start of the module
|
||||
object_type: Override for the type from the symobl to use (or if the symbol type is missing)
|
||||
|
||||
Returns:
|
||||
The constructed object
|
||||
@@ -259,13 +274,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 +321,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:
|
||||
|
||||
@@ -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,15 +280,21 @@ 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]] = []
|
||||
position = 0
|
||||
for (start, length) in sorted(sections):
|
||||
for start, length in sorted(sections):
|
||||
if result and start <= position:
|
||||
initial_start, _ = result.pop()
|
||||
result.append((initial_start, (start + length) - initial_start))
|
||||
@@ -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,11 +331,16 @@ 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
|
||||
# It we've got more than the scanner's chunk_size, only move up by the chunk_size
|
||||
# If we've got more than the scanner's chunk_size, only move up by the chunk_size
|
||||
if chunk_size > scanner.chunk_size:
|
||||
chunk_size -= scanner.overlap
|
||||
length -= 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,8 +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")
|
||||
last_section, last_length = sections[-1]
|
||||
@@ -357,11 +398,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 +415,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 +434,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 +451,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 +468,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 +512,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.
|
||||
|
||||
@@ -474,7 +550,7 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
|
||||
scanner.chunk_size + scanner.overlap DataLayers by default are
|
||||
assumed to have no holes
|
||||
"""
|
||||
for (section_start, section_length) in sections:
|
||||
for section_start, section_length in sections:
|
||||
output: List[Tuple[str, int, int]] = []
|
||||
|
||||
# Hold the offsets of each chunk (including how much has been filled)
|
||||
@@ -483,7 +559,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 +584,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
|
||||
@@ -517,10 +598,14 @@ class TranslationLayerInterface(DataLayerInterface, metaclass = ABCMeta):
|
||||
yield output, chunk_position
|
||||
output = []
|
||||
chunk_position = chunk_start
|
||||
# Take from chunk_position as far as far as the block can go,
|
||||
# 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 +653,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 +678,12 @@ 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]
|
||||
if depend_list:
|
||||
if name in self._layers[layer].dependencies:
|
||||
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 by {layer}",
|
||||
)
|
||||
# Otherwise, wipe out the layer
|
||||
self._layers[name].destroy()
|
||||
del self._layers[name]
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interpreted values of data from a layer."""
|
||||
import abc
|
||||
import collections
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any, Dict, List, Mapping, Optional
|
||||
|
||||
@@ -27,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."""
|
||||
@@ -60,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:
|
||||
@@ -77,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:
|
||||
@@ -115,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
|
||||
|
||||
@@ -142,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.
|
||||
|
||||
@@ -162,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
|
||||
@@ -187,11 +205,9 @@ class ObjectInterface(metaclass = abc.ABCMeta):
|
||||
"""
|
||||
if self.has_member(member_name):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_ = getattr(self, member_name)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def has_valid_members(self, member_names: List[str]) -> bool:
|
||||
@@ -202,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
|
||||
@@ -215,35 +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 has_member(cls, template: 'Template', member_name: str) -> bool:
|
||||
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}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def has_member(cls, template: "Template", member_name: str) -> bool:
|
||||
"""Returns whether the object would contain a member called
|
||||
member_name."""
|
||||
return False
|
||||
@@ -277,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:
|
||||
@@ -287,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).
|
||||
|
||||
@@ -306,7 +341,11 @@ class Template:
|
||||
offset."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def replace_child(self, old_child: 'Template', new_child: 'Template') -> None:
|
||||
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:
|
||||
"""Replaces `old_child` with `new_child` in the list of children."""
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -314,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())
|
||||
@@ -328,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."""
|
||||
|
||||
@@ -43,7 +43,7 @@ class FileHandlerInterface(io.RawIOBase):
|
||||
return self._preferred_filename
|
||||
|
||||
@preferred_filename.setter
|
||||
def preferred_filename(self, filename):
|
||||
def preferred_filename(self, filename: str):
|
||||
"""Sets the preferred filename"""
|
||||
if self.closed:
|
||||
raise IOError("FileHandler name cannot be changed once closed")
|
||||
@@ -57,6 +57,18 @@ class FileHandlerInterface(io.RawIOBase):
|
||||
def close(self):
|
||||
"""Method that commits the file and fixes the final filename for use"""
|
||||
|
||||
@staticmethod
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
"""Sanititizes the filename to ensure only a specific whitelist of characters is allowed through"""
|
||||
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ()[]\{\}!$%^:#~?<>,|"
|
||||
result = ""
|
||||
for char in filename:
|
||||
if char in allowed:
|
||||
result += char
|
||||
else:
|
||||
result += "?"
|
||||
return result
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -64,7 +76,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 +96,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 +113,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 +132,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
"""Functions that read AVML files.
|
||||
|
||||
The user of the file doesn't have to worry about the compression,
|
||||
but random access is not allowed."""
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
from typing import Tuple, List, Optional
|
||||
@@ -12,13 +17,56 @@ from volatility3.framework.layers import segmented
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import snappy
|
||||
# TODO: Find library for windows if needed
|
||||
try:
|
||||
# Linux
|
||||
lib_snappy = ctypes.cdll.LoadLibrary("libsnappy.so.1")
|
||||
except OSError:
|
||||
lib_snappy = None
|
||||
|
||||
try:
|
||||
if not lib_snappy:
|
||||
# macOS
|
||||
lib_snappy = ctypes.cdll.LoadLibrary("libsnappy.1.dylib")
|
||||
except OSError:
|
||||
lib_snappy = None
|
||||
|
||||
try:
|
||||
if not lib_snappy:
|
||||
# Windows 64
|
||||
lib_snappy = ctypes.cdll.LoadLibrary("snappy64")
|
||||
except OSError:
|
||||
lib_snappy = None
|
||||
|
||||
if not lib_snappy:
|
||||
# Windows 32
|
||||
lib_snappy = ctypes.cdll.LoadLibrary("snappy32")
|
||||
|
||||
__snappy_uncompress = lib_snappy.snappy_uncompress
|
||||
__snappy_uncompressed_length = lib_snappy.snappy_uncompressed_length
|
||||
|
||||
HAS_SNAPPY = True
|
||||
except ImportError:
|
||||
except (AttributeError, OSError):
|
||||
HAS_SNAPPY = False
|
||||
|
||||
|
||||
class SnappyException(exceptions.VolatilityException):
|
||||
pass
|
||||
|
||||
|
||||
def uncompress(s):
|
||||
"""Uncompress a snappy compressed string."""
|
||||
ulen = ctypes.c_int(0)
|
||||
cresult = __snappy_uncompressed_length(s, len(s), ctypes.byref(ulen))
|
||||
if cresult != 0:
|
||||
raise SnappyException(f"Error in snappy_uncompressed_length: {cresult}")
|
||||
ubuf = ctypes.create_string_buffer(ulen.value)
|
||||
cresult = __snappy_uncompress(s, len(s), ubuf, ctypes.byref(ulen))
|
||||
if cresult != 0:
|
||||
raise SnappyException(f"Error in snappy_uncompress: {cresult}")
|
||||
return ubuf.raw
|
||||
|
||||
|
||||
class AVMLLayer(segmented.NonLinearlySegmentedLayer):
|
||||
"""A Lime format TranslationLayer.
|
||||
|
||||
@@ -33,13 +81,20 @@ 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:
|
||||
raise exceptions.LayerException("File not completely in AVML format")
|
||||
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 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 library could not be found\n"
|
||||
"Please install the snappy from your distribution or https://google.github.io/snappy/."
|
||||
)
|
||||
raise exceptions.LayerException(
|
||||
"AVML format dependencies not satisfied (snappy)"
|
||||
)
|
||||
|
||||
def _load_segments(self) -> None:
|
||||
base_layer = self.context.layers[self._base_layer]
|
||||
@@ -48,24 +103,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))
|
||||
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._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
|
||||
|
||||
@@ -80,41 +149,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)
|
||||
frame_data = uncompress(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)
|
||||
decoded_data = uncompress(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
|
||||
|
||||
@@ -123,14 +213,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)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import Optional, Any, List
|
||||
|
||||
try:
|
||||
import s3fs
|
||||
|
||||
HAS_S3FS = True
|
||||
except ImportError:
|
||||
HAS_S3FS = False
|
||||
|
||||
try:
|
||||
import gcsfs
|
||||
|
||||
HAS_GCSFS = True
|
||||
except ImportError:
|
||||
HAS_GCSFS = False
|
||||
|
||||
from volatility3.framework.layers import resources
|
||||
|
||||
vollog = logging.getLogger(__file__)
|
||||
|
||||
if HAS_S3FS:
|
||||
|
||||
class S3FileSystemHandler(resources.VolatilityHandler):
|
||||
@classmethod
|
||||
def non_cached_schemes(cls) -> List[str]:
|
||||
return ["s3"]
|
||||
|
||||
@staticmethod
|
||||
def default_open(req: urllib.request.Request) -> Optional[Any]:
|
||||
"""Handles the request if it's the s3 scheme."""
|
||||
if req.type == "s3":
|
||||
object_uri = "://".join(req.full_url.split("://")[1:])
|
||||
return s3fs.S3FileSystem().open(object_uri)
|
||||
return None
|
||||
|
||||
|
||||
if HAS_GCSFS:
|
||||
|
||||
class GSFileSystemHandler(resources.VolatilityHandler):
|
||||
@classmethod
|
||||
def non_cached_schemes(cls) -> List[str]:
|
||||
return ["gs"]
|
||||
|
||||
@staticmethod
|
||||
def default_open(req: urllib.request.Request) -> Optional[Any]:
|
||||
"""Handles the request if it's the gs scheme."""
|
||||
if req.type == "gs":
|
||||
object_uri = "://".join(req.full_url.split("://")[1:])
|
||||
return gcsfs.GCSFileSystem().open(object_uri)
|
||||
return None
|
||||
@@ -1,3 +1,7 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
"""Codecs used for encoding or decoding data should live here
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import contextlib
|
||||
import logging
|
||||
import struct
|
||||
from typing import Tuple, Optional
|
||||
@@ -26,17 +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
|
||||
self._config_path = config_path
|
||||
@@ -45,15 +47,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]
|
||||
@@ -70,21 +75,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."""
|
||||
@@ -92,15 +106,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:
|
||||
@@ -117,7 +141,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:
|
||||
@@ -134,48 +165,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
|
||||
|
||||
@@ -187,8 +244,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
|
||||
|
||||
@@ -197,16 +254,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]:
|
||||
try:
|
||||
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)
|
||||
except WindowsCrashDumpFormatException:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -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,12 @@ 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)
|
||||
try:
|
||||
return Elf64Layer(context, new_name, new_name)
|
||||
except ElfFormatException as excp:
|
||||
vollog.log(constants.LOGLEVEL_VVVV, f"Exception: {excp}")
|
||||
return None
|
||||
|
||||
@@ -28,28 +28,44 @@ 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
|
||||
self._canonical_prefix = self._mask(
|
||||
(1 << self._bits_per_register) - 1,
|
||||
self._bits_per_register,
|
||||
self._maxvirtaddr,
|
||||
)
|
||||
|
||||
# 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 +102,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
|
||||
|
||||
@@ -95,6 +111,28 @@ class Intel(linear.LinearlyMappedLayer):
|
||||
"""Returns whether a particular page is valid based on its entry."""
|
||||
return bool(entry & 1)
|
||||
|
||||
@staticmethod
|
||||
def _page_is_dirty(entry: int) -> bool:
|
||||
"""Returns whether a particular page is dirty based on its entry."""
|
||||
return bool(entry & (1 << 6))
|
||||
|
||||
def canonicalize(self, addr: int) -> int:
|
||||
"""Canonicalizes an address by performing an appropiate sign extension on the higher addresses"""
|
||||
if self._bits_per_register <= self._maxvirtaddr:
|
||||
return addr & self.address_mask
|
||||
elif addr < (1 << self._maxvirtaddr - 1):
|
||||
return addr
|
||||
return self._mask(addr, self._maxvirtaddr, 0) + self._canonical_prefix
|
||||
|
||||
def decanonicalize(self, addr: int) -> int:
|
||||
"""Removes canonicalization to ensure an adress fits within the correct range if it has been canonicalized
|
||||
|
||||
This will produce an address outside the range if the canonicalization is incorrect
|
||||
"""
|
||||
if addr < (1 << self._maxvirtaddr - 1):
|
||||
return addr
|
||||
return addr ^ self._canonical_prefix
|
||||
|
||||
def _translate(self, offset: int) -> Tuple[int, int, str]:
|
||||
"""Translates a specific offset based on paging tables.
|
||||
|
||||
@@ -106,9 +144,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 +169,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:
|
||||
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 +202,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 +255,40 @@ 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 is_dirty(self, offset: int) -> bool:
|
||||
"""Returns whether the page at offset is marked dirty"""
|
||||
return self._page_is_dirty(self._translate_entry(offset)[0])
|
||||
|
||||
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 +303,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,20 +325,29 @@ 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
|
||||
return
|
||||
return None
|
||||
yield offset, length, mapped_offset, length, layer_name
|
||||
return
|
||||
return None
|
||||
while length > 0:
|
||||
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 +355,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 +372,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 +390,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 +431,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 +443,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 +471,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)
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import io
|
||||
import logging
|
||||
import urllib.parse
|
||||
@@ -5,6 +9,7 @@ from typing import Optional, Any, List
|
||||
|
||||
try:
|
||||
import leechcorepyc
|
||||
|
||||
HAS_LEECHCORE = True
|
||||
except ImportError:
|
||||
HAS_LEECHCORE = False
|
||||
@@ -62,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:
|
||||
@@ -86,10 +91,14 @@ if HAS_LEECHCORE:
|
||||
chunk_size = size
|
||||
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)
|
||||
|
||||
@@ -110,14 +119,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")
|
||||
@@ -155,20 +166,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import functools
|
||||
from typing import List, Optional, Tuple, Iterable
|
||||
|
||||
@@ -10,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))
|
||||
@@ -54,18 +71,25 @@ class LinearlyMappedLayer(interfaces.layers.TranslationLayerInterface):
|
||||
underlying mapping."""
|
||||
current_offset = offset
|
||||
length = len(value)
|
||||
for (offset, _, mapped_offset, length, layer) in self.mapping(offset, length):
|
||||
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)
|
||||
|
||||
@@ -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")
|
||||
@@ -43,59 +47,82 @@ class PdbMultiStreamFormat(linear.LinearlyMappedLayer):
|
||||
def read_streams(self):
|
||||
# Shortcut in case they've already been read
|
||||
if self._streams:
|
||||
return
|
||||
return None
|
||||
|
||||
# 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
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import threading
|
||||
from typing import Any, Dict, IO, List, Optional, Union
|
||||
|
||||
from volatility3.framework import exceptions, interfaces, constants
|
||||
from volatility3.framework import constants, exceptions, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import resources
|
||||
|
||||
@@ -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)
|
||||
@@ -191,9 +214,9 @@ class FileLayer(interfaces.layers.DataLayerInterface):
|
||||
"""Closes the file handle."""
|
||||
self._file.close()
|
||||
|
||||
def __del__(self) -> None:
|
||||
def __exit__(self, type, value, traceback) -> None:
|
||||
self.destroy()
|
||||
|
||||
@classmethod
|
||||
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
|
||||
return [requirements.StringRequirement(name = 'location', optional = False)]
|
||||
return [requirements.StringRequirement(name="location", optional=False)]
|
||||
|
||||
@@ -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,88 @@ 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):
|
||||
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 = b""
|
||||
for i in range(
|
||||
base_layer.maximum_address + 1, base_layer.minimum_address, -chunk_size
|
||||
):
|
||||
# Since we're going backwards, we need to include one extra byte so the tail doesn't get chopped off
|
||||
if i != base_layer.maximum_address + 1:
|
||||
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 +147,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 +160,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 +234,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 +371,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 +475,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
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
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]"
|
||||
@@ -92,55 +116,73 @@ class RegistryHive(linear.LinearlyMappedLayer):
|
||||
@property
|
||||
def root_cell_offset(self) -> int:
|
||||
"""Returns the offset for the root cell in this hive."""
|
||||
try:
|
||||
if self._base_block.Signature.cast("string", max_length = 4, encoding = "latin-1") == 'regf':
|
||||
with contextlib.suppress(InvalidAddressException):
|
||||
if (
|
||||
self._base_block.Signature.cast(
|
||||
"string", max_length=4, encoding="latin-1"
|
||||
)
|
||||
== "regf"
|
||||
):
|
||||
return self._base_block.RootCell
|
||||
except InvalidAddressException:
|
||||
pass
|
||||
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
|
||||
node (default) or a list of nodes from root to the current node
|
||||
(if return_list is true).
|
||||
"""
|
||||
node_key = [self.get_node(self.root_cell_offset)]
|
||||
root_node = self.get_node(self.root_cell_offset)
|
||||
if not root_node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"):
|
||||
raise RegistryFormatException(
|
||||
self.name,
|
||||
"Encountered {} instead of _CM_KEY_NODE".format(
|
||||
root_node.vol.type_name
|
||||
),
|
||||
)
|
||||
node_key = [root_node]
|
||||
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()
|
||||
@@ -154,14 +196,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:
|
||||
@@ -174,22 +220,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:
|
||||
@@ -198,15 +250,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
|
||||
@@ -217,11 +274,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")
|
||||
|
||||
@@ -236,7 +291,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
|
||||
@@ -248,18 +311,19 @@ 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."""
|
||||
try:
|
||||
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)
|
||||
])
|
||||
except exceptions.InvalidAddressException:
|
||||
return False
|
||||
return all(
|
||||
[
|
||||
self.context.layers[layer].is_valid(offset, length)
|
||||
for (_, _, offset, length, layer) in self.mapping(offset, length)
|
||||
]
|
||||
)
|
||||
return False
|
||||
|
||||
@property
|
||||
def minimum_address(self) -> int:
|
||||
|
||||
@@ -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
|
||||
@@ -184,23 +204,29 @@ class ResourceAccessor(object):
|
||||
stop = False
|
||||
while not stop:
|
||||
detected = None
|
||||
try:
|
||||
with contextlib.suppress(AttributeError, IOError):
|
||||
# Detect the content
|
||||
detected = magic.detect_from_fobj(curfile)
|
||||
IMPORTED_MAGIC = True
|
||||
# This is because python-magic and file provide a magic module
|
||||
# Only file's python has magic.detect_from_fobj
|
||||
except (AttributeError, IOError):
|
||||
pass
|
||||
|
||||
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)
|
||||
@@ -223,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
|
||||
|
||||
@@ -234,7 +262,6 @@ class ResourceAccessor(object):
|
||||
|
||||
|
||||
class VolatilityHandler(urllib.request.BaseHandler):
|
||||
|
||||
@classmethod
|
||||
def non_cached_schemes(cls) -> List[str]:
|
||||
return []
|
||||
@@ -252,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
|
||||
@@ -277,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())
|
||||
|
||||
@@ -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,14 +116,19 @@ 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
|
||||
if logical_offset > offset + length:
|
||||
return
|
||||
return None
|
||||
except exceptions.InvalidAddressException:
|
||||
return
|
||||
return None
|
||||
# Crop it to the amount we need left
|
||||
chunk_size = min(size, length + offset - logical_offset)
|
||||
yield logical_offset, chunk_size, mapped_offset, mapped_size, self._base_layer
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import struct
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from volatility3.framework import interfaces, constants, exceptions
|
||||
from volatility3.framework import constants, exceptions, interfaces
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.layers import physical, segmented, resources
|
||||
from volatility3.framework.layers import physical, resources, segmented
|
||||
from volatility3.framework.symbols import native
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
@@ -22,18 +23,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 +52,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,47 +92,74 @@ 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
|
||||
|
||||
if data_len in [62, 63]: # Handle special data sizes that indicate a longer data stream
|
||||
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
|
||||
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
|
||||
length = tags[("regionSize", (region, ))][1] * self._page_size
|
||||
offset = tags[("regionPPN", (region,))][1] * self._page_size
|
||||
mapped_offset = tags[("regionPageNum", (region,))][1] * self._page_size
|
||||
length = tags[("regionSize", (region,))][1] * self._page_size
|
||||
self._segments.append((offset, mapped_offset, length, length))
|
||||
|
||||
@property
|
||||
@@ -127,8 +171,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 +180,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,35 +195,57 @@ 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
|
||||
try:
|
||||
_ = 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 contextlib.suppress(IOError):
|
||||
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
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
vmsn_success = False
|
||||
if not vmss_success:
|
||||
try:
|
||||
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
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
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:
|
||||
vmem_file_basename = os.path.basename(location)
|
||||
example_vmss_file_basename = os.path.basename(vmss)
|
||||
vollog.warning(
|
||||
f"No metadata file found alongside VMEM file. A VMSS or VMSN file may be required to correctly process a VMEM file. These should be placed in the same directory with the same file name, e.g. {vmem_file_basename} and {example_vmss_file_basename}.",
|
||||
)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import logging
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
from volatility3.framework import constants, interfaces, exceptions
|
||||
from volatility3.framework.layers import elf
|
||||
from volatility3.framework.symbols import intermed
|
||||
|
||||
vollog = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XenCoreDumpLayer(elf.Elf64Layer):
|
||||
"""A layer that supports the Xen Dump-Core format as documented at: https://xenbits.xen.org/docs/4.6-testing/misc/dump-core-format.txt"""
|
||||
|
||||
_header_struct = struct.Struct("<IBBB")
|
||||
MAGIC = 0x464C457F # "\x7fELF"
|
||||
ELF_CLASS = 2
|
||||
|
||||
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._xen_table_name = intermed.IntermediateSymbolTable.create(
|
||||
context, config_path, "linux", "xen"
|
||||
)
|
||||
self._segment_headers = {}
|
||||
|
||||
super().__init__(context, config_path, name)
|
||||
|
||||
def _extract_result_array(
|
||||
self, varname: str, segment_index: int
|
||||
) -> interfaces.objects.ObjectInterface:
|
||||
hdr = self._segment_headers[segment_index]
|
||||
result = self.context.object(
|
||||
self._xen_table_name + constants.BANG + varname,
|
||||
layer_name=self._base_layer,
|
||||
offset=hdr.sh_offset,
|
||||
size=hdr.sh_size,
|
||||
)
|
||||
result.entries.count = hdr.sh_size // result.entries.vol.subtype.size
|
||||
return result
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
segments = []
|
||||
self._segment_headers = []
|
||||
|
||||
for sindex in range(ehdr.e_shnum):
|
||||
shdr = self.context.object(
|
||||
self._elf_table_name + constants.BANG + "Elf64_Shdr",
|
||||
layer_name=self._base_layer,
|
||||
offset=ehdr.e_shoff + (sindex * ehdr.e_shentsize),
|
||||
)
|
||||
|
||||
self._segment_headers.append(shdr)
|
||||
|
||||
if sindex == ehdr.e_shstrndx:
|
||||
segment_names = self.context.layers[self._base_layer].read(
|
||||
shdr.sh_offset, shdr.sh_size
|
||||
)
|
||||
segment_names = segment_names.split(b"\x00")
|
||||
|
||||
if not segment_names:
|
||||
raise elf.ElfFormatException("No segment names, not a Xen Core Dump")
|
||||
|
||||
try:
|
||||
p2m_data = self._extract_result_array(
|
||||
"xen_p2m", segment_names.index(b".xen_p2m")
|
||||
)
|
||||
except ValueError:
|
||||
p2m_data = None
|
||||
try:
|
||||
pfn_data = self._extract_result_array(
|
||||
"xen_pfn", segment_names.index(b".xen_pfn")
|
||||
)
|
||||
except ValueError:
|
||||
pfn_data = None
|
||||
|
||||
pages_hdr = self._segment_headers[segment_names.index(b".xen_pages")]
|
||||
page_size = 0x1000
|
||||
|
||||
if pfn_data and not p2m_data:
|
||||
for entry_index in range(len(pfn_data.entries)):
|
||||
entry = pfn_data.entries[entry_index]
|
||||
# TODO: Don't hardcode the maximum value here
|
||||
if entry and entry != 0xFFFFFFFF:
|
||||
segments.append(
|
||||
(
|
||||
entry * page_size,
|
||||
pages_hdr.sh_offset + (entry_index * page_size),
|
||||
page_size,
|
||||
page_size,
|
||||
)
|
||||
)
|
||||
elif p2m_data and not pfn_data:
|
||||
for entry_index in range(len(p2m_data.entries)):
|
||||
entry = p2m_data.entries[entry_index]
|
||||
# TODO: Don't hardcode the maximum value here
|
||||
if entry.pfn != 0xFFFFFFFF:
|
||||
segments.append(
|
||||
(
|
||||
entry.pfn * page_size,
|
||||
pages_hdr.sh_offset + (entry_index * page_size),
|
||||
page_size,
|
||||
page_size,
|
||||
)
|
||||
)
|
||||
elif p2m_data and pfn_data:
|
||||
raise elf.ElfFormatException(
|
||||
self.name, f"Both P2M and PFN in Xen Core Dump"
|
||||
)
|
||||
else:
|
||||
raise elf.ElfFormatException(
|
||||
self.name, f"Neither P2M nor PFN in Xen Core Dump"
|
||||
)
|
||||
|
||||
if len(segments) == 0:
|
||||
raise elf.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:
|
||||
try:
|
||||
header_data = base_layer.read(offset, cls._header_struct.size)
|
||||
except exceptions.InvalidAddressException:
|
||||
raise elf.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 elf.ElfFormatException(
|
||||
base_layer.name, f"Bad magic 0x{magic:x} at file offset 0x{offset:x}"
|
||||
)
|
||||
if elf_class != cls.ELF_CLASS:
|
||||
raise elf.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
|
||||
|
||||
|
||||
class XenCoreDumpStacker(elf.Elf64Stacker):
|
||||
stack_order = 10
|
||||
|
||||
@classmethod
|
||||
def stack(
|
||||
cls,
|
||||
context: interfaces.context.ContextInterface,
|
||||
layer_name: str,
|
||||
progress_callback: constants.ProgressCallback = None,
|
||||
) -> Optional[interfaces.layers.DataLayerInterface]:
|
||||
try:
|
||||
if not XenCoreDumpLayer._check_header(context.layers[layer_name]):
|
||||
return None
|
||||
except elf.ElfFormatException as excp:
|
||||
vollog.log(constants.LOGLEVEL_VVVV, f"Exception: {excp}")
|
||||
return None
|
||||
new_name = context.layers.free_layer_name("XenCoreDumpLayer")
|
||||
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
|
||||
layer_name
|
||||
)
|
||||
|
||||
return XenCoreDumpLayer(context, new_name, new_name)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
@@ -48,28 +57,45 @@ class ObjectTemplate(interfaces.objects.Template):
|
||||
plateProxy`)"""
|
||||
return self.vol.object_class.VolTemplateProxy.relative_child_offset(self, child)
|
||||
|
||||
def replace_child(self, old_child: interfaces.objects.Template, new_child: interfaces.objects.Template) -> None:
|
||||
def child_template(self, child: str) -> interfaces.objects.Template:
|
||||
"""Returns the template of a child of the templated object (see
|
||||
:class:`~volatility3.framework.interfaces.objects.ObjectInterface.VolTem
|
||||
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:
|
||||
"""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 adhereing to the :class:`~volatility3.framework.interfaces.objects.ObjectInterface`
|
||||
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):
|
||||
@@ -93,14 +119,21 @@ 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
|
||||
relative_child_offset: ClassVar[Any] = _unresolved
|
||||
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)
|
||||
|
||||
@@ -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,36 @@ 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)"
|
||||
)
|
||||
# We have to clone the pointer class, or we'll be defining the pointer subtype for all future pointers
|
||||
subtype_pointer = context.symbol_space.get_type(
|
||||
symbol_table + constants.BANG + "pointer"
|
||||
).clone()
|
||||
subtype_pointer.update_vol(subtype=subtype)
|
||||
return array.cast("array", count=count, subtype=subtype_pointer)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
|
||||
from typing import List
|
||||
|
||||
from volatility3 import framework
|
||||
@@ -16,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())
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
# This file is Copyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0
|
||||
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
|
||||
#
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import zipfile
|
||||
from typing import List, Type, Any, Generator
|
||||
from typing import Generator, List
|
||||
|
||||
from volatility3 import schemas, symbols
|
||||
from volatility3.framework import interfaces, renderers, constants
|
||||
from volatility3.framework.automagic import mac, linux, symbol_cache
|
||||
from volatility3.framework import constants, interfaces, renderers
|
||||
from volatility3.framework.automagic import symbol_cache
|
||||
from volatility3.framework.configuration import requirements
|
||||
from volatility3.framework.interfaces import plugins
|
||||
from volatility3.framework.layers import resources
|
||||
@@ -23,108 +22,182 @@ class IsfInfo(plugins.PluginInterface):
|
||||
"""Determines information about the currently available ISF files, or a specific one"""
|
||||
|
||||
_required_framework_version = (2, 0, 0)
|
||||
_version = (1, 0, 0)
|
||||
_version = (2, 0, 0)
|
||||
|
||||
@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.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:
|
||||
if filename.endswith(extension):
|
||||
yield pathlib.Path(base_name).as_uri()
|
||||
|
||||
def _get_banner(self, clazz: Type[symbol_cache.SymbolBannerCache], data: Any) -> str:
|
||||
"""Gets a banner from an ISF file"""
|
||||
banner_symbol = data.get('symbols', {}).get(clazz.symbol_name, {}).get('constant_data',
|
||||
renderers.NotAvailableValue())
|
||||
if not isinstance(banner_symbol, interfaces.renderers.BaseAbsentValue):
|
||||
banner_symbol = str(base64.b64decode(banner_symbol), encoding = 'latin-1')
|
||||
return banner_symbol
|
||||
|
||||
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"
|
||||
|
||||
# Process the filtered list
|
||||
for entry in filtered_list:
|
||||
num_types = num_enums = num_bases = num_symbols = 0
|
||||
windows_info = linux_banner = mac_banner = renderers.NotAvailableValue()
|
||||
valid = "Unknown"
|
||||
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', []))
|
||||
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:
|
||||
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", []))
|
||||
|
||||
linux_banner = self._get_banner(linux.LinuxBannerCache, data)
|
||||
mac_banner = self._get_banner(mac.MacBannerCache, data)
|
||||
if not linux_banner and not mac_banner:
|
||||
windows_info = os.path.splitext(os.path.basename(entry))[0]
|
||||
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, windows_info, linux_banner,
|
||||
mac_banner))
|
||||
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)
|
||||
if identifier:
|
||||
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,
|
||||
),
|
||||
)
|
||||
else:
|
||||
identifiers_path = os.path.join(
|
||||
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME
|
||||
)
|
||||
cache = symbol_cache.SqliteCache(identifiers_path)
|
||||
valid = "Unknown"
|
||||
for identifier, location in cache.get_identifier_dictionary().items():
|
||||
(
|
||||
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"]:
|
||||
# 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:
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
# 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), ("Windows info", str), ("Linux banner", str),
|
||||
("Mac banner", 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(),
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
@@ -63,45 +75,67 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface):
|
||||
|
||||
bang_addrs = []
|
||||
|
||||
# get task memory sections to be used by scanners
|
||||
task_memory_sections = [
|
||||
section for section in task.get_process_memory_sections(heap_only=True)
|
||||
]
|
||||
|
||||
# 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_memory_sections,
|
||||
):
|
||||
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_memory_sections,
|
||||
):
|
||||
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])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user