diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3a0cce8cc..2ccd4713c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..5f4523072 --- /dev/null +++ b/.github/workflows/black.yml @@ -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" diff --git a/.github/workflows/build-pypi.yml b/.github/workflows/build-pypi.yml index 77fe26931..f21898971 100644 --- a/.github/workflows/build-pypi.yml +++ b/.github/workflows/build-pypi.yml @@ -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: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..fa9bd7ef6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -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}}" diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 000000000..cc2a7fd3e --- /dev/null +++ b/.github/workflows/install.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..925a9f624 --- /dev/null +++ b/.github/workflows/stale.yml @@ -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" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000..b1a9dd31b --- /dev/null +++ b/.github/workflows/test.yaml @@ -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 - diff --git a/.gitignore b/.gitignore index d26e17d91..328ba5f83 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ ENV/ # Memory dump files *.dmp *.vmem +*.img + +# PyTest cache files +.pytest_cache/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 4d21d9b40..e7c2b25d5 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -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 diff --git a/.style.yapf b/.style.yapf index 8159be910..3f154e07b 100644 --- a/.style.yapf +++ b/.style.yapf @@ -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= diff --git a/API_CHANGES.md b/API_CHANGES.md index 274d1d8bb..61d8781fb 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -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. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..c36c3b7d5 --- /dev/null +++ b/CITATION.cff @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt index 96f222187..2a37fd0ed 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -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. diff --git a/README.md b/README.md index 9f9c1bbb7..f69acb3bb 100644 --- a/README.md +++ b/README.md @@ -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: 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(): diff --git a/doc/requirements.txt b/doc/requirements.txt index 93d6ea70a..b715e59f5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -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 diff --git a/doc/source/basics.rst b/doc/source/basics.rst index d493c61b3..1b8e64780 100644 --- a/doc/source/basics.rst +++ b/doc/source/basics.rst @@ -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 ` and whose internal nodes are -specifically called a :py:class:`TranslationLayer `. -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) ` 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 ` and whose internal nodes are :py:class:`TranslationLayers `. +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() ` +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. diff --git a/doc/source/conf.py b/doc/source/conf.py index 731a73d56..cabfdc327 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -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" diff --git a/doc/source/getting-started-linux-tutorial.rst b/doc/source/getting-started-linux-tutorial.rst new file mode 100644 index 000000000..26ad2c2e4 --- /dev/null +++ b/doc/source/getting-started-linux-tutorial.rst @@ -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 `_ +* `LiME - Linux Memory Extract `_ + + +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 `_ , + which is built and maintained by `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 `. +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 + + +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 `_ for providing this memory dump and `writeup `_. + + +.. 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 diff --git a/doc/source/getting-started-mac-tutorial.rst b/doc/source/getting-started-mac-tutorial.rst new file mode 100644 index 000000000..42e58c0d5 --- /dev/null +++ b/doc/source/getting-started-mac-tutorial.rst @@ -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 `_ + + + +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 `_ , + which is built and maintained by `volatilityfoundation `_. + 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 `. +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 + + +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 `_ for providing this memory dump and `writeup `_. + + +.. 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. diff --git a/doc/source/getting-started-windows-tutorial.rst b/doc/source/getting-started-windows-tutorial.rst new file mode 100644 index 000000000..c89b065f5 --- /dev/null +++ b/doc/source/getting-started-windows-tutorial.rst @@ -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 `_ +* `FTK Imager `_ + +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 `. +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 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. + + + + + + diff --git a/doc/source/index.rst b/doc/source/index.rst index 3b5a5d2a8..7f35e9bcb 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -7,9 +7,10 @@ Volatility 3 is Open Source. :doc:`List of 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 ================== diff --git a/doc/source/simple-plugin.rst b/doc/source/simple-plugin.rst index 8446b0ef5..39670a62d 100644 --- a/doc/source/simple-plugin.rst +++ b/doc/source/simple-plugin.rst @@ -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 `. 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 ` and a +:py:class:`SymbolTable `, 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 ` 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 ` -to be loaded. This gets populated by various -:py:class:`Automagic ` 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 ` can determine -the appropriate :py:class:`SymbolTable `, the -name of the :py:class:`SymbolTable ` will be stored in the configuration. + This requirement indicates that the plugin will operate on a single + :py:class:`TranslationLayer `. 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 ` + to be loaded. This gets populated by various + :py:class:`Automagic ` 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 ` can determine + the appropriate :py:class:`SymbolTable `, the + name of the :py:class:`SymbolTable ` 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 diff --git a/doc/source/symbol-tables.rst b/doc/source/symbol-tables.rst index 4dea6077d..b7c26e046 100644 --- a/doc/source/symbol-tables.rst +++ b/doc/source/symbol-tables.rst @@ -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:`/-.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 `_. 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` \ No newline at end of file + * For Mac change `linux` to `mac` diff --git a/doc/source/using-as-a-library.rst b/doc/source/using-as-a-library.rst index c63adcfc3..4acf35f98 100644 --- a/doc/source/using-as-a-library.rst +++ b/doc/source/using-as-a-library.rst @@ -54,6 +54,12 @@ also be included, which can be found in `volatility3.constants.PLUGINS_PATH`. volatility3.plugins.__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 diff --git a/doc/source/vol-cli.rst b/doc/source/vol-cli.rst index 9db29c818..cc6f7fe6a 100644 --- a/doc/source/vol-cli.rst +++ b/doc/source/vol-cli.rst @@ -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] ... @@ -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. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..c9b615cd8 --- /dev/null +++ b/requirements-dev.txt @@ -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 diff --git a/requirements-minimal.txt b/requirements-minimal.txt index 31ac02814..c030b332d 100644 --- a/requirements-minimal.txt +++ b/requirements-minimal.txt @@ -1,2 +1,2 @@ # These packages are required for core functionality. -pefile>=2017.8.1 #foo \ No newline at end of file +pefile>=2023.2.7 #foo \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 290d9ca97..4d09ff82a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/setup.py b/setup.py index f6bb687f2..c2c55067d 100644 --- a/setup.py +++ b/setup.py @@ -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(), +) diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..dcbe289b0 --- /dev/null +++ b/test/README.md @@ -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` \ No newline at end of file diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..4ad63065b --- /dev/null +++ b/test/conftest.py @@ -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") diff --git a/test/known_files.json b/test/known_files.json new file mode 100644 index 000000000..a579c8053 --- /dev/null +++ b/test/known_files.json @@ -0,0 +1,19 @@ +{ + "windows_dumpfiles": { + "win-xp-laptop-2005-06-25.img": { + "0x82220e78": [ + "9bdd5532286f1660f3778e68bc36efe6", + "e3bc1e9e7370e3b5a661ebe591ecf4ec" + ], + "0x82350bf8": [ + "e5c5e8d97b6280745b41f6572c85d1f0", + "8589f1463422884dbf1411aaad278465" + ], + "0x81eaf418": [ + "f7a1ae2060a58f8470b97affdb46dccf", + "54fd611021fa784912530b8007545986" + ], + "0x820588e8": "458efbc8fdb859488a6ab2b200cce809" + } + } +} diff --git a/test/requirements-testing.txt b/test/requirements-testing.txt new file mode 100644 index 000000000..7afe19b94 --- /dev/null +++ b/test/requirements-testing.txt @@ -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 diff --git a/test/test_volatility.py b/test/test_volatility.py new file mode 100644 index 000000000..aaad615bc --- /dev/null +++ b/test/test_volatility.py @@ -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 diff --git a/vol.py b/vol.py index 080413545..ff420cad5 100755 --- a/vol.py +++ b/vol.py @@ -6,5 +6,5 @@ import volatility3.cli -if __name__ == '__main__': +if __name__ == "__main__": volatility3.cli.main() diff --git a/vol.spec b/vol.spec index 42b69af3f..666526dde 100644 --- a/vol.spec +++ b/vol.spec @@ -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 = [], diff --git a/volatility3/__init__.py b/volatility3/__init__.py index db52aa9b0..94a6721e1 100644 --- a/volatility3/__init__.py +++ b/volatility3/__init__.py @@ -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) diff --git a/volatility3/cli/__init__.py b/volatility3/cli/__init__.py index 6881311bf..0e7ba41ee 100644 --- a/volatility3/cli/__init__.py +++ b/volatility3/cli/__init__.py @@ -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 '{} --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 '{} --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 '{} --help'".format( - self.CLI_NAME), - action = volargparse.HelpfulSubparserAction) + subparser = parser.add_subparsers( + title="Plugins", + dest="plugin", + description="For plugin specific options, run '{} --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 ') - args.save_config = 'config.json' + vollog.warning( + "Use of --write-config has been deprecated, replaced by --save-config " + ) + 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(): diff --git a/volatility3/cli/text_renderer.py b/volatility3/cli/text_renderer.py index ecb5179e0..6e58ee68d 100644 --- a/volatility3/cli/text_renderer.py +++ b/volatility3/cli/text_renderer.py @@ -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") diff --git a/volatility3/cli/volargparse.py b/volatility3/cli/volargparse.py index 8ba807fee..3048a0885 100644 --- a/volatility3/cli/volargparse.py +++ b/volatility3/cli/volargparse.py @@ -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) diff --git a/volatility3/cli/volshell/__init__.py b/volatility3/cli/volshell/__init__.py index e2e7bc394..40f42f3af 100644 --- a/volatility3/cli/volshell/__init__.py +++ b/volatility3/cli/volshell/__init__.py @@ -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 ') - args.save_config = 'config.json' + vollog.warning( + "Use of --write-config has been deprecated, replaced by --save-config " + ) + 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(): diff --git a/volatility3/cli/volshell/generic.py b/volatility3/cli/volshell/generic.py index 19e263a03..b95129d19 100644 --- a/volatility3/cli/volshell/generic.py +++ b/volatility3/cli/volshell/generic.py @@ -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) diff --git a/volatility3/cli/volshell/linux.py b/volatility3/cli/volshell/linux.py index 97a488743..c5e555ec7 100644 --- a/volatility3/cli/volshell/linux.py +++ b/volatility3/cli/volshell/linux.py @@ -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 diff --git a/volatility3/cli/volshell/mac.py b/volatility3/cli/volshell/mac.py index 305f80505..2b32ad677 100644 --- a/volatility3/cli/volshell/mac.py +++ b/volatility3/cli/volshell/mac.py @@ -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): diff --git a/volatility3/cli/volshell/windows.py b/volatility3/cli/volshell/windows.py index 2cc5d3e1d..5c2190c02 100644 --- a/volatility3/cli/volshell/windows.py +++ b/volatility3/cli/volshell/windows.py @@ -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: diff --git a/volatility3/framework/__init__.py b/volatility3/framework/__init__.py index 176eb2242..1565b2267 100644 --- a/volatility3/framework/__init__.py +++ b/volatility3/framework/__init__.py @@ -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") diff --git a/volatility3/framework/automagic/__init__.py b/volatility3/framework/automagic/__init__.py index 7567f206d..42c728b59 100644 --- a/volatility3/framework/automagic/__init__.py +++ b/volatility3/framework/automagic/__init__.py @@ -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) diff --git a/volatility3/framework/automagic/construct_layers.py b/volatility3/framework/automagic/construct_layers.py index 40a17419f..239f0cfb6 100644 --- a/volatility3/framework/automagic/construct_layers.py +++ b/volatility3/framework/automagic/construct_layers.py @@ -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) diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index f1d6c91e4..2eebcc2dc 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -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"] diff --git a/volatility3/framework/automagic/mac.py b/volatility3/framework/automagic/mac.py index c37aef463..e51753139 100644 --- a/volatility3/framework/automagic/mac.py +++ b/volatility3/framework/automagic/mac.py @@ -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(" 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(" 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"] diff --git a/volatility3/framework/automagic/module.py b/volatility3/framework/automagic/module.py index 3d2bb584a..ff13db905 100644 --- a/volatility3/framework/automagic/module.py +++ b/volatility3/framework/automagic/module.py @@ -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 diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index 5db66a3d0..06b2111b4 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -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") diff --git a/volatility3/framework/automagic/stacker.py b/volatility3/framework/automagic/stacker.py index 928e3d068..c251d3c46 100644 --- a/volatility3/framework/automagic/stacker.py +++ b/volatility3/framework/automagic/stacker.py @@ -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__) diff --git a/volatility3/framework/automagic/symbol_cache.py b/volatility3/framework/automagic/symbol_cache.py index 7b6adf9b4..22f1c94f3 100644 --- a/volatility3/framework/automagic/symbol_cache.py +++ b/volatility3/framework/automagic/symbol_cache.py @@ -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 diff --git a/volatility3/framework/automagic/symbol_finder.py b/volatility3/framework/automagic/symbol_finder.py index 143abd02e..21e594549 100644 --- a/volatility3/framework/automagic/symbol_finder.py +++ b/volatility3/framework/automagic/symbol_finder.py @@ -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? diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index f5dd720d6..52296f5ad 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -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, + ) ] diff --git a/volatility3/framework/configuration/requirements.py b/volatility3/framework/configuration/requirements.py index 4edc6d17c..1c0622574 100644 --- a/volatility3/framework/configuration/requirements.py +++ b/volatility3/framework/configuration/requirements.py @@ -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() diff --git a/volatility3/framework/constants/__init__.py b/volatility3/framework/constants/__init__.py index 472a743e6..09dded076 100644 --- a/volatility3/framework/constants/__init__.py +++ b/volatility3/framework/constants/__init__.py @@ -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 diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index c0f85593f..6e8883f19 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -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 diff --git a/volatility3/framework/contexts/__init__.py b/volatility3/framework/contexts/__init__.py index 85a7d32b7..73868a58f 100644 --- a/volatility3/framework/contexts/__init__.py +++ b/volatility3/framework/contexts/__init__.py @@ -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 + ) diff --git a/volatility3/framework/exceptions.py b/volatility3/framework/exceptions.py index a234a353a..f8701683b 100644 --- a/volatility3/framework/exceptions.py +++ b/volatility3/framework/exceptions.py @@ -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}" diff --git a/volatility3/framework/interfaces/__init__.py b/volatility3/framework/interfaces/__init__.py index 05cf7d837..51d81d63a 100644 --- a/volatility3/framework/interfaces/__init__.py +++ b/volatility3/framework/interfaces/__init__.py @@ -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, +) diff --git a/volatility3/framework/interfaces/automagic.py b/volatility3/framework/interfaces/automagic.py index c96c9bdbe..fe1361b30 100644 --- a/volatility3/framework/interfaces/automagic.py +++ b/volatility3/framework/interfaces/automagic.py @@ -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" + ) diff --git a/volatility3/framework/interfaces/configuration.py b/volatility3/framework/interfaces/configuration.py index c39dba680..3bb3cb019 100644 --- a/volatility3/framework/interfaces/configuration.py +++ b/volatility3/framework/interfaces/configuration.py @@ -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) diff --git a/volatility3/framework/interfaces/context.py b/volatility3/framework/interfaces/context.py index b8470ae47..a95f2b464 100644 --- a/volatility3/framework/interfaces/context.py +++ b/volatility3/framework/interfaces/context.py @@ -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: diff --git a/volatility3/framework/interfaces/layers.py b/volatility3/framework/interfaces/layers.py index a42282c39..e2a68780a 100644 --- a/volatility3/framework/interfaces/layers.py +++ b/volatility3/framework/interfaces/layers.py @@ -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] diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index 2240c58c9..ab568b927 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -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.""" diff --git a/volatility3/framework/interfaces/plugins.py b/volatility3/framework/interfaces/plugins.py index 983232cf0..29395aadf 100644 --- a/volatility3/framework/interfaces/plugins.py +++ b/volatility3/framework/interfaces/plugins.py @@ -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: diff --git a/volatility3/framework/interfaces/renderers.py b/volatility3/framework/interfaces/renderers.py index 9368009a9..b13de1834 100644 --- a/volatility3/framework/interfaces/renderers.py +++ b/volatility3/framework/interfaces/renderers.py @@ -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 diff --git a/volatility3/framework/interfaces/symbols.py b/volatility3/framework/interfaces/symbols.py index 1ad30cfdf..b645f5cd1 100644 --- a/volatility3/framework/interfaces/symbols.py +++ b/volatility3/framework/interfaces/symbols.py @@ -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]: diff --git a/volatility3/framework/layers/avml.py b/volatility3/framework/layers/avml.py index acc4493f4..2e5572192 100644 --- a/volatility3/framework/layers/avml.py +++ b/volatility3/framework/layers/avml.py @@ -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 = " None: base_layer = self.context.layers[self._base_layer] @@ -48,24 +103,38 @@ class AVMLLayer(segmented.NonLinearlySegmentedLayer): avml_header_structure = " 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 = '> 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("> 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) diff --git a/volatility3/framework/layers/cloudstorage.py b/volatility3/framework/layers/cloudstorage.py new file mode 100644 index 000000000..97ed54231 --- /dev/null +++ b/volatility3/framework/layers/cloudstorage.py @@ -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 diff --git a/volatility3/framework/layers/codecs/__init__.py b/volatility3/framework/layers/codecs/__init__.py index 550161e6d..e019bcbcd 100644 --- a/volatility3/framework/layers/codecs/__init__.py +++ b/volatility3/framework/layers/codecs/__init__.py @@ -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 diff --git a/volatility3/framework/layers/crash.py b/volatility3/framework/layers/crash.py index c690c8d8f..8efd4f7c7 100644 --- a/volatility3/framework/layers/crash.py +++ b/volatility3/framework/layers/crash.py @@ -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(' 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 diff --git a/volatility3/framework/layers/elf.py b/volatility3/framework/layers/elf.py index 4eb93a1c8..b2fd6d4d1 100644 --- a/volatility3/framework/layers/elf.py +++ b/volatility3/framework/layers/elf.py @@ -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(" 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 diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 723e4143b..ae477854d 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -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 = " 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) diff --git a/volatility3/framework/layers/leechcore.py b/volatility3/framework/layers/leechcore.py index 8c492ca85..542fd6ca2 100644 --- a/volatility3/framework/layers/leechcore.py +++ b/volatility3/framework/layers/leechcore.py @@ -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 diff --git a/volatility3/framework/layers/lime.py b/volatility3/framework/layers/lime.py index 4f4a66f18..8b93932ab 100644 --- a/volatility3/framework/layers/lime.py +++ b/volatility3/framework/layers/lime.py @@ -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(' 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) diff --git a/volatility3/framework/layers/linear.py b/volatility3/framework/layers/linear.py index c5cb47bdc..47170df7b 100644 --- a/volatility3/framework/layers/linear.py +++ b/volatility3/framework/layers/linear.py @@ -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) diff --git a/volatility3/framework/layers/msf.py b/volatility3/framework/layers/msf.py index 02fc570bc..8d84a774b 100644 --- a/volatility3/framework/layers/msf.py +++ b/volatility3/framework/layers/msf.py @@ -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 diff --git a/volatility3/framework/layers/physical.py b/volatility3/framework/layers/physical.py index 5cf0b776d..70dd9541a 100644 --- a/volatility3/framework/layers/physical.py +++ b/volatility3/framework/layers/physical.py @@ -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)] diff --git a/volatility3/framework/layers/qemu.py b/volatility3/framework/layers/qemu.py index 907116e99..ff483291c 100644 --- a/volatility3/framework/layers/qemu.py +++ b/volatility3/framework/layers/qemu.py @@ -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 diff --git a/volatility3/framework/layers/registry.py b/volatility3/framework/layers/registry.py index 55a6e5186..9841d2bb0 100644 --- a/volatility3/framework/layers/registry.py +++ b/volatility3/framework/layers/registry.py @@ -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: diff --git a/volatility3/framework/layers/resources.py b/volatility3/framework/layers/resources.py index 8a0e96208..a64fa7d7a 100644 --- a/volatility3/framework/layers/resources.py +++ b/volatility3/framework/layers/resources.py @@ -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 diff --git a/volatility3/framework/layers/scanners/__init__.py b/volatility3/framework/layers/scanners/__init__.py index ec66f2708..dd8dc46be 100644 --- a/volatility3/framework/layers/scanners/__init__.py +++ b/volatility3/framework/layers/scanners/__init__.py @@ -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() diff --git a/volatility3/framework/layers/scanners/multiregexp.py b/volatility3/framework/layers/scanners/multiregexp.py index 45feb51d1..be3581f05 100644 --- a/volatility3/framework/layers/scanners/multiregexp.py +++ b/volatility3/framework/layers/scanners/multiregexp.py @@ -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()) diff --git a/volatility3/framework/layers/segmented.py b/volatility3/framework/layers/segmented.py index 05fc01b97..0d29d8bff 100644 --- a/volatility3/framework/layers/segmented.py +++ b/volatility3/framework/layers/segmented.py @@ -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 diff --git a/volatility3/framework/layers/vmware.py b/volatility3/framework/layers/vmware.py index 85e961b24..622ff0250 100644 --- a/volatility3/framework/layers/vmware.py +++ b/volatility3/framework/layers/vmware.py @@ -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 diff --git a/volatility3/framework/layers/xen.py b/volatility3/framework/layers/xen.py new file mode 100644 index 000000000..927b30430 --- /dev/null +++ b/volatility3/framework/layers/xen.py @@ -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(" 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) diff --git a/volatility3/framework/objects/__init__.py b/volatility3/framework/objects/__init__.py index e0f927ec9..316a30bec 100644 --- a/volatility3/framework/objects/__init__.py +++ b/volatility3/framework/objects/__init__.py @@ -6,28 +6,51 @@ import collections import collections.abc import logging import struct -from typing import Any, ClassVar, Dict, Iterable, List, Optional, Tuple, Type, Union as TUnion, overload +from typing import ( + Any, + ClassVar, + Dict, + Iterable, + List, + Optional, + Tuple, + Type, + Union as TUnion, + overload, +) from volatility3.framework import constants, interfaces -from volatility3.framework.objects import templates, utility +from volatility3.framework.objects import templates vollog = logging.getLogger(__name__) -DataFormatInfo = collections.namedtuple('DataFormatInfo', ['length', 'byteorder', 'signed']) +DataFormatInfo = collections.namedtuple( + "DataFormatInfo", ["length", "byteorder", "signed"] +) -def convert_data_to_value(data: bytes, struct_type: Type[TUnion[int, float, bytes, str, bool]], - data_format: DataFormatInfo) -> TUnion[int, float, bytes, str, bool]: +def convert_data_to_value( + data: bytes, + struct_type: Type[TUnion[int, float, bytes, str, bool]], + data_format: DataFormatInfo, +) -> TUnion[int, float, bytes, str, bool]: """Converts a series of bytes to a particular type of value.""" if struct_type == int: - return int.from_bytes(data, byteorder = data_format.byteorder, signed = data_format.signed) + return int.from_bytes( + data, byteorder=data_format.byteorder, signed=data_format.signed + ) if struct_type == bool: struct_format = "?" elif struct_type == float: float_vals = "zzezfzzzd" - if data_format.length > len(float_vals) or float_vals[data_format.length] not in "efd": + if ( + data_format.length > len(float_vals) + or float_vals[data_format.length] not in "efd" + ): raise ValueError("Invalid float size") - struct_format = ("<" if data_format.byteorder == 'little' else ">") + float_vals[data_format.length] + struct_format = ( + "<" if data_format.byteorder == "little" else ">" + ) + float_vals[data_format.length] elif struct_type in [bytes, str]: struct_format = str(data_format.length) + "s" else: @@ -36,29 +59,40 @@ def convert_data_to_value(data: bytes, struct_type: Type[TUnion[int, float, byte return struct.unpack(struct_format, data)[0] -def convert_value_to_data(value: TUnion[int, float, bytes, str, bool], struct_type: Type[TUnion[int, float, bytes, str, - bool]], - data_format: DataFormatInfo) -> bytes: +def convert_value_to_data( + value: TUnion[int, float, bytes, str, bool], + struct_type: Type[TUnion[int, float, bytes, str, bool]], + data_format: DataFormatInfo, +) -> bytes: """Converts a particular value to a series of bytes.""" if not isinstance(value, struct_type): - raise TypeError(f"Written value is not of the correct type for {struct_type.__name__}") + raise TypeError( + f"Written value is not of the correct type for {struct_type.__name__}" + ) if struct_type == int and isinstance(value, int): # Doubling up on the isinstance is for mypy - return int.to_bytes(value, - length = data_format.length, - byteorder = data_format.byteorder, - signed = data_format.signed) + return int.to_bytes( + value, + length=data_format.length, + byteorder=data_format.byteorder, + signed=data_format.signed, + ) if struct_type == bool: struct_format = "?" elif struct_type == float: float_vals = "zzezfzzzd" - if data_format.length > len(float_vals) or float_vals[data_format.length] not in "efd": + if ( + data_format.length > len(float_vals) + or float_vals[data_format.length] not in "efd" + ): raise ValueError("Invalid float size") - struct_format = ("<" if data_format.byteorder == 'little' else ">") + float_vals[data_format.length] + struct_format = ( + "<" if data_format.byteorder == "little" else ">" + ) + float_vals[data_format.length] elif struct_type in [bytes, str]: if isinstance(value, str): - value = bytes(value, 'latin-1') + value = bytes(value, "latin-1") struct_format = str(data_format.length) + "s" else: raise TypeError(f"Cannot construct struct format for type {type(struct_type)}") @@ -70,7 +104,6 @@ class Void(interfaces.objects.ObjectInterface): """Returns an object to represent void/unknown types.""" class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Dummy size for Void objects. @@ -95,20 +128,33 @@ class Function(interfaces.objects.ObjectInterface): class PrimitiveObject(interfaces.objects.ObjectInterface): """PrimitiveObject is an interface for any objects that should simulate a Python primitive.""" + _struct_type: ClassVar[Type] = int - def __init__(self, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, data_format: DataFormatInfo) -> None: - super().__init__(context = context, type_name = type_name, object_info = object_info, data_format = data_format) + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + data_format: DataFormatInfo, + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + data_format=data_format, + ) self._data_format = data_format - def __new__(cls: Type, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - data_format: DataFormatInfo, - new_value: TUnion[int, float, bool, bytes, str] = None, - **kwargs) -> 'PrimitiveObject': + def __new__( + cls: Type, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + data_format: DataFormatInfo, + new_value: TUnion[int, float, bool, bytes, str] = None, + **kwargs, + ) -> "PrimitiveObject": """Creates the appropriate class and returns it so that the native type is inherited. @@ -135,26 +181,38 @@ class PrimitiveObject(interfaces.objects.ObjectInterface): for k, v in self._vol.maps[-1].items(): if k not in ["context", "data_format", "object_info", "type_name"]: kwargs[k] = v - kwargs['new_value'] = self.__new_value - return (self._context, self._vol.maps[-3]['type_name'], self._vol.maps[-2], self._data_format), kwargs + kwargs["new_value"] = self.__new_value + return ( + self._context, + self._vol.maps[-3]["type_name"], + self._vol.maps[-2], + self._data_format, + ), kwargs @classmethod - def _unmarshall(cls, context: interfaces.context.ContextInterface, data_format: DataFormatInfo, - object_info: interfaces.objects.ObjectInformation) -> TUnion[int, float, bool, bytes, str]: + def _unmarshall( + cls, + context: interfaces.context.ContextInterface, + data_format: DataFormatInfo, + object_info: interfaces.objects.ObjectInformation, + ) -> TUnion[int, float, bool, bytes, str]: # Don't try to lookup a 0 length data format, incase it's at an invalid offset. Length 0 means b'' - data = b'' + data = b"" if data_format.length > 0: - data = context.layers.read(object_info.layer_name, object_info.offset, data_format.length) + data = context.layers.read( + object_info.layer_name, object_info.offset, data_format.length + ) return convert_data_to_value(data, cls._struct_type, data_format) class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the templated object.""" return template.vol.data_format.length - def write(self, value: TUnion[int, float, bool, bytes, str]) -> interfaces.objects.ObjectInterface: + def write( + self, value: TUnion[int, float, bool, bytes, str] + ) -> interfaces.objects.ObjectInterface: """Writes the object into the layer of the context at the current offset.""" data = convert_value_to_data(value, self._struct_type, self._data_format) @@ -167,6 +225,7 @@ class PrimitiveObject(interfaces.objects.ObjectInterface): # https://mail.python.org/pipermail/python-dev/2004-February/042537.html class Boolean(PrimitiveObject, int): """Primitive Object that handles boolean types.""" + _struct_type: ClassVar[Type] = int @@ -176,35 +235,44 @@ class Integer(PrimitiveObject, int): class Float(PrimitiveObject, float): """Primitive Object that handles double or floating point numbers.""" + _struct_type: ClassVar[Type] = float class Char(PrimitiveObject, int): """Primitive Object that handles characters.""" + _struct_type: ClassVar[Type] = int class Bytes(PrimitiveObject, bytes): """Primitive Object that handles specific series of bytes.""" + _struct_type: ClassVar[Type] = bytes - def __init__(self, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - length: int = 1) -> None: - super().__init__(context = context, - type_name = type_name, - object_info = object_info, - data_format = DataFormatInfo(length, "big", False)) - self._vol['length'] = length + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + length: int = 1, + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + data_format=DataFormatInfo(length, "big", False), + ) + self._vol["length"] = length - def __new__(cls: Type, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - length: int = 1, - **kwargs) -> 'Bytes': + def __new__( + cls: Type, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + length: int = 1, + **kwargs, + ) -> "Bytes": """Creates the appropriate class and returns it so that the native type is inherited. @@ -213,11 +281,15 @@ class Bytes(PrimitiveObject, bytes): override __new__ """ return cls._struct_type.__new__( - cls, cls._unmarshall(context, data_format = DataFormatInfo(length, "big", False), - object_info = object_info)) + cls, + cls._unmarshall( + context, + data_format=DataFormatInfo(length, "big", False), + object_info=object_info, + ), + ) class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: return template.vol.length @@ -230,31 +302,38 @@ class String(PrimitiveObject, str): max_length: specifies the maximum possible length that the string could hold within memory (for multibyte characters, this will not be the maximum length of the string) """ + _struct_type: ClassVar[Type] = str - def __init__(self, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - max_length: int = 1, - encoding: str = "utf-8", - errors: str = "strict") -> None: - super().__init__(context = context, - type_name = type_name, - object_info = object_info, - data_format = DataFormatInfo(max_length, "big", False)) + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + max_length: int = 1, + encoding: str = "utf-8", + errors: str = "strict", + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + data_format=DataFormatInfo(max_length, "big", False), + ) self._vol["max_length"] = max_length - self._vol['encoding'] = encoding - self._vol['errors'] = errors + self._vol["encoding"] = encoding + self._vol["errors"] = errors - def __new__(cls: Type, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - max_length: int = 1, - encoding: str = "utf-8", - errors: str = "strict", - **kwargs) -> 'String': + def __new__( + cls: Type, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + max_length: int = 1, + encoding: str = "utf-8", + errors: str = "strict", + **kwargs, + ) -> "String": """Creates the appropriate class and returns it so that the native type is inherited. @@ -264,20 +343,24 @@ class String(PrimitiveObject, str): """ params = {} if encoding: - params['encoding'] = encoding + params["encoding"] = encoding if errors: - params['errors'] = errors + params["errors"] = errors # Pass the encoding and error parameters to the string constructor to appropriately encode the string value = cls._struct_type.__new__( cls, - cls._unmarshall(context, data_format = DataFormatInfo(max_length, "big", False), object_info = object_info), - **params) - if value.find('\x00') >= 0: - value = value[:value.find('\x00')] + cls._unmarshall( + context, + data_format=DataFormatInfo(max_length, "big", False), + object_info=object_info, + ), + **params, + ) + if value.find("\x00") >= 0: + value = value[: value.find("\x00")] return value class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the templated object.""" @@ -287,19 +370,30 @@ class String(PrimitiveObject, str): class Pointer(Integer): """Pointer which points to another object.""" - def __init__(self, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - data_format: DataFormatInfo, - subtype: Optional[templates.ObjectTemplate] = None) -> None: - super().__init__(context = context, object_info = object_info, type_name = type_name, data_format = data_format) - self._vol['subtype'] = subtype + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + data_format: DataFormatInfo, + subtype: Optional[templates.ObjectTemplate] = None, + ) -> None: + super().__init__( + context=context, + object_info=object_info, + type_name=type_name, + data_format=data_format, + ) + self._vol["subtype"] = subtype self._cache: Dict[str, interfaces.objects.ObjectInterface] = {} @classmethod - def _unmarshall(cls, context: interfaces.context.ContextInterface, data_format: DataFormatInfo, - object_info: interfaces.objects.ObjectInformation) -> Any: + def _unmarshall( + cls, + context: interfaces.context.ContextInterface, + data_format: DataFormatInfo, + object_info: interfaces.objects.ObjectInformation, + ) -> Any: """Ensure that pointer values always fall within the domain of the layer they're constructed on. @@ -312,10 +406,12 @@ class Pointer(Integer): raise ValueError("Pointers cannot have signed values") mask = context.layers[object_info.native_layer_name].address_mask data = context.layers.read(object_info.layer_name, object_info.offset, length) - value = int.from_bytes(data, byteorder = endian, signed = signed) + value = int.from_bytes(data, byteorder=endian, signed=signed) return value & mask - def dereference(self, layer_name: Optional[str] = None) -> interfaces.objects.ObjectInterface: + def dereference( + self, layer_name: Optional[str] = None + ) -> interfaces.objects.ObjectInterface: """Dereferences the pointer. Layer_name is identifies the appropriate layer within the @@ -332,126 +428,156 @@ class Pointer(Integer): layer_name = layer_name or self.vol.native_layer_name mask = self._context.layers[layer_name].address_mask offset = self & mask - self._cache[layer_name] = self.vol.subtype(context = self._context, - object_info = interfaces.objects.ObjectInformation( - layer_name = layer_name, - offset = offset, - parent = self, - size = self.vol.subtype.size)) + self._cache[layer_name] = self.vol.subtype( + context=self._context, + object_info=interfaces.objects.ObjectInformation( + layer_name=layer_name, + offset=offset, + parent=self, + size=self.vol.subtype.size, + ), + ) return self._cache[layer_name] def is_readable(self, layer_name: Optional[str] = None) -> bool: """Determines whether the address of this pointer can be read from memory.""" - layer_name = layer_name or self.vol.layer_name + layer_name = layer_name or self.vol.native_layer_name return self._context.layers[layer_name].is_valid(self, self.vol.subtype.size) def __getattr__(self, attr: str) -> Any: """Convenience function to access unknown attributes by getting them from the subtype object.""" - if attr in ['vol', '_vol', '_cache']: + if attr in ["vol", "_vol", "_cache"]: raise AttributeError("Pointer not initialized before use") return getattr(self.dereference(), attr) def has_member(self, member_name: str) -> bool: """Returns whether the dereferenced type has this member.""" - return self._vol['subtype'].has_member(member_name) + return self._vol["subtype"].has_member(member_name) class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: return Integer.VolTemplateProxy.size(template) @classmethod - def children(cls, template: interfaces.objects.Template) -> List[interfaces.objects.Template]: + def children( + cls, template: interfaces.objects.Template + ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" - if 'subtype' in template.vol: + if "subtype" in template.vol: return [template.vol.subtype] return [] @classmethod - def replace_child(cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, - new_child: interfaces.objects.Template) -> None: + def replace_child( + cls, + template: interfaces.objects.Template, + old_child: interfaces.objects.Template, + new_child: interfaces.objects.Template, + ) -> None: """Substitutes the old_child for the new_child.""" - if 'subtype' in template.vol: + if "subtype" in template.vol: if template.vol.subtype == old_child: - template.update_vol(subtype = new_child) + template.update_vol(subtype=new_child) @classmethod - def has_member(cls, template: interfaces.objects.Template, member_name: str) -> bool: - return template.vol['subtype'].has_member(member_name) + def has_member( + cls, template: interfaces.objects.Template, member_name: str + ) -> bool: + return template.vol["subtype"].has_member(member_name) class BitField(interfaces.objects.ObjectInterface, int): """Object containing a field which is made up of bits rather than whole bytes.""" - def __init__(self, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - base_type: interfaces.objects.Template, - start_bit: int = 0, - end_bit: int = 0) -> None: + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + base_type: interfaces.objects.Template, + start_bit: int = 0, + end_bit: int = 0, + ) -> None: super().__init__(context, type_name, object_info) - self._vol['base_type'] = base_type - self._vol['start_bit'] = start_bit - self._vol['end_bit'] = end_bit + self._vol["base_type"] = base_type + self._vol["start_bit"] = start_bit + self._vol["end_bit"] = end_bit - def __new__(cls, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - base_type: interfaces.objects.Template, - start_bit: int = 0, - end_bit: int = 0, - **kwargs) -> 'BitField': - value = base_type(context = context, object_info = object_info) + def __new__( + cls, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + base_type: interfaces.objects.Template, + start_bit: int = 0, + end_bit: int = 0, + **kwargs, + ) -> "BitField": + value = base_type(context=context, object_info=object_info) return int.__new__(cls, ((value & ((1 << end_bit) - 1)) >> start_bit)) # type: ignore def write(self, value): raise NotImplementedError("Writing to BitFields is not yet implemented") class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: return template.vol.base_type.size @classmethod - def children(cls, template: interfaces.objects.Template) -> List[interfaces.objects.Template]: + def children( + cls, template: interfaces.objects.Template + ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" - if 'base_type' in template.vol: + if "base_type" in template.vol: return [template.vol.base_type] return [] @classmethod - def replace_child(cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, - new_child: interfaces.objects.Template) -> None: + def replace_child( + cls, + template: interfaces.objects.Template, + old_child: interfaces.objects.Template, + new_child: interfaces.objects.Template, + ) -> None: """Substitutes the old_child for the new_child.""" - if 'base_type' in template.vol: + if "base_type" in template.vol: if template.vol.base_type == old_child: - template.update_vol(base_type = new_child) + template.update_vol(base_type=new_child) class Enumeration(interfaces.objects.ObjectInterface, int): """Returns an object made up of choices.""" - def __new__(cls, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, base_type: interfaces.objects.Template, - choices: Dict[str, int], **kwargs) -> 'Enumeration': - value = base_type(context = context, object_info = object_info) + def __new__( + cls, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + base_type: interfaces.objects.Template, + choices: Dict[str, int], + **kwargs, + ) -> "Enumeration": + value = base_type(context=context, object_info=object_info) return int.__new__(cls, value) # type: ignore - def __init__(self, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, base_type: Integer, choices: Dict[str, - int]) -> None: + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + base_type: Integer, + choices: Dict[str, int], + ) -> None: super().__init__(context, type_name, object_info) self._inverse_choices = self._generate_inverse_choices(choices) - self._vol['choices'] = choices + self._vol["choices"] = choices - self._vol['base_type'] = base_type + self._vol["base_type"] = base_type def __eq__(self, other): """An enumeration must be equivalent to its value, even if the other value is not an enumeration""" @@ -467,15 +593,19 @@ class Enumeration(interfaces.objects.ObjectInterface, int): inverse_choices: Dict[int, str] = {} for k, v in choices.items(): if v in inverse_choices: - # Technically this shouldn't be a problem, but since we inverse cache - # and can't map one value to two possibilities we throw an exception during build - # We can remove/work around this if it proves a common issue - raise ValueError(f"Enumeration value {v} duplicated as {k} and {inverse_choices[v]}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Enumeration value {v} duplicated as {k}. Keeping name {inverse_choices[v]}", + ) + continue inverse_choices[v] = k return inverse_choices def lookup(self, value: int = None) -> str: - """Looks up an individual value and returns the associated name.""" + """Looks up an individual value and returns the associated name. + + If multiple identifiers map to the same value, the first matching identifier will be returned + """ if value is None: return self.lookup(self) if value in self._inverse_choices: @@ -489,7 +619,7 @@ class Enumeration(interfaces.objects.ObjectInterface, int): @property def choices(self) -> Dict[str, int]: - return self._vol['choices'] + return self._vol["choices"] @property def is_valid_choice(self) -> bool: @@ -498,59 +628,76 @@ class Enumeration(interfaces.objects.ObjectInterface, int): def __getattr__(self, attr: str) -> str: """Returns the value for a specific name.""" - if attr in self._vol['choices']: - return self._vol['choices'][attr] - raise AttributeError(f"Unknown attribute {attr} for Enumeration {self._vol['type_name']}") + if attr in self._vol["choices"]: + return self._vol["choices"][attr] + raise AttributeError( + f"Unknown attribute {attr} for Enumeration {self._vol['type_name']}" + ) def write(self, value: bytes): raise NotImplementedError("Writing to Enumerations is not yet implemented") class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - _methods = ['lookup'] + _methods = ["lookup"] @classmethod def lookup(cls, template: interfaces.objects.Template, value: int) -> str: - """Looks up an individual value and returns the associated name.""" - _inverse_choices = Enumeration._generate_inverse_choices(template.vol['choices']) + """Looks up an individual value and returns the associated name. + + If multiple identifiers map to the same value, the first matching identifier will be returned + """ + _inverse_choices = Enumeration._generate_inverse_choices( + template.vol["choices"] + ) if value in _inverse_choices: return _inverse_choices[value] - raise ValueError("The value of the enumeration is outside the possible choices") + raise ValueError( + "The value of the enumeration is outside the possible choices" + ) @classmethod def size(cls, template: interfaces.objects.Template) -> int: - return template.vol['base_type'].size + return template.vol["base_type"].size @classmethod - def children(cls, template: interfaces.objects.Template) -> List[interfaces.objects.Template]: + def children( + cls, template: interfaces.objects.Template + ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" - if 'base_type' in template.vol: + if "base_type" in template.vol: return [template.vol.base_type] return [] @classmethod - def replace_child(cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, - new_child: interfaces.objects.Template) -> None: + def replace_child( + cls, + template: interfaces.objects.Template, + old_child: interfaces.objects.Template, + new_child: interfaces.objects.Template, + ) -> None: """Substitutes the old_child for the new_child.""" - if 'base_type' in template.vol: + if "base_type" in template.vol: if template.vol.base_type == old_child: - template.update_vol(base_type = new_child) + template.update_vol(base_type=new_child) class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): """Object which can contain a fixed number of an object type.""" - def __init__(self, - context: interfaces.context.ContextInterface, - type_name: str, - object_info: interfaces.objects.ObjectInformation, - count: int = 0, - subtype: templates.ObjectTemplate = None) -> None: - super().__init__(context = context, type_name = type_name, object_info = object_info) - self._vol['count'] = count - self._vol['subtype'] = subtype - self._vol['size'] = 0 + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + count: int = 0, + subtype: templates.ObjectTemplate = None, + ) -> None: + super().__init__(context=context, type_name=type_name, object_info=object_info) + self._vol["count"] = count + self._vol["subtype"] = subtype + self._vol["size"] = 0 if subtype is not None: - self._vol['size'] = count * subtype.size + self._vol["size"] = count * subtype.size # This overrides the little known Sequence.count(val) that returns the number of items in the list that match val # Changing the name would be confusing (since we use count of an array everywhere else), so this is more important @@ -562,53 +709,69 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): @count.setter def count(self, value: int) -> None: """Sets the count to a specific value.""" - self._vol['count'] = value - self._vol['size'] = value * self._vol['subtype'].size + self._vol["count"] = value + self._vol["size"] = value * self._vol["subtype"].size def __repr__(self) -> str: """Describes the object appropriately""" return AggregateType.__repr__(self) class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Returns the size of the array, based on the count and the subtype.""" - if 'subtype' not in template.vol and 'count' not in template.vol: - raise ValueError("Array ObjectTemplate must be provided a count and subtype") - return template.vol.get('subtype', None).size * template.vol.get('count', 0) + if "subtype" not in template.vol and "count" not in template.vol: + raise ValueError( + "Array ObjectTemplate must be provided a count and subtype" + ) + return template.vol.get("subtype", None).size * template.vol.get("count", 0) @classmethod - def children(cls, template: interfaces.objects.Template) -> List[interfaces.objects.Template]: + def children( + cls, template: interfaces.objects.Template + ) -> List[interfaces.objects.Template]: """Returns the children of the template.""" - if 'subtype' in template.vol: + if "subtype" in template.vol: return [template.vol.subtype] return [] @classmethod - def replace_child(cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, - new_child: interfaces.objects.Template) -> None: + def replace_child( + cls, + template: interfaces.objects.Template, + old_child: interfaces.objects.Template, + new_child: interfaces.objects.Template, + ) -> None: """Substitutes the old_child for the new_child.""" - if 'subtype' in template.vol: - if template.vol['subtype'] == old_child: - template.update_vol(subtype = new_child) + if "subtype" in template.vol: + if template.vol["subtype"] == old_child: + template.update_vol(subtype=new_child) @classmethod - def relative_child_offset(cls, template: interfaces.objects.Template, child: str) -> int: + def relative_child_offset( + cls, template: interfaces.objects.Template, child: str + ) -> int: """Returns the relative offset from the head of the parent data to the child member.""" - if 'subtype' in template.vol and child == 'subtype': + if "subtype" in template.vol and child == "subtype": return 0 raise IndexError(f"Member not present in array template: {child}") - @overload - def __getitem__(self, i: int) -> interfaces.objects.Template: - ... + @classmethod + def child_template( + cls, template: interfaces.objects.Template, child: str + ) -> interfaces.objects.Template: + """Returns the template of the child member.""" + if "subtype" in template.vol and child == "subtype": + return template.vol.subtype + raise IndexError(f"Member not present in array template: {child}") @overload - def __getitem__(self, s: slice) -> List[interfaces.objects.Template]: - ... + def __getitem__(self, i: int) -> interfaces.objects.Template: ... + + @overload + def __getitem__(self, s: slice) -> List[interfaces.objects.Template]: ... def __getitem__(self, i): """Returns the i-th item from the array.""" @@ -622,12 +785,13 @@ class Array(interfaces.objects.ObjectInterface, collections.abc.Sequence): series = [series] for index in series: object_info = interfaces.objects.ObjectInformation( - layer_name = self.vol.layer_name, - offset = mask & (self.vol.offset + (self.vol.subtype.size * index)), - parent = self, - native_layer_name = self.vol.native_layer_name, - size = self.vol.subtype.size) - result += [self.vol.subtype(context = self._context, object_info = object_info)] + layer_name=self.vol.layer_name, + offset=mask & (self.vol.offset + (self.vol.subtype.size * index)), + parent=self, + native_layer_name=self.vol.native_layer_name, + size=self.vol.subtype.size, + ) + result += [self.vol.subtype(context=self._context, object_info=object_info)] if not return_list: return result[0] return result @@ -651,14 +815,21 @@ class AggregateType(interfaces.objects.ObjectInterface): each one could overload a valid member. """ - def __init__(self, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, size: int, - members: Dict[str, Tuple[int, interfaces.objects.Template]]) -> None: - super().__init__(context = context, - type_name = type_name, - object_info = object_info, - size = size, - members = members) + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + size: int, + members: Dict[str, Tuple[int, interfaces.objects.Template]], + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + size=size, + members=members, + ) # self._check_members(members) self._concrete_members: Dict[str, Dict] = {} @@ -669,7 +840,7 @@ class AggregateType(interfaces.objects.ObjectInterface): def __repr__(self) -> str: """Describes the object appropriately""" - extras = member_name = '' + extras = member_name = "" if self.vol.native_layer_name != self.vol.layer_name: extras += f" (Native: {self.vol.native_layer_name})" if self.vol.member_name: @@ -677,25 +848,30 @@ class AggregateType(interfaces.objects.ObjectInterface): return f"<{self.__class__.__name__} {self.vol.type_name}{member_name}: {self.vol.layer_name} @ 0x{self.vol.offset:x} #{self.vol.size}{extras}>" class VolTemplateProxy(interfaces.objects.ObjectInterface.VolTemplateProxy): - @classmethod def size(cls, template: interfaces.objects.Template) -> int: """Method to return the size of this type.""" - if template.vol.get('size', None) is None: + if template.vol.get("size", None) is None: raise ValueError("ObjectTemplate not provided with a size") return template.vol.size @classmethod - def children(cls, template: interfaces.objects.Template) -> List[interfaces.objects.Template]: + def children( + cls, template: interfaces.objects.Template + ) -> List[interfaces.objects.Template]: """Method to list children of a template.""" return [member for _, member in template.vol.members.values()] @classmethod - def replace_child(cls, template: interfaces.objects.Template, old_child: interfaces.objects.Template, - new_child: interfaces.objects.Template) -> None: + def replace_child( + cls, + template: interfaces.objects.Template, + old_child: interfaces.objects.Template, + new_child: interfaces.objects.Template, + ) -> None: """Replace a child elements within the arguments handed to the template.""" - for member in template.vol.members.get('members', {}): + for member in template.vol.members.get("members", {}): relative_offset, member_template = template.vol.members[member] if member_template == old_child: # Members will give access to the mutable members list, @@ -705,10 +881,12 @@ class AggregateType(interfaces.objects.ObjectInterface): # If there's trouble with mutability, consider making update_vol return a clone with the changes # (there will be a few other places that will be necessary) and/or making these part of the # permanent dictionaries rather than the non-cloneable ones - template.update_vol(members = tmp_list) + template.update_vol(members=tmp_list) @classmethod - def relative_child_offset(cls, template: interfaces.objects.Template, child: str) -> int: + def relative_child_offset( + cls, template: interfaces.objects.Template, child: str + ) -> int: """Returns the relative offset of a child to its parent.""" retlist = template.vol.members.get(child, None) if retlist is None: @@ -716,60 +894,83 @@ class AggregateType(interfaces.objects.ObjectInterface): return retlist[0] @classmethod - def has_member(cls, template: interfaces.objects.Template, member_name: str) -> bool: + def child_template( + cls, template: interfaces.objects.Template, child: str + ) -> interfaces.objects.Template: + """Returns the template of a child to its parent.""" + retlist = template.vol.members.get(child, None) + if retlist is None: + raise IndexError(f"Member not present in template: {child}") + return retlist[1] + + @classmethod + def has_member( + cls, template: interfaces.objects.Template, member_name: str + ) -> bool: """Returns whether the object would contain a member called member_name.""" return member_name in template.vol.members @classmethod - def _check_members(cls, members: Dict[str, Tuple[int, interfaces.objects.Template]]) -> None: + def _check_members( + cls, members: Dict[str, Tuple[int, interfaces.objects.Template]] + ) -> None: # Members should be an iterable mapping of symbol names to tuples of (relative_offset, ObjectTemplate) # An object template is a callable that when called with a context, offset, layer_name and type_name # We duplicate this code to avoid polluting the methodspace - agg_name = 'AggregateType' + agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(cls, agg_type): agg_name = agg_type.__name__ - assert isinstance(members, collections.abc.Mapping) - f"{agg_name} members parameter must be a mapping: {type(members)}" - assert all([(isinstance(member, tuple) and len(member) == 2) for member in members.values()]) - f"{agg_name} members must be a tuple of relative_offsets and templates" + assert isinstance( + members, collections.abc.Mapping + ), f"{agg_name} members parameter must be a mapping: {type(members)}" + assert all( + [ + (isinstance(member, tuple) and len(member) == 2) + for member in members.values() + ] + ), f"{agg_name} members must be a tuple of relative_offsets and templates" - def member(self, attr: str = 'member') -> object: + def member(self, attr: str = "member") -> object: """Specifically named method for retrieving members.""" return self.__getattr__(attr) def __getattr__(self, attr: str) -> Any: """Method for accessing members of the type.""" - if attr in ['_concrete_members', 'vol']: + if attr in ["_concrete_members", "vol"]: raise AttributeError("Object has not been properly initialized") if attr in self._concrete_members: return self._concrete_members[attr] if attr.startswith("_") and not attr.startswith("__") and "__" in attr: - attr = attr[attr.find("__", 1):] # See issue #522 + attr = attr[attr.find("__", 1) :] # See issue #522 if attr in self.vol.members: mask = self._context.layers[self.vol.layer_name].address_mask relative_offset, template = self.vol.members[attr] if isinstance(template, templates.ReferenceTemplate): template = self._context.symbol_space.get_type(template.vol.type_name) - object_info = interfaces.objects.ObjectInformation(layer_name = self.vol.layer_name, - offset = mask & (self.vol.offset + relative_offset), - member_name = attr, - parent = self, - native_layer_name = self.vol.native_layer_name, - size = template.size) - member = template(context = self._context, object_info = object_info) + object_info = interfaces.objects.ObjectInformation( + layer_name=self.vol.layer_name, + offset=mask & (self.vol.offset + relative_offset), + member_name=attr, + parent=self, + native_layer_name=self.vol.native_layer_name, + size=template.size, + ) + member = template(context=self._context, object_info=object_info) self._concrete_members[attr] = member return member # We duplicate this code to avoid polluting the methodspace - agg_name = 'AggregateType' + agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(self, agg_type): agg_name = agg_type.__name__ - raise AttributeError(f"{agg_name} has no attribute: {self.vol.type_name}.{attr}") + raise AttributeError( + f"{agg_name} has no attribute: {self.vol.type_name}.{attr}" + ) # Disable messing around with setattr until the consequences have been considered properly # For example pdbutil constructs objects and then sets values for them @@ -788,12 +989,13 @@ class AggregateType(interfaces.objects.ObjectInterface): def write(self, value): # We duplicate this code to avoid polluting the methodspace - agg_name = 'AggregateType' + agg_name = "AggregateType" for agg_type in AggregateTypes: if isinstance(self, agg_type): agg_name = agg_type.__name__ raise TypeError( - f"{agg_name}s cannot be written to directly, individual members must be written instead") + f"{agg_name}s cannot be written to directly, individual members must be written instead" + ) class StructType(AggregateType): @@ -808,4 +1010,4 @@ class ClassType(AggregateType): pass -AggregateTypes = {StructType: 'struct', UnionType: 'union', ClassType: 'class'} +AggregateTypes = {StructType: "struct", UnionType: "union", ClassType: "class"} diff --git a/volatility3/framework/objects/templates.py b/volatility3/framework/objects/templates.py index b544d117f..7782029bc 100644 --- a/volatility3/framework/objects/templates.py +++ b/volatility3/framework/objects/templates.py @@ -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) diff --git a/volatility3/framework/objects/utility.py b/volatility3/framework/objects/utility.py index 9c4b59575..0292608c1 100644 --- a/volatility3/framework/objects/utility.py +++ b/volatility3/framework/objects/utility.py @@ -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) diff --git a/volatility3/framework/plugins/__init__.py b/volatility3/framework/plugins/__init__.py index 7dbce5208..5c3a03152 100644 --- a/volatility3/framework/plugins/__init__.py +++ b/volatility3/framework/plugins/__init__.py @@ -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 diff --git a/volatility3/framework/plugins/banners.py b/volatility3/framework/plugins/banners.py index ac2006207..b3c2fd3a5 100644 --- a/volatility3/framework/plugins/banners.py +++ b/volatility3/framework/plugins/banners.py @@ -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() + ) diff --git a/volatility3/framework/plugins/configwriter.py b/volatility3/framework/plugins/configwriter.py index f0979eb4d..eca01a84a 100644 --- a/volatility3/framework/plugins/configwriter.py +++ b/volatility3/framework/plugins/configwriter.py @@ -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}") diff --git a/volatility3/framework/plugins/frameworkinfo.py b/volatility3/framework/plugins/frameworkinfo.py index b7c887d5c..39b7f3bdc 100644 --- a/volatility3/framework/plugins/frameworkinfo.py +++ b/volatility3/framework/plugins/frameworkinfo.py @@ -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()) diff --git a/volatility3/framework/plugins/isfinfo.py b/volatility3/framework/plugins/isfinfo.py index 575f25426..4f07bd5a8 100644 --- a/volatility3/framework/plugins/isfinfo.py +++ b/volatility3/framework/plugins/isfinfo.py @@ -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(), + ) diff --git a/volatility3/framework/plugins/layerwriter.py b/volatility3/framework/plugins/layerwriter.py index 0068ec224..1bee5f20d 100644 --- a/volatility3/framework/plugins/layerwriter.py +++ b/volatility3/framework/plugins/layerwriter.py @@ -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()) diff --git a/volatility3/framework/plugins/linux/bash.py b/volatility3/framework/plugins/linux/bash.py index 7f606115c..ce4567ca6 100644 --- a/volatility3/framework/plugins/linux/bash.py +++ b/volatility3/framework/plugins/linux/bash.py @@ -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]) diff --git a/volatility3/framework/plugins/linux/capabilities.py b/volatility3/framework/plugins/linux/capabilities.py new file mode 100644 index 000000000..bfdb69aba --- /dev/null +++ b/volatility3/framework/plugins/linux/capabilities.py @@ -0,0 +1,208 @@ +# This file is Copyright 2023 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 +from dataclasses import dataclass, astuple, fields +from typing import Iterable, List, Tuple + +from volatility3.framework import interfaces, renderers, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols.linux import extensions +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +@dataclass +class TaskData: + """Stores basic information about a task""" + + comm: str + pid: int + tgid: int + ppid: int + euid: int + + +@dataclass +class CapabilitiesData: + """Stores each set of capabilties for a task""" + + cap_inheritable: interfaces.objects.ObjectInterface + cap_permitted: interfaces.objects.ObjectInterface + cap_effective: interfaces.objects.ObjectInterface + cap_bset: interfaces.objects.ObjectInterface + cap_ambient: interfaces.objects.ObjectInterface + + def astuple(self) -> Tuple: + """Returns a shallow copy of the capability sets in a tuple. + + Otherwise, when dataclasses.astuple() performs a deep-copy recursion on + ObjectInterface will take a substantial amount of time. + """ + return tuple(getattr(self, field.name) for field in fields(self)) + + +class Capabilities(plugins.PluginInterface): + """Lists process capabilities""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pids", + description="Filter on specific process IDs.", + element_type=int, + optional=True, + ), + ] + + def _check_capabilities_support( + self, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + ): + """Checks that the framework supports at least as much capabilities as + the kernel being analysed. Otherwise, it shows a warning for the + developers. + """ + + vmlinux = context.modules[vmlinux_module_name] + + try: + kernel_cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap") + except exceptions.SymbolError: + # It should be a kernel < 3.2 + return None + + vol2_last_cap = extensions.kernel_cap_struct.get_last_cap_value() + if kernel_cap_last_cap > vol2_last_cap: + vollog.warning( + "Developers: The supported Linux capabilities of this plugin are outdated for this kernel" + ) + + @staticmethod + def _decode_cap(cap: interfaces.objects.ObjectInterface) -> str: + """Returns a textual representation of the capability set. + The format is a comma-separated list of capabilitites. In order to + summarize the output and if all the capabilities are enabled, instead of + the individual capabilities, the special name "all" will be shown. + + Args: + cap: Kernel capability object. Usually a 'kernel_cap_struct' struct + + Returns: + str: A string with a comma separated list of decoded capabilities + """ + if isinstance(cap, renderers.NotAvailableValue): + return cap + + cap_value = cap.get_capabilities() + if not cap_value: + return "" + + if cap_value == cap.get_kernel_cap_full(): + return "all" + + return ", ".join(cap.enumerate_capabilities()) + + @classmethod + def get_task_capabilities( + cls, task: interfaces.objects.ObjectInterface + ) -> Tuple[TaskData, CapabilitiesData]: + """Returns a tuple with the task basic information along with its capabilities + + Args: + task: A task object from where to get the fields. + + Returns: + A tuple with the task basic information and its capabilities + """ + task_data = TaskData( + comm=utility.array_to_string(task.comm), + pid=int(task.pid), + tgid=int(task.tgid), + ppid=int(task.parent.pid), + euid=int(task.cred.euid), + ) + + task_cred = task.real_cred + capabilities_data = CapabilitiesData( + cap_inheritable=task_cred.cap_inheritable, + cap_permitted=task_cred.cap_permitted, + cap_effective=task_cred.cap_effective, + cap_bset=task_cred.cap_bset, + cap_ambient=renderers.NotAvailableValue(), + ) + + # Ambient capabilities were added in kernels 4.3.6 + if task_cred.has_member("cap_ambient"): + capabilities_data.cap_ambient = task_cred.cap_ambient + + return task_data, capabilities_data + + @classmethod + def get_tasks_capabilities( + cls, tasks: List[interfaces.objects.ObjectInterface] + ) -> Iterable[Tuple[TaskData, CapabilitiesData]]: + """Yields a tuple for each task containing the task's basic information along with its capabilities + + Args: + tasks: An iterable with the tasks to process. + + Yields: + A tuple for each task containing the task's basic information and its capabilities + """ + for task in tasks: + yield cls.get_task_capabilities(task) + + def _generator( + self, tasks: Iterable[interfaces.objects.ObjectInterface] + ) -> Iterable[Tuple[int, Tuple]]: + for task_fields, capabilities_fields in self.get_tasks_capabilities(tasks): + task_fields = astuple(task_fields) + + capabilities_text = tuple( + self._decode_cap(cap) for cap in capabilities_fields.astuple() + ) + + yield 0, task_fields + capabilities_text + + def run(self): + self._check_capabilities_support(self.context, self.config["kernel"]) + + pids = self.config.get("pids") + pid_filter = pslist.PsList.create_pid_filter(pids) + tasks = pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=pid_filter + ) + + columns = [ + ("Name", str), + ("Tid", int), + ("Pid", int), + ("PPid", int), + ("EUID", int), + ("cap_inheritable", str), + ("cap_permitted", str), + ("cap_effective", str), + ("cap_bounding", str), + ("cap_ambient", str), + ] + + return renderers.TreeGrid(columns, self._generator(tasks)) diff --git a/volatility3/framework/plugins/linux/check_afinfo.py b/volatility3/framework/plugins/linux/check_afinfo.py index 4cd065c7e..7fced6acd 100644 --- a/volatility3/framework/plugins/linux/check_afinfo.py +++ b/volatility3/framework/plugins/linux/check_afinfo.py @@ -23,8 +23,11 @@ class Check_afinfo(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), ] # returns whether the symbol is found within the kernel (system.map) or not @@ -40,7 +43,7 @@ class Check_afinfo(plugins.PluginInterface): continue if check == "write": - addr = var_ops.member(attr = 'write') + addr = var_ops.member(attr="write") else: addr = getattr(var_ops, check) @@ -48,30 +51,61 @@ class Check_afinfo(plugins.PluginInterface): yield check, addr def _check_afinfo(self, var_name, var, op_members, seq_members): - for hooked_member, hook_address in self._check_members(var.seq_fops, var_name, op_members): - yield var_name, hooked_member, hook_address + # check if object has a least one of the members used for analysis by this function + required_members = ["seq_fops", "seq_ops", "seq_show"] + has_required_member = any( + [var.has_member(member) for member in required_members] + ) + if not has_required_member: + vollog.debug( + f"{var_name} object at {hex(var.vol.offset)} had none of the required members: {', '.join([member for member in required_members])}" + ) + raise exceptions.PluginRequirementException + + if var.has_member("seq_fops"): + for hooked_member, hook_address in self._check_members( + var.seq_fops, var_name, op_members + ): + yield var_name, hooked_member, hook_address # newer kernels if var.has_member("seq_ops"): - for hooked_member, hook_address in self._check_members(var.seq_ops, var_name, seq_members): + for hooked_member, hook_address in self._check_members( + var.seq_ops, var_name, seq_members + ): yield var_name, hooked_member, hook_address # this is the most commonly hooked member by rootkits, so a force a check on it - elif not self._is_known_address(var.seq_show): - yield var_name, "show", var.seq_show + else: + if var.has_member("seq_show"): + if not self._is_known_address(var.seq_show): + yield var_name, "show", var.seq_show def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] - vmlinux = self.context.modules[self.config['kernel']] - - op_members = vmlinux.get_type('file_operations').members - seq_members = vmlinux.get_type('seq_operations').members + op_members = vmlinux.get_type("file_operations").members + seq_members = vmlinux.get_type("seq_operations").members tcp = ("tcp_seq_afinfo", ["tcp6_seq_afinfo", "tcp4_seq_afinfo"]) - udp = ("udp_seq_afinfo", ["udplite6_seq_afinfo", "udp6_seq_afinfo", "udplite4_seq_afinfo", "udp4_seq_afinfo"]) + udp = ( + "udp_seq_afinfo", + [ + "udplite6_seq_afinfo", + "udp6_seq_afinfo", + "udplite4_seq_afinfo", + "udp4_seq_afinfo", + ], + ) protocols = [tcp, udp] - for (struct_type, global_vars) in protocols: + # used to track the calls to _check_afinfo and the + # number of errors produced due to missing members + symbols_checked = set() + symbols_with_errors = set() + + # loop through all symbols + for struct_type, global_vars in protocols: for global_var_name in global_vars: # this will lookup fail for the IPv6 protocols on kernels without IPv6 support try: @@ -79,12 +113,31 @@ class Check_afinfo(plugins.PluginInterface): except exceptions.SymbolError: continue - global_var = vmlinux.object(object_type = struct_type, offset = global_var.address) + global_var = vmlinux.object( + object_type=struct_type, offset=global_var.address + ) - for name, member, address in self._check_afinfo(global_var_name, global_var, op_members, seq_members): - yield 0, (name, member, format_hints.Hex(address)) + symbols_checked.add(global_var_name) + try: + for name, member, address in self._check_afinfo( + global_var_name, global_var, op_members, seq_members + ): + yield 0, (name, member, format_hints.Hex(address)) + except exceptions.PluginRequirementException: + symbols_with_errors.add(global_var_name) + + # if every call to _check_afinfo failed show a warning + if symbols_checked == symbols_with_errors: + vollog.warning( + "This plugin was not able to check for hooks. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) def run(self): - - return renderers.TreeGrid([("Symbol Name", str), ("Member", str), ("Handler Address", format_hints.Hex)], - self._generator()) + return renderers.TreeGrid( + [ + ("Symbol Name", str), + ("Member", str), + ("Handler Address", format_hints.Hex), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/check_creds.py b/volatility3/framework/plugins/linux/check_creds.py index 9bc1a067d..ab6ee4935 100644 --- a/volatility3/framework/plugins/linux/check_creds.py +++ b/volatility3/framework/plugins/linux/check_creds.py @@ -19,13 +19,18 @@ class Check_creds(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), ] def _generator(self): - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] type_task = vmlinux.get_type("task_struct") @@ -41,7 +46,6 @@ class Check_creds(interfaces.plugins.PluginInterface): tasks = pslist.PsList.list_tasks(self.context, vmlinux.name) for task in tasks: - cred_addr = task.cred.dereference().vol.offset if cred_addr not in creds: @@ -49,7 +53,7 @@ class Check_creds(interfaces.plugins.PluginInterface): creds[cred_addr].append(task.pid) - for (_, pids) in creds.items(): + for _, pids in creds.items(): if len(pids) > 1: pid_str = "" for pid in pids: diff --git a/volatility3/framework/plugins/linux/check_idt.py b/volatility3/framework/plugins/linux/check_idt.py index 1764b6364..cc3a08933 100644 --- a/volatility3/framework/plugins/linux/check_idt.py +++ b/volatility3/framework/plugins/linux/check_idt.py @@ -15,27 +15,38 @@ vollog = logging.getLogger(__name__) class Check_idt(interfaces.plugins.PluginInterface): - """ Checks if the IDT has been altered """ + """Checks if the IDT has been altered""" _required_framework_version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self): - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules) + handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, vmlinux.name, modules + ) - is_32bit = not symbols.symbol_table_is_64bit(self.context, vmlinux.symbol_table_name) + is_32bit = not symbols.symbol_table_is_64bit( + self.context, vmlinux.symbol_table_name + ) idt_table_size = 256 @@ -59,11 +70,13 @@ class Check_idt(interfaces.plugins.PluginInterface): addrs = vmlinux.object_from_symbol("idt_table") - table = vmlinux.object(object_type = 'array', - offset = addrs.vol.offset, - subtype = vmlinux.get_type(idt_type), - count = idt_table_size, - absolute = True) + table = vmlinux.object( + object_type="array", + offset=addrs.vol.offset, + subtype=vmlinux.get_type(idt_type), + count=idt_table_size, + absolute=True, + ) for i in check_idxs: ent = table[i] @@ -86,10 +99,27 @@ class Check_idt(interfaces.plugins.PluginInterface): idt_addr = idt_addr & address_mask - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, idt_addr) + module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( + vmlinux, handlers, idt_addr + ) - yield (0, [format_hints.Hex(i), format_hints.Hex(idt_addr), module_name, symbol_name]) + yield ( + 0, + [ + format_hints.Hex(i), + format_hints.Hex(idt_addr), + module_name, + symbol_name, + ], + ) def run(self): - return renderers.TreeGrid([("Index", format_hints.Hex), ("Address", format_hints.Hex), ("Module", str), - ("Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Index", format_hints.Hex), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/check_modules.py b/volatility3/framework/plugins/linux/check_modules.py index 6af8dec96..9b3594c5e 100644 --- a/volatility3/framework/plugins/linux/check_modules.py +++ b/volatility3/framework/plugins/linux/check_modules.py @@ -23,14 +23,20 @@ class Check_modules(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] @classmethod - def get_kset_modules(self, context: interfaces.context.ContextInterface, vmlinux_name: str): - + def get_kset_modules( + cls, context: interfaces.context.ContextInterface, vmlinux_name: str + ): vmlinux = context.modules[vmlinux_name] try: @@ -45,12 +51,16 @@ class Check_modules(plugins.PluginInterface): ret = {} - kobj_off = vmlinux.get_type('module_kobject').relative_child_offset('kobj') + kobj_off = vmlinux.get_type("module_kobject").relative_child_offset("kobj") - for kobj in module_kset.list.to_list(vmlinux.symbol_table_name + constants.BANG + "kobject", "entry"): - - mod_kobj = vmlinux.object(object_type = "module_kobject", offset = kobj.vol.offset - kobj_off, - absolute = True) + for kobj in module_kset.list.to_list( + vmlinux.symbol_table_name + constants.BANG + "kobject", "entry" + ): + mod_kobj = vmlinux.object( + object_type="module_kobject", + offset=kobj.vol.offset - kobj_off, + absolute=True, + ) mod = mod_kobj.mod @@ -61,14 +71,18 @@ class Check_modules(plugins.PluginInterface): return ret def _generator(self): - kset_modules = self.get_kset_modules(self.context, self.config['kernel']) + kset_modules = self.get_kset_modules(self.context, self.config["kernel"]) lsmod_modules = set( str(utility.array_to_string(modules.name)) - for modules in lsmod.Lsmod.list_modules(self.context, self.config['kernel'])) + for modules in lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) + ) for mod_name in set(kset_modules.keys()).difference(lsmod_modules): yield (0, (format_hints.Hex(kset_modules[mod_name]), str(mod_name))) def run(self): - return renderers.TreeGrid([("Module Address", format_hints.Hex), ("Module Name", str)], self._generator()) + return renderers.TreeGrid( + [("Module Address", format_hints.Hex), ("Module Name", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/check_syscall.py b/volatility3/framework/plugins/linux/check_syscall.py index 87d252cd5..b6634d612 100644 --- a/volatility3/framework/plugins/linux/check_syscall.py +++ b/volatility3/framework/plugins/linux/check_syscall.py @@ -3,11 +3,11 @@ # """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" +import contextlib import logging from typing import List -from volatility3.framework import exceptions, interfaces -from volatility3.framework import renderers, constants +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.renderers import format_hints @@ -30,8 +30,11 @@ class Check_syscall(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), ] def _get_table_size_next_symbol(self, table_addr, ptr_sz, vmlinux): @@ -40,11 +43,9 @@ class Check_syscall(plugins.PluginInterface): symbol_list = [] for sn in vmlinux.symbols: - try: + with contextlib.suppress(exceptions.SymbolError): # When requesting the symbol from the module, a full resolve is performed symbol_list.append((vmlinux.get_symbol(sn).address, sn)) - except exceptions.SymbolError: - pass sorted_symbols = sorted(symbol_list) sym_address = 0 @@ -65,8 +66,12 @@ class Check_syscall(plugins.PluginInterface): accurate.""" return len( - [sym for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols if - sym.startswith("__syscall_meta__")]) + [ + sym + for sym in self.context.symbol_space[vmlinux.symbol_table_name].symbols + if sym.startswith("__syscall_meta__") + ] + ) def _get_table_info_other(self, table_addr, ptr_sz, vmlinux): table_size_meta = self._get_table_size_meta(vmlinux) @@ -80,7 +85,7 @@ class Check_syscall(plugins.PluginInterface): def _get_table_info_disassembly(self, ptr_sz, vmlinux): """Find the size of the system call table by disassembling functions - that immediately reference it in their first isntruction This is in the + that immediately reference it in their first instruction This is in the form 'cmp reg,NR_syscalls'.""" table_size = 0 @@ -102,12 +107,12 @@ class Check_syscall(plugins.PluginInterface): # if we can't find the disassemble function then bail and rely on a different method return 0 - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] data = self.context.layers.read(vmlinux.layer_name, func_addr, 6) - for (address, size, mnemonic, op_str) in md.disasm_lite(data, func_addr): - if mnemonic == 'CMP': - table_size = int(op_str.split(",")[1].strip()) & 0xffff + for address, size, mnemonic, op_str in md.disasm_lite(data, func_addr): + if mnemonic == "CMP": + table_size = int(op_str.split(",")[1].strip()) & 0xFFFF break return table_size @@ -128,7 +133,7 @@ class Check_syscall(plugins.PluginInterface): # TODO - add finding and parsing unistd.h once cached file enumeration is added def _generator(self): - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] ptr_sz = vmlinux.get_type("pointer").size if ptr_sz == 4: @@ -140,7 +145,7 @@ class Check_syscall(plugins.PluginInterface): table_info = self._get_table_info(vmlinux, "sys_call_table", ptr_sz) except exceptions.SymbolError: vollog.error("Unable to find the system call table. Exiting.") - return + return None tables = [(table_name, table_info)] @@ -156,27 +161,48 @@ class Check_syscall(plugins.PluginInterface): ia32_info = self._get_table_info(vmlinux, "ia32_sys_call_table", ptr_sz) tables.append(("32bit", ia32_info)) - for (table_name, (tableaddr, tblsz)) in tables: - table = vmlinux.object(object_type = "array", - subtype = vmlinux.get_type("pointer"), - offset = tableaddr, - count = tblsz) + for table_name, (tableaddr, tblsz) in tables: + table = vmlinux.object( + object_type="array", + subtype=vmlinux.get_type("pointer"), + offset=tableaddr, + count=tblsz, + ) - for (i, call_addr) in enumerate(table): + for i, call_addr in enumerate(table): if not call_addr: continue symbols = list(vmlinux.get_symbols_by_absolute_location(call_addr)) if len(symbols) > 0: - sym_name = str(symbols[0].split(constants.BANG)[1]) if constants.BANG in symbols[0] else \ - str(symbols[0]) + sym_name = ( + str(symbols[0].split(constants.BANG)[1]) + if constants.BANG in symbols[0] + else str(symbols[0]) + ) else: sym_name = "UNKNOWN" - yield (0, (format_hints.Hex(tableaddr), table_name, i, format_hints.Hex(call_addr), sym_name)) + yield ( + 0, + ( + format_hints.Hex(tableaddr), + table_name, + i, + format_hints.Hex(call_addr), + sym_name, + ), + ) def run(self): - - return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int), - ("Handler Address", format_hints.Hex), ("Handler Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Table Address", format_hints.Hex), + ("Table Name", str), + ("Index", int), + ("Handler Address", format_hints.Hex), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/elfs.py b/volatility3/framework/plugins/linux/elfs.py index d2380817d..e688ecb42 100644 --- a/volatility3/framework/plugins/linux/elfs.py +++ b/volatility3/framework/plugins/linux/elfs.py @@ -4,34 +4,131 @@ """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" -from typing import List +import logging +from typing import List, Optional, Type -from volatility3.framework import renderers, interfaces +from volatility3.framework import constants, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.linux.extensions import elf from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class Elfs(plugins.PluginInterface): """Lists all memory mapped ELF files for all processes.""" _required_framework_version = (2, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed processes", + default=False, + optional=True, + ), ] + @classmethod + def elf_dump( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + elf_table_name: str, + vma: interfaces.objects.ObjectInterface, + task: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + ) -> Optional[interfaces.plugins.FileHandlerInterface]: + """Extracts an ELF as a FileHandlerInterface + Args: + context: the context to operate upon + layer_name: The name of the layer on which to operate + elf_table_name: the name for the symbol table containing the symbols for ELF-files + vma: virtual memory allocation of ELF + task: the task object whose memory should be output + open_method: class to provide context manager for opening the file + Returns: + An open FileHandlerInterface object containing the complete data for the task or None in the case of failure + """ + + proc_layer = context.layers[layer_name] + file_handle = None + + elf_object = context.object( + elf_table_name + constants.BANG + "Elf", + offset=vma.vm_start, + layer_name=layer_name, + ) + + if not elf_object.is_valid(): + return None + + sections = {} + # TODO: Apply more effort to reconstruct ELF, e.g.: https://github.com/enbarberis/core2ELF64 ? + for phdr in elf_object.get_program_headers(): + if phdr.p_type != 1: # PT_LOAD = 1 + continue + + start = phdr.p_vaddr + size = phdr.p_memsz + end = start + size + + # Use complete memory pages for dumping + # If start isn't a multiple of 4096, stick to the highest multiple < start + # If end isn't a multiple of 4096, stick to the lowest multiple > end + if start % 4096: + start = start & ~0xFFF + + if end % 4096: + end = (end & ~0xFFF) + 4096 + + real_size = end - start + + # Check if ELF has a legitimate size + if real_size < 0 or real_size > constants.linux.ELF_MAX_EXTRACTION_SIZE: + raise ValueError(f"The claimed size of the ELF is invalid: {real_size}") + + sections[start] = real_size + + elf_data = b"" + for section_start in sorted(sections.keys()): + read_size = sections[section_start] + + buf = proc_layer.read(vma.vm_start + section_start, read_size, pad=True) + elf_data = elf_data + buf + + file_handle = open_method( + f"pid.{task.pid}.{utility.array_to_string(task.comm)}.{vma.vm_start:#x}.dmp" + ) + file_handle.write(elf_data) + + return file_handle + def _generator(self, tasks): + elf_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "linux", "elf", class_types=elf.class_types + ) for task in tasks: proc_layer_name = task.add_process_layer() if not proc_layer_name: @@ -41,21 +138,60 @@ class Elfs(plugins.PluginInterface): name = utility.array_to_string(task.comm) - for vma in task.mm.get_mmap_iter(): - hdr = proc_layer.read(vma.vm_start, 4, pad = True) - if not (hdr[0] == 0x7f and hdr[1] == 0x45 and hdr[2] == 0x4c and hdr[3] == 0x46): + for vma in task.mm.get_vma_iter(): + hdr = proc_layer.read(vma.vm_start, 4, pad=True) + if not ( + hdr[0] == 0x7F + and hdr[1] == 0x45 + and hdr[2] == 0x4C + and hdr[3] == 0x46 + ): continue path = vma.get_name(self.context, task) - yield (0, (task.pid, name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), path)) + file_output = "Disabled" + if self.config["dump"]: + file_handle = self.elf_dump( + self.context, + proc_layer_name, + elf_table_name, + vma, + task, + self.open, + ) + file_output = "Error outputting file" + if file_handle: + file_handle.close() + file_output = str(file_handle.preferred_filename) + + yield ( + 0, + ( + task.pid, + name, + format_hints.Hex(vma.vm_start), + format_hints.Hex(vma.vm_end), + path, + file_output, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex), - ("End", format_hints.Hex), ("File Path", str)], - self._generator( - pslist.PsList.list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("File Path", str), + ("File Output", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/envars.py b/volatility3/framework/plugins/linux/envars.py new file mode 100644 index 000000000..5cbf0f502 --- /dev/null +++ b/volatility3/framework/plugins/linux/envars.py @@ -0,0 +1,121 @@ +# 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 + +from volatility3.framework import exceptions, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.plugins.linux import pslist + +vollog = logging.getLogger(__name__) + + +class Envars(plugins.PluginInterface): + """Lists processes with their environment variables""" + + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + ] + + def _generator(self, tasks): + """Generates a listing of processes along with environment variables""" + + # walk the process list and return the envars + for task in tasks: + pid = task.pid + + # get process name as string + name = utility.array_to_string(task.comm) + + # try and get task parent + try: + ppid = task.parent.pid + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read parent pid for task {pid} {name}, setting ppid to 0." + ) + ppid = 0 + + # kernel threads never have an mm as they do not have userland mappings + try: + mm = task.mm + except exceptions.InvalidAddressException: + # no mm so cannot get envars + vollog.debug( + f"Unable to access mm for task {pid} {name} it is likely a kernel thread, will not extract any envars." + ) + mm = None + continue + + # if mm exists attempt to get envars + if mm: + # get process layer to read envars from + proc_layer_name = task.add_process_layer() + if proc_layer_name is None: + vollog.debug( + f"Unable to construct process layer for task {pid} {name}, will not extract any envars." + ) + continue + proc_layer = self.context.layers[proc_layer_name] + + # get the size of the envars with sanity checking + envars_size = task.mm.env_end - task.mm.env_start + if not (0 < envars_size <= 8192): + vollog.debug( + f"Task {pid} {name} appears to have envars of size {envars_size} bytes which fails the sanity checking, will not extract any envars." + ) + continue + + # attempt to read all envars data + try: + envar_data = proc_layer.read(task.mm.env_start, envars_size) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read full envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)} for {envars_size} bytes, will not extract any envars." + ) + continue + + # parse envar data, envars are null terminated, keys and values are separated by '=' + envar_data = envar_data.rstrip(b"\x00") + for envar_pair in envar_data.split(b"\x00"): + try: + key, value = envar_pair.decode().split("=", 1) + except ValueError: + vollog.debug( + f"Unable to extract envars for {pid} {name} starting at virtual offset {hex(task.mm.env_start)}, they don't appear to be '=' separated" + ) + continue + yield (0, (pid, ppid, name, key, value)) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [("PID", int), ("PPID", int), ("COMM", str), ("KEY", str), ("VALUE", str)], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/iomem.py b/volatility3/framework/plugins/linux/iomem.py new file mode 100644 index 000000000..6732084db --- /dev/null +++ b/volatility3/framework/plugins/linux/iomem.py @@ -0,0 +1,161 @@ +# This file is Copyright 2023 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 +from typing import List + +from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class IOMem(interfaces.plugins.PluginInterface): + """Generates an output similar to /proc/iomem on a running system.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 1) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ) + ] + + @classmethod + def parse_resource( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + resource_offset: int, + seen: set = set(), + depth: int = 0, + ): + """Recursively parse from a root resource to find details about all related resources. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + resource_offset: The offset to the resource to be parsed + seen: The set of resource offsets that have already been parsed + depth: How deep into the resource structure we are + + Yields: + Each row of output + """ + vmlinux = context.modules[vmlinux_module_name] + + # create the resource object with protection against memory smear + try: + resource = vmlinux.object("resource", resource_offset, absolute=True) + except exceptions.InvalidAddressException: + vollog.warning( + f"Unable to create resource object at {resource_offset:#x}. This resource, " + "its sibling, and any of it's children and will be missing from the output." + ) + return None + + # get name with protection against smear as following a pointer + try: + name = utility.pointer_to_string(resource.name, 128) + except exceptions.InvalidAddressException: + vollog.warning( + f"Unable to follow pointer to name for resource object at {resource_offset:#x}, " + "replaced with UnreadableValue" + ) + name = renderers.UnreadableValue() + + # mark this resource as seen in the seen set. Normally this should not be needed but will protect + # against possible infinite loops. Warn the user if an infinite loop would have happened. + if resource_offset in seen: + vollog.warning( + f"The resource object at {resource_offset:#x} '{name}' has already been processed, " + "this should not normally occur. No further results from related resources will be " + "displayed to protect against infinite loops." + ) + return None + else: + seen.add(resource_offset) + + # yield information on this resource + yield depth, (name, resource.start, resource.end) + + # process child resource if this exists + if resource.child != 0: + yield from cls.parse_resource( + context, + vmlinux_module_name, + resource.child, + seen, + depth + 1, + ) + + # process sibling resource if this exists + if resource.sibling != 0: + yield from cls.parse_resource( + context, + vmlinux_module_name, + resource.sibling, + seen, + depth, + ) + + def _generator(self): + """Generates an output similar to /proc/iomem on a running system + + Args: + None + + Yields: + Each row of output using the parse_resource function + """ + + # get the kernel module from the current context + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] + + # get the address for the iomem_resource + try: + iomem_root_offset = vmlinux.get_absolute_symbol_address("iomem_resource") + except exceptions.SymbolError: + iomem_root_offset = None + + # only continue if iomem_root address was located + if iomem_root_offset is not None: + # recursively parse the resources starting from the root resource at 'iomem_resource' + for depth, (name, start, end) in self.parse_resource( + self.context, vmlinux_module_name, iomem_root_offset + ): + # use format_hints to format start and end addresses for the renderers + yield depth, (name, format_hints.Hex(start), format_hints.Hex(end)) + + def run(self): + # get the kernel module from the current context + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] + + # check that the iomem_resource symbol exists + # normally exported in /kernel/resource.c + if not vmlinux.has_symbol("iomem_resource"): + raise TypeError( + "This plugin requires the iomem_resource symbol. This symbol is not present in the supplied symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + # error if type 'resource' is not found + if not vmlinux.has_type("resource"): + raise TypeError( + "This plugin requires the resource type. This type is not present in the supplied symbol table. This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." + ) + + columns = [ + ("Name", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ] + return renderers.TreeGrid(columns, self._generator()) diff --git a/volatility3/framework/plugins/linux/keyboard_notifiers.py b/volatility3/framework/plugins/linux/keyboard_notifiers.py index 51e684e6f..72273a77b 100644 --- a/volatility3/framework/plugins/linux/keyboard_notifiers.py +++ b/volatility3/framework/plugins/linux/keyboard_notifiers.py @@ -21,18 +21,27 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), ] def _generator(self): - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules) + handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, vmlinux.name, modules + ) try: knl_addr = vmlinux.object_from_symbol("keyboard_notifier_list") @@ -46,14 +55,25 @@ class Keyboard_notifiers(interfaces.plugins.PluginInterface): "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." ) - knl = vmlinux.object(object_type = "atomic_notifier_head", offset = knl_addr.vol.offset, absolute = True) + knl = vmlinux.object( + object_type="atomic_notifier_head", + offset=knl_addr.vol.offset, + absolute=True, + ) - for call_back in linux.LinuxUtilities.walk_internal_list(vmlinux, "notifier_block", "next", knl.head): + for call_back in linux.LinuxUtilities.walk_internal_list( + vmlinux, "notifier_block", "next", knl.head + ): call_addr = call_back.notifier_call - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, call_addr) + module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( + vmlinux, handlers, call_addr + ) yield (0, [format_hints.Hex(call_addr), module_name, symbol_name]) def run(self): - return renderers.TreeGrid([("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], self._generator()) + return renderers.TreeGrid( + [("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/kmsg.py b/volatility3/framework/plugins/linux/kmsg.py index 8f4540766..d1f17bf94 100644 --- a/volatility3/framework/plugins/linux/kmsg.py +++ b/volatility3/framework/plugins/linux/kmsg.py @@ -1,14 +1,19 @@ # 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 re import logging from abc import ABC, abstractmethod from enum import Enum from typing import Generator, Iterator, List, Tuple -from volatility3.framework import class_subclasses, constants, contexts, interfaces, renderers +from volatility3.framework import ( + class_subclasses, + constants, + interfaces, + renderers, +) from volatility3.framework.configuration import requirements -from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility vollog = logging.getLogger(__name__) @@ -24,6 +29,7 @@ class DescStateEnum(Enum): class ABCKmsg(ABC): """Kernel log buffer reader""" + LEVELS = ( "emerg", # system is unusable "alert", # action must be taken immediately @@ -47,27 +53,25 @@ class ABCKmsg(ABC): "uucp", # UUCP subsystem "cron", # clock daemon "authpriv", # security/authorization messages (private) - "ftp" # FTP daemon + "ftp", # FTP daemon ) def __init__( - self, - context: interfaces.context.ContextInterface, - config: interfaces.configuration.HierarchicalDict + self, + context: interfaces.context.ContextInterface, + config: interfaces.configuration.HierarchicalDict, ): self._context = context self._config = config - vmlinux = context.modules[self._config['kernel']] - self.layer_name = vmlinux.layer_name # type: ignore - symbol_table_name = vmlinux.symbol_table_name # type: ignore - self.vmlinux = contexts.Module.create(context, symbol_table_name, self.layer_name, 0) # type: ignore - self.long_unsigned_int_size = self.vmlinux.get_type('long unsigned int').size + self.vmlinux = context.modules[self._config["kernel"]] + self.layer_name = self.vmlinux.layer_name # type: ignore + self.long_unsigned_int_size = self.vmlinux.get_type("long unsigned int").size @classmethod def run_all( - cls, - context: interfaces.context.ContextInterface, - config: interfaces.configuration.HierarchicalDict + cls, + context: interfaces.context.ContextInterface, + config: interfaces.configuration.HierarchicalDict, ) -> Iterator[Tuple[str, str, str, str, str]]: """It calls each subclass symtab_checks() to test the required conditions to that specific kernel implementation. @@ -77,31 +81,46 @@ class ABCKmsg(ABC): config: Core configuration Yields: - kmsg records + The kmsg records. Same as run() """ - vmlinux = context.modules[config['kernel']] + vmlinux = context.modules[config["kernel"]] kmsg_inst = None # type: ignore for subclass in class_subclasses(cls): - if not subclass.symtab_checks(vmlinux = vmlinux): - vollog.log(constants.LOGLEVEL_VVVV, - "Kmsg implementation '%s' doesn't match this memory dump", subclass.__name__) + if not subclass.symtab_checks(vmlinux=vmlinux): + vollog.log( + constants.LOGLEVEL_VVVV, + "Kmsg implementation '%s' doesn't match this memory dump", + subclass.__name__, + ) continue - vollog.log(constants.LOGLEVEL_VVVV, "Kmsg implementation '%s' matches!", subclass.__name__) - kmsg_inst = subclass(context = context, config = config) - # More than one class could be executed for an specific kernel - # version i.e. Netfilter Ingress hooks - # We expect just one implementation to be executed for an specific kernel + vollog.log( + constants.LOGLEVEL_VVVV, + "Kmsg implementation '%s' matches!", + subclass.__name__, + ) + kmsg_inst = subclass(context=context, config=config) yield from kmsg_inst.run() + # So far, it only allows a single implementation to be executed for each + # specific kernel. break if kmsg_inst is None: - vollog.error("Unsupported Netfilter kernel implementation") + vollog.error("Unsupported kernel ring buffer implementation") @abstractmethod def run(self) -> Iterator[Tuple[str, str, str, str, str]]: - """Walks through the specific kernel implementation.""" + """Walks through the specific kernel implementation. + + Returns: + tuple: + facility [str]: The log facility: kern, user, etc. See FACILITIES + level [str]: The log level: info, debug, etc. See LEVELS + timestamp [str]: The message timestamp. See nsec_to_sec_str() + caller [str]: The caller ID: CPU(1) or Task(1234). See get_caller() + line [str]: The log message. + """ @classmethod @abstractmethod @@ -111,12 +130,13 @@ class ABCKmsg(ABC): The first class returning True will be instantiated and called via the run() method. - :return: True is the kernel being analysed fulfill the class requirements. + Returns: + bool: True if the kernel being analyzed fulfill the class requirements. """ def get_string(self, addr: int, length: int) -> str: txt = self._context.layers[self.layer_name].read(addr, length) # type: ignore - return txt.decode(encoding = 'utf8', errors = 'replace') + return txt.decode(encoding="utf8", errors="replace") def nsec_to_sec_str(self, nsec: int) -> str: # See kernel/printk/printk.c:print_time() @@ -131,26 +151,31 @@ class ABCKmsg(ABC): return "%lu.%06lu" % (nsec / 1000000000, (nsec % 1000000000) / 1000) def get_timestamp_in_sec_str(self, obj) -> str: - # obj could be printk_log or printk_info + # obj could be log, printk_log or printk_info return self.nsec_to_sec_str(obj.ts_nsec) def get_caller(self, obj): # In some kernel versions, it's only available if CONFIG_PRINTK_CALLER is defined. # caller_id is a member of printk_log struct from 5.1 to the latest 5.9 # From kernels 5.10 on, it's a member of printk_info struct - if obj.has_member('caller_id'): + if obj.has_member("caller_id"): return self.get_caller_text(obj.caller_id) else: - return "" + return renderers.NotAvailableValue() def get_caller_text(self, caller_id): - caller_name = 'CPU' if caller_id & 0x80000000 else 'Task' + caller_name = "CPU" if caller_id & 0x80000000 else "Task" caller = "%s(%u)" % (caller_name, caller_id & ~0x80000000) return caller def get_prefix(self, obj) -> Tuple[int, int, str, str]: - # obj could be printk_log or printk_info - return obj.facility, obj.level, self.get_timestamp_in_sec_str(obj), self.get_caller(obj) + # obj could be log, printk_log or printk_info + return ( + obj.facility, + obj.level, + self.get_timestamp_in_sec_str(obj), + self.get_caller(obj), + ) @classmethod def get_level_text(cls, level: int) -> str: @@ -169,61 +194,132 @@ class ABCKmsg(ABC): return str(facility) -class KmsgLegacy(ABCKmsg): - """Linux kernels prior to v5.10, the ringbuffer is initially kept in - __log_buf, and log_buf is a pointer to the former. __log_buf is declared as - a char array but it actually contains an array of printk_log structs. - The length of this array is defined in the kernel KConfig configuration via - the CONFIG_LOG_BUF_SHIFT value as a power of 2. - This can also be modified by the log_buf_len kernel boot parameter. - In SMP systems with more than 64 CPUs this ringbuffer size is dynamically - allocated according the number of CPUs based on the value of - CONFIG_LOG_CPU_MAX_BUF_SHIFT, and the log_buf pointer is updated - consequently to the new buffer. - In that case, the original static buffer in __log_buf is unused. +class Kmsg_pre_3_5(ABCKmsg): + """The kernel ring buffer (log_buf) is a char array that sequentially stores + log lines, each separated by newline (LF) characters. i.e: + <6>[ 9565.250411] line1!\n<6>[ 9565.250412] line2\n... """ @classmethod def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_type('printk_log') + return ( + vmlinux.has_symbol("log_end") + and not vmlinux.has_symbol("log_first_idx") + and not ( + vmlinux.has_type("log") + and vmlinux.get_type("log").has_member("ts_nsec") + ) + ) - def get_text_from_printk_log(self, msg) -> str: - msg_offset = msg.vol.offset + self.vmlinux.get_type('printk_log').size + def run(self) -> Iterator[Tuple[str, str, str, str, str]]: + log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name="log_buf") + log_buf_len = self.vmlinux.object_from_symbol(symbol_name="log_buf_len") + log_buf = utility.pointer_to_string(log_buf_ptr, count=log_buf_len) + log_end = self.vmlinux.object_from_symbol(symbol_name="log_end") + + if log_end > log_buf_len: + start = log_end - log_buf_len + first_half = log_buf[start:] + second_half = log_buf[:start] + log_buf = first_half + second_half + + log_buf_lines = log_buf.splitlines() + + for log_buf_line in log_buf_lines: + m = re.match(r"<(\d+)>\[\s*(\d+\.\d+)\]\s(.*?)$", log_buf_line) + if not m: + # If there was a wrap-around in the ring buffer, it will find + # remnants at the top. As those remnants do not conform to the + # expected line format, they are discarded + continue + + level_facility_str, timestamp_str, line = m.groups() + level_facility = int(level_facility_str) + # The lower 3 bit are the log level, the rest are the log facility + level = level_facility & 7 + facility = level_facility >> 3 + level_txt = self.get_level_text(level) + facility_txt = self.get_facility_text(facility) + caller = renderers.NotAvailableValue() + yield facility_txt, level_txt, timestamp_str, caller, line + + +class Kmsg_3_5_to_3_11(ABCKmsg): + """While 'log_buf' is declared as a pointer and '__log_buf' as a char array, + it essentially holds an array of 'log' structs. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return ( + vmlinux.has_type("log") + and vmlinux.get_type("log").has_member("ts_nsec") + and vmlinux.has_symbol("log_first_idx") + ) + + def _get_log_struct_name(self): + return "log" + + def get_text_from_log(self, msg) -> str: + log_struct_name = self._get_log_struct_name() + log_struct_size = self.vmlinux.get_type(log_struct_name).size + msg_offset = msg.vol.offset + log_struct_size return self.get_string(msg_offset, msg.text_len) def get_log_lines(self, msg) -> Generator[str, None, None]: if msg.text_len > 0: - text = self.get_text_from_printk_log(msg) + text = self.get_text_from_log(msg) yield from text.splitlines() def get_dict_lines(self, msg) -> Generator[str, None, None]: if msg.dict_len == 0: return None - dict_offset = msg.vol.offset + self.vmlinux.get_type('printk_log').size + msg.text_len - dict_data = self._context.layers[self.layer_name].read(dict_offset, msg.dict_len) - for chunk in dict_data.split(b'\x00'): + + log_struct_name = self._get_log_struct_name() + log_struct_size = self.vmlinux.get_type(log_struct_name).size + dict_offset = msg.vol.offset + log_struct_size + msg.text_len + dict_data = self._context.layers[self.layer_name].read( + dict_offset, msg.dict_len + ) + for chunk in dict_data.split(b"\x00"): yield " " + chunk.decode() def run(self) -> Iterator[Tuple[str, str, str, str, str]]: - log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name = 'log_buf') - if log_buf_ptr == 0: - # This is weird, let's fallback to check the static ringbuffer. - log_buf_ptr = self.vmlinux.object_from_symbol(symbol_name = '__log_buf').vol.offset - if log_buf_ptr == 0: - raise ValueError("Log buffer is not available") + # First, the ring buffer size is determined in the kernel configuration + # by CONFIG_LOG_BUF_SHIFT. This static buffer is held in the '__log_buf' + # global variable, with 'log_buf' serving as a pointer to it. + # The user can also update this size using 'log_buf_len' in the + # kernel boot parameters. Additionally, in SMP systems with over 64 CPUs, + # the ring buffer size dynamically allocates based on the number of CPUs, + # following CONFIG_LOG_CPU_MAX_BUF_SHIFT. + # In the last two cases mentioned above, the 'log_buf' pointer is + # updated to this new buffer. The original static buffer in '__log_buf' + # remains unused. Therefore, it is crucial to read from 'log_buf' rather + # than '__log_buf'. + + log_buf_ptr = self.vmlinux.object_from_symbol("log_buf") + log_buf_len = self.vmlinux.object_from_symbol("log_buf_len") + + log_first_idx = int(self.vmlinux.object_from_symbol("log_first_idx")) + log_next_idx = int(self.vmlinux.object_from_symbol("log_next_idx")) + + log_struct_name = self._get_log_struct_name() - log_first_idx = int(self.vmlinux.object_from_symbol(symbol_name = 'log_first_idx')) cur_idx = log_first_idx - end_idx = None # We don't need log_next_idx here. See below msg.len == 0 - while cur_idx != end_idx: - end_idx = log_first_idx + if log_first_idx < log_next_idx: + end_idx = log_next_idx + else: + end_idx = log_buf_len + + while cur_idx < end_idx: msg_offset = log_buf_ptr + cur_idx # type: ignore - msg = self.vmlinux.object(object_type = 'printk_log', offset = msg_offset) + msg = self.vmlinux.object(object_type=log_struct_name, offset=msg_offset) if msg.len == 0: - # As per kernel/printk/printk.c: + # As per kernel/printk.c: # A length == 0 for the next message indicates a wrap-around to # the beginning of the buffer. cur_idx = 0 + end_idx = log_next_idx else: facility, level, timestamp, caller = self.get_prefix(msg) level_txt = self.get_level_text(level) @@ -237,39 +333,53 @@ class KmsgLegacy(ABCKmsg): cur_idx += msg.len -class KmsgFiveTen(ABCKmsg): - """In 5.10 the kernel ringbuffer implementation changed. +class Kmsg_3_11_to_5_10(Kmsg_3_5_to_3_11): + """Starting from version 3.11, the struct 'log' was renamed to 'printk_log'. + While 'log_buf' is declared as a pointer and '__log_buf' as a char array, + it essentially holds an array of 'printk_log' structs. + """ + + @classmethod + def symtab_checks(cls, vmlinux) -> bool: + return vmlinux.has_type("printk_log") + + def _get_log_struct_name(self): + return "printk_log" + + +class Kmsg_5_10_to_(ABCKmsg): + """In 5.10 the kernel ring buffer implementation changed. Previously only one process should read /proc/kmsg and it is permanently open and periodically read by the syslog daemon. A high level structure 'printk_ringbuffer' was added to represent the printk - ringbuffer which actually contains two ringbuffers. The descriptor ring + ring buffer which actually contains two ring buffers. The descriptor ring 'desc_ring' contains the records' metadata, text offsets and states. The data block ring 'text_data_ring' contains the records' text strings. A pointer to the high level structure is kept in the prb pointer which is - initialized to a static ringbuffer. + initialized to a static ring buffer. .. code-block:: c static struct printk_ringbuffer *prb = &printk_rb_static; - In SMP systems with more than 64 CPUs this ringbuffer size is dynamically + In SMP systems with more than 64 CPUs this ring buffer size is dynamically allocated according the number of CPUs based on the value of CONFIG_LOG_CPU_MAX_BUF_SHIFT. The prb pointer is updated consequently to - this dynamic ringbuffer in setup_log_buf(). + this dynamic ring buffer in setup_log_buf(). .. code-block:: c prb = &printk_rb_dynamic; - Behind scenes, log_buf is still used as external buffer. - When the static printk_ringbuffer struct is initialized, _DEFINE_PRINTKRB - sets text_data_ring.data pointer to the address in log_buf which points to - the static buffer __log_buff. - If a dynamic ringbuffer takes place, setup_log_buf() sets - text_data_ring.data of printk_rb_dynamic to the new allocated external - buffer via the prb_init function. - In that case, the original external static buffer in __log_buf and - printk_rb_static are unused. + Behind scenes, 'log_buf' is still used as external buffer. + When the static 'printk_ringbuffer' struct is initialized, _DEFINE_PRINTKRB + sets text_data_ring.data pointer to the address in 'log_buf' which points + to the static buffer '__log_buf'. + If a dynamic ring buffer takes place, setup_log_buf() sets + text_data_ring.data of 'printk_rb_dynamic' to the new allocated external + buffer via the 'prb_init' function. + In that case, the original external static buffer in '__log_buf' and + 'printk_rb_static' are unused. .. code-block:: c @@ -284,7 +394,7 @@ class KmsgFiveTen(ABCKmsg): @classmethod def symtab_checks(cls, vmlinux) -> bool: - return vmlinux.has_symbol('prb') + return vmlinux.has_symbol("prb") def get_text_from_data_ring(self, text_data_ring, desc, info) -> str: text_data_sz = text_data_ring.size_bits @@ -327,20 +437,29 @@ class KmsgFiveTen(ABCKmsg): def run(self) -> Iterator[Tuple[str, str, str, str, str]]: # static struct printk_ringbuffer *prb = &printk_rb_static; - ringbuffers = self.vmlinux.object_from_symbol(symbol_name = 'prb').dereference() + ringbuffers = self.vmlinux.object_from_symbol("prb").dereference() desc_ring = ringbuffers.desc_ring text_data_ring = ringbuffers.text_data_ring - desc_count = 1 << desc_ring.count_bits - desc_arr = self.vmlinux.object(object_type = "array", - offset = desc_ring.descs, - subtype = self.vmlinux.get_type("prb_desc"), - count = desc_count) - info_arr = self.vmlinux.object(object_type = "array", - offset = desc_ring.infos, - subtype = self.vmlinux.get_type("printk_info"), - count = desc_count) + + array_type = self.vmlinux.symbol_table_name + constants.BANG + "array" + + desc_arr = self._context.object( + array_type, + offset=desc_ring.descs, + subtype=self.vmlinux.get_type("prb_desc"), + count=desc_count, + layer_name=self.layer_name, + ) + + info_arr = self._context.object( + array_type, + offset=desc_ring.infos, + subtype=self.vmlinux.get_type("printk_info"), + count=desc_count, + layer_name=self.layer_name, + ) # See kernel/printk/printk_ringbuffer.h desc_state_var_bytes_sz = self.long_unsigned_int_size @@ -356,7 +475,10 @@ class KmsgFiveTen(ABCKmsg): desc = desc_arr[cur_id % desc_count] # type: ignore info = info_arr[cur_id % desc_count] # type: ignore desc_state = DescStateEnum((desc.state_var.counter >> desc_flags_shift) & 3) - if desc_state in (DescStateEnum.desc_committed, DescStateEnum.desc_finalized): + if desc_state in ( + DescStateEnum.desc_committed, + DescStateEnum.desc_finalized, + ): facility, level, timestamp, caller = self.get_prefix(info) level_txt = self.get_level_text(level) facility_txt = self.get_facility_text(facility) @@ -370,28 +492,35 @@ class KmsgFiveTen(ABCKmsg): cur_id &= desc_id_mask -class Kmsg(plugins.PluginInterface): +class Kmsg(interfaces.plugins.PluginInterface): """Kernel log buffer reader""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (1, 0, 2) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ['Intel32', 'Intel64']), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), ] def _generator(self) -> Iterator[Tuple[int, Tuple[str, str, str, str, str]]]: - for values in ABCKmsg.run_all(context = self.context, config = self.config): + for values in ABCKmsg.run_all(context=self.context, config=self.config): yield (0, values) def run(self): - return renderers.TreeGrid([("facility", str), - ("level", str), - ("timestamp", str), - ("caller", str), - ("line", str)], - self._generator()) # type: ignore + return renderers.TreeGrid( + [ + ("facility", str), + ("level", str), + ("timestamp", str), + ("caller", str), + ("line", str), + ], + self._generator(), + ) # type: ignore diff --git a/volatility3/framework/plugins/linux/lsmod.py b/volatility3/framework/plugins/linux/lsmod.py index ecb262d00..a65b0d00b 100644 --- a/volatility3/framework/plugins/linux/lsmod.py +++ b/volatility3/framework/plugins/linux/lsmod.py @@ -25,13 +25,17 @@ class Lsmod(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), ] @classmethod - def list_modules(cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str) -> Iterable[ - interfaces.objects.ObjectInterface]: + def list_modules( + cls, context: interfaces.context.ContextInterface, vmlinux_module_name: str + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the modules in the primary layer. Args: @@ -46,7 +50,7 @@ class Lsmod(plugins.PluginInterface): """ vmlinux = context.modules[vmlinux_module_name] - modules = vmlinux.object_from_symbol(symbol_name = "modules").cast("list_head") + modules = vmlinux.object_from_symbol(symbol_name="modules").cast("list_head") table_name = modules.vol.type_name.split(constants.BANG)[0] @@ -55,8 +59,7 @@ class Lsmod(plugins.PluginInterface): def _generator(self): try: - for module in self.list_modules(self.context, self.config['kernel']): - + for module in self.list_modules(self.context, self.config["kernel"]): mod_size = module.get_init_size() + module.get_core_size() mod_name = utility.array_to_string(module.name) @@ -69,4 +72,7 @@ class Lsmod(plugins.PluginInterface): ) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Name", str), ("Size", int)], self._generator()) + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Name", str), ("Size", int)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/lsof.py b/volatility3/framework/plugins/linux/lsof.py index a074f5744..d970ad8a9 100644 --- a/volatility3/framework/plugins/linux/lsof.py +++ b/volatility3/framework/plugins/linux/lsof.py @@ -4,7 +4,7 @@ """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" import logging -from typing import List +from typing import List, Callable from volatility3.framework import renderers, interfaces, constants from volatility3.framework.configuration import requirements @@ -21,39 +21,69 @@ class Lsof(plugins.PluginInterface): _required_framework_version = (2, 0, 0) + _version = (1, 1, 0) + @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] - def _generator(self, tasks): - symbol_table = None - for task in tasks: - if symbol_table is None: + @classmethod + def list_fds( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + filter_func: Callable[[int], bool] = lambda _: False, + ): + linuxutils_symbol_table = None # type: ignore + for task in pslist.PsList.list_tasks(context, symbol_table, filter_func): + if linuxutils_symbol_table is None: if constants.BANG not in task.vol.type_name: raise ValueError("Task is not part of a symbol table") - symbol_table = task.vol.type_name.split(constants.BANG)[0] + linuxutils_symbol_table = task.vol.type_name.split(constants.BANG)[0] - name = utility.array_to_string(task.comm) + task_comm = utility.array_to_string(task.comm) pid = int(task.pid) - for fd_num, _, full_path in linux.LinuxUtilities.files_descriptors_for_process( - self.context, symbol_table, task): - yield (0, (pid, name, fd_num, full_path)) + fd_generator = linux.LinuxUtilities.files_descriptors_for_process( + context, linuxutils_symbol_table, task + ) + + for fd_fields in fd_generator: + yield pid, task_comm, task, fd_fields + + def _generator(self, pids, symbol_table): + filter_func = pslist.PsList.create_pid_filter(pids) + fds_generator = self.list_fds( + self.context, symbol_table, filter_func=filter_func + ) + + for pid, task_comm, _task, fd_fields in fds_generator: + fd_num, _filp, full_path = fd_fields + + fields = (pid, task_comm, fd_num, full_path) + yield (0, fields) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + pids = self.config.get("pid", None) + symbol_table = self.config["kernel"] - return renderers.TreeGrid([("PID", int), ("Process", str), ("FD", int), ("Path", str)], - self._generator( - pslist.PsList.list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + tree_grid_args = [("PID", int), ("Process", str), ("FD", int), ("Path", str)] + return renderers.TreeGrid(tree_grid_args, self._generator(pids, symbol_table)) diff --git a/volatility3/framework/plugins/linux/malfind.py b/volatility3/framework/plugins/linux/malfind.py index abc2cf7d2..cf06ee0cc 100644 --- a/volatility3/framework/plugins/linux/malfind.py +++ b/volatility3/framework/plugins/linux/malfind.py @@ -3,7 +3,7 @@ # from typing import List - +import logging from volatility3.framework import constants, interfaces from volatility3.framework import renderers from volatility3.framework.configuration import requirements @@ -11,6 +11,8 @@ from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class Malfind(interfaces.plugins.PluginInterface): """Lists process memory ranges that potentially contain injected code.""" @@ -20,13 +22,20 @@ class Malfind(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _list_injections(self, task): @@ -35,19 +44,31 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer_name = task.add_process_layer() if not proc_layer_name: - return + return None proc_layer = self.context.layers[proc_layer_name] - for vma in task.mm.get_mmap_iter(): - if vma.is_suspicious() and vma.get_name(self.context, task) != "[vdso]": - data = proc_layer.read(vma.vm_start, 64, pad = True) + for vma in task.mm.get_vma_iter(): + vma_name = vma.get_name(self.context, task) + vollog.debug( + f"Injections : processing PID {task.pid} : VMA {vma_name} : {hex(vma.vm_start)}-{hex(vma.vm_end)}" + ) + if ( + vma.is_suspicious(proc_layer) + and vma.get_name(self.context, task) != "[vdso]" + ): + data = proc_layer.read(vma.vm_start, 64, pad=True) yield vma, data def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel - vmlinux = self.context.modules[self.config['kernel']] - if self.context.symbol_space.get_type(vmlinux.symbol_table_name + constants.BANG + "pointer").size == 4: + vmlinux = self.context.modules[self.config["kernel"]] + if ( + self.context.symbol_space.get_type( + vmlinux.symbol_table_name + constants.BANG + "pointer" + ).size + == 4 + ): is_32bit_arch = True else: is_32bit_arch = False @@ -61,18 +82,39 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly(data, vma.vm_start, architecture) + disasm = interfaces.renderers.Disassembly( + data, vma.vm_start, architecture + ) - yield (0, (task.pid, process_name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), - vma.get_protection(), format_hints.HexBytes(data), disasm)) + yield ( + 0, + ( + task.pid, + process_name, + format_hints.Hex(vma.vm_start), + format_hints.Hex(vma.vm_end), + vma.get_protection(), + format_hints.HexBytes(data), + disasm, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex), - ("End", format_hints.Hex), ("Protection", str), ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly)], - self._generator( - pslist.PsList.list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Protection", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", interfaces.renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/mountinfo.py b/volatility3/framework/plugins/linux/mountinfo.py index 6f3cb712d..da743bb60 100644 --- a/volatility3/framework/plugins/linux/mountinfo.py +++ b/volatility3/framework/plugins/linux/mountinfo.py @@ -1,7 +1,6 @@ # This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0 # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -# Author: Gustavo Moreira import logging from collections import namedtuple @@ -10,12 +9,28 @@ from typing import Tuple, List, Iterable, Union from volatility3.framework import renderers, interfaces from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins +from volatility3.framework.symbols import linux from volatility3.plugins.linux import pslist + vollog = logging.getLogger(__name__) -MountInfoData = namedtuple("MountInfoData", ("mnt_id", "parent_id", "st_dev", "mnt_root_path", "path_root", - "mnt_opts", "fields", "mnt_type", "devname", "sb_opts")) +MountInfoData = namedtuple( + "MountInfoData", + ( + "mnt_id", + "parent_id", + "st_dev", + "mnt_root_path", + "path_root", + "mnt_opts", + "fields", + "mnt_type", + "devname", + "sb_opts", + ), +) + class MountInfo(plugins.PluginInterface): """Lists mount points on processes mount namespaces""" @@ -27,61 +42,46 @@ class MountInfo(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name="kernel", description="Linux kernel", - architectures=["Intel32", "Intel64"]), - requirements.PluginRequirement(name="pslist", - plugin=pslist.PsList, version=(2, 0, 0)), - requirements.ListRequirement(name="pids", - description="Filter on specific process IDs.", - element_type=int, - optional=True), - requirements.ListRequirement(name="mntns", - description="Filter results by mount namespace. " - "Otherwise, all of them are shown.", - element_type=int, - optional=True), - requirements.BooleanRequirement(name="mount-format", - description="Shows a brief summary of the mount points information " - "with similar output format to the older /proc/[pid]/mounts or the " - "user-land command 'mount -l'.", - optional=True, - default=False), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 1, 0) + ), + requirements.ListRequirement( + name="pids", + description="Filter on specific process IDs.", + element_type=int, + optional=True, + ), + requirements.ListRequirement( + name="mntns", + description="Filter results by mount namespace. " + "Otherwise, all of them are shown.", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="mount-format", + description="Shows a brief summary of the mount points information " + "with similar output format to the older /proc/[pid]/mounts or the " + "user-land command 'mount -l'.", + optional=True, + default=False, + ), ] @classmethod - def _do_get_path(cls, mnt, fs_root) -> Union[None, str]: - """It mimics the Linux kernel prepend_path function.""" - vfsmnt = mnt.mnt - dentry = vfsmnt.get_mnt_root() - - path_reversed = [] - while dentry != fs_root.dentry or vfsmnt.vol.offset != fs_root.mnt: - if dentry == vfsmnt.get_mnt_root() or dentry.is_root(): - parent = mnt.get_mnt_parent().dereference() - # Escaped? - if dentry != vfsmnt.get_mnt_root(): - return None - - # Global root? - if mnt.vol.offset != parent.vol.offset: - dentry = mnt.get_mnt_mountpoint() - mnt = parent - vfsmnt = mnt.mnt - continue - - return None - - parent = dentry.d_parent - dname = dentry.d_name.name_as_str() - path_reversed.append(dname.strip("/")) - dentry = parent - - path = "/" + "/".join(reversed(path_reversed)) - return path - - @classmethod - def get_mountinfo(cls, mnt, task) -> Union[None, Tuple[int, int, str, str, str, List[str], - List[str], str, str, List[str]]]: + def get_mountinfo( + cls, mnt, task + ) -> Union[ + None, Tuple[int, int, str, str, str, List[str], List[str], str, str, List[str]] + ]: """Extract various information about a mount point. It mimics the Linux kernel show_mountinfo function. """ @@ -89,8 +89,8 @@ class MountInfo(plugins.PluginInterface): if not mnt_root: return None - path_root = cls._do_get_path(mnt, task.fs.root) - if path_root is None: + path_root = linux.LinuxUtilities.get_path_mnt(task, mnt) + if not path_root: return None mnt_root_path = mnt_root.path() @@ -130,37 +130,75 @@ class MountInfo(plugins.PluginInterface): sb_opts.append(superblock.get_flags_access()) sb_opts.extend(superblock.get_flags_opts()) - return MountInfoData(mnt_id, parent_id, st_dev, mnt_root_path, path_root, mnt_opts, fields, - mnt_type, devname, sb_opts) + return MountInfoData( + mnt_id, + parent_id, + st_dev, + mnt_root_path, + path_root, + mnt_opts, + fields, + mnt_type, + devname, + sb_opts, + ) - def _get_tasks_mountpoints(self, tasks: Iterable[interfaces.objects.ObjectInterface], per_namespace: bool): - seen_namespaces = set() + def _get_tasks_mountpoints( + self, + tasks: Iterable[interfaces.objects.ObjectInterface], + filtered_by_pids: bool, + ): + seen_mountpoints = set() for task in tasks: - if not (task and task.fs and task.fs.root and task.nsproxy and task.nsproxy.mnt_ns): - # This task doesn't have all the information required + if not ( + task + and task.fs + and task.fs.root + and task.nsproxy + and task.nsproxy.mnt_ns + ): + # This task doesn't have all the information required. + # It should be a kernel < 2.6.30 continue mnt_namespace = task.nsproxy.mnt_ns - mnt_ns_id = mnt_namespace.get_inode() - - if per_namespace: - if mnt_ns_id in seen_namespaces: - continue - else: - seen_namespaces.add(mnt_ns_id) + try: + mnt_ns_id = mnt_namespace.get_inode() + except AttributeError: + mnt_ns_id = renderers.NotAvailableValue() for mount in mnt_namespace.get_mount_points(): + # When PIDs are filtered, it makes sense that the user want to + # see each of those processes mount points. So we don't filter + # by mount id in this case. + if not filtered_by_pids: + mnt_id = int(mount.mnt_id) + if mnt_id in seen_mountpoints: + continue + else: + seen_mountpoints.add(mnt_id) + yield task, mount, mnt_ns_id def _generator( - self, - tasks: Iterable[interfaces.objects.ObjectInterface], - mnt_ns_ids: List[int], - mount_format: bool, - per_namespace: bool) -> Iterable[Tuple[int, Tuple]]: + self, + tasks: Iterable[interfaces.objects.ObjectInterface], + mnt_ns_ids: List[int], + mount_format: bool, + filtered_by_pids: bool, + ) -> Iterable[Tuple[int, Tuple]]: + show_filter_warning = False + for task, mnt, mnt_ns_id in self._get_tasks_mountpoints( + tasks, filtered_by_pids + ): + if mnt_ns_ids and isinstance(mnt_ns_id, renderers.NotAvailableValue): + show_filter_warning = True - for task, mnt, mnt_ns_id in self._get_tasks_mountpoints(tasks, per_namespace): - if mnt_ns_ids and mnt_ns_id not in mnt_ns_ids: + if ( + not isinstance(mnt_ns_id, renderers.NotAvailableValue) + and mnt_ns_ids + and mnt_ns_id not in mnt_ns_ids + ): continue mnt_info = self.get_mountinfo(mnt, task) @@ -173,48 +211,86 @@ class MountInfo(plugins.PluginInterface): all_opts.update(mnt_info.sb_opts) all_opts_str = ",".join(all_opts) - extra_fields_values = [mnt_info.devname, mnt_info.path_root, mnt_info.mnt_type, all_opts_str] + extra_fields_values = [ + mnt_info.devname, + mnt_info.path_root, + mnt_info.mnt_type, + all_opts_str, + ] else: mnt_opts_str = ",".join(mnt_info.mnt_opts) fields_str = " ".join(mnt_info.fields) sb_opts_str = ",".join(mnt_info.sb_opts) - extra_fields_values = [mnt_info.mnt_id, mnt_info.parent_id, mnt_info.st_dev, mnt_info.mnt_root_path, - mnt_info.path_root, mnt_opts_str, fields_str, mnt_info.mnt_type, - mnt_info.devname, sb_opts_str] + extra_fields_values = [ + mnt_info.mnt_id, + mnt_info.parent_id, + mnt_info.st_dev, + mnt_info.mnt_root_path, + mnt_info.path_root, + mnt_opts_str, + fields_str, + mnt_info.mnt_type, + mnt_info.devname, + sb_opts_str, + ] fields_values = [mnt_ns_id] - if not per_namespace: + if filtered_by_pids: fields_values.append(task.pid) fields_values.extend(extra_fields_values) yield (0, fields_values) + if show_filter_warning: + vollog.warning( + "Could not filter by mount namespace id. This field is not available in this kernel." + ) + def run(self): - pids = self.config.get('pids') - mount_ns_ids = self.config.get('mntns') - mount_format = self.config.get('mount-format') + pids = self.config.get("pids") + mount_ns_ids = self.config.get("mntns") + mount_format = self.config.get("mount-format") pid_filter = pslist.PsList.create_pid_filter(pids) - tasks = pslist.PsList.list_tasks(self.context, self.config['kernel'], filter_func=pid_filter) + tasks = pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=pid_filter + ) columns = [("MNT_NS_ID", int)] # The PID column does not make sense when a PID filter is not specified. In that case, the default behavior is # to displays the mountpoints per namespace. if pids: columns.append(("PID", int)) - per_namespace = False + filtered_by_pids = True else: - per_namespace = True + filtered_by_pids = False - if self.config.get('mount-format'): - extra_columns = [("DEVNAME", str), ("PATH", str), ("FSTYPE", str), ("MNT_OPTS", str)] + if self.config.get("mount-format"): + extra_columns = [ + ("DEVNAME", str), + ("PATH", str), + ("FSTYPE", str), + ("MNT_OPTS", str), + ] else: # /proc/[pid]/mountinfo output format - extra_columns = [("MOUNT ID", int), ("PARENT_ID", int), ("MAJOR:MINOR", str), ("ROOT", str), - ("MOUNT_POINT", str), ("MOUNT_OPTIONS", str), ("FIELDS", str), ("FSTYPE", str), - ("MOUNT_SRC", str), ("SB_OPTIONS", str)] + extra_columns = [ + ("MOUNT ID", int), + ("PARENT_ID", int), + ("MAJOR:MINOR", str), + ("ROOT", str), + ("MOUNT_POINT", str), + ("MOUNT_OPTIONS", str), + ("FIELDS", str), + ("FSTYPE", str), + ("MOUNT_SRC", str), + ("SB_OPTIONS", str), + ] columns.extend(extra_columns) - return renderers.TreeGrid(columns, self._generator(tasks, mount_ns_ids, mount_format, per_namespace)) + return renderers.TreeGrid( + columns, + self._generator(tasks, mount_ns_ids, mount_format, filtered_by_pids), + ) diff --git a/volatility3/framework/plugins/linux/proc.py b/volatility3/framework/plugins/linux/proc.py index 13fd87f53..e7d38b107 100644 --- a/volatility3/framework/plugins/linux/proc.py +++ b/volatility3/framework/plugins/linux/proc.py @@ -4,40 +4,187 @@ """A module containing a collection of plugins that produce data typically found in Linux's /proc file system.""" -from volatility3.framework import renderers +import logging +from typing import Callable, Generator, Type, Optional + +from volatility3.framework import renderers, interfaces, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist +vollog = logging.getLogger(__name__) + class Maps(plugins.PluginInterface): """Lists all memory maps for all processes.""" _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @classmethod def get_requirements(cls): # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed memory segments", + default=False, + optional=True, + ), + requirements.ListRequirement( + name="address", + description="Process virtual memory addresses to include " + "(all other VMA sections are excluded). This can be any " + "virtual address within the VMA section.", + element_type=int, + optional=True, + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size for dumped VMA sections " + "(all the bigger sections will be ignored)", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), ] + @classmethod + def list_vmas( + cls, + task: interfaces.objects.ObjectInterface, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: True, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: + """Lists the Virtual Memory Areas of a specific process. + + Args: + task: task object from which to list the vma + filter_func: Function to take a vma and return False if it should be filtered out + + Returns: + Yields vmas based on the task and filtered based on the filter function + """ + if task.mm: + for vma in task.mm.get_vma_iter(): + if filter_func(vma): + yield vma + else: + vollog.debug( + f"Excluded vma at offset {vma.vol.offset:#x} for pid {task.pid} due to filter_func" + ) + else: + vollog.debug( + f"Excluded pid {task.pid} as there is no mm member. It is likely a kernel thread." + ) + + @classmethod + def vma_dump( + cls, + context: interfaces.context.ContextInterface, + task: interfaces.objects.ObjectInterface, + vm_start: int, + vm_end: int, + open_method: Type[interfaces.plugins.FileHandlerInterface], + maxsize: int = MAXSIZE_DEFAULT, + ) -> Optional[interfaces.plugins.FileHandlerInterface]: + """Extracts the complete data for VMA as a FileInterface. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + task: an task_struct instance + vm_start: The start virtual address from the vma to dump + vm_end: The end virtual address from the vma to dump + open_method: class to provide context manager for opening the file + maxsize: Max size of VMA section (default MAXSIZE_DEFAULT) + + Returns: + An open FileInterface object containing the complete data for the task or None in the case of failure + """ + pid = task.pid + + try: + proc_layer_name = task.add_process_layer() + except exceptions.InvalidAddressException as excp: + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + pid, excp.invalid_address, excp.layer_name + ) + ) + return None + vm_size = vm_end - vm_start + + # check if vm_size is negative, this should never happen. + if vm_size < 0: + vollog.warning( + f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is negative." + ) + return None + # check if vm_size is larger than the maxsize limit, and therefore is not saved out. + if maxsize <= vm_size: + vollog.warning( + f"Skip virtual memory dump for pid {pid} between {vm_start:#x}-{vm_end:#x} as {vm_size} is larger than maxsize limit of {maxsize}" + ) + return None + proc_layer = context.layers[proc_layer_name] + file_name = f"pid.{pid}.vma.{vm_start:#x}-{vm_end:#x}.dmp" + try: + file_handle = open_method(file_name) + chunk_size = 1024 * 1024 * 10 + offset = vm_start + while offset < vm_start + vm_size: + to_read = min(chunk_size, vm_start + vm_size - offset) + data = proc_layer.read(offset, to_read, pad=True) + file_handle.write(data) + offset += to_read + except Exception as excp: + vollog.debug(f"Unable to dump virtual memory {file_name}: {excp}") + return None + return file_handle + def _generator(self, tasks): + # build filter for addresses if required + address_list = self.config.get("address", None) + if not address_list: + # do not filter as no address_list was supplied + vma_filter_func = lambda _: True + else: + # filter for any vm_start that matches the supplied address config + def vma_filter_function(x: interfaces.objects.ObjectInterface) -> bool: + addrs_in_vma = [ + addr for addr in address_list if x.vm_start <= addr <= x.vm_end + ] + + # if any of the user supplied addresses would fall within this vma return true + if addrs_in_vma: + return True + else: + return False + + vma_filter_func = vma_filter_function for task in tasks: if not task.mm: continue - name = utility.array_to_string(task.comm) - for vma in task.mm.get_mmap_iter(): + for vma in self.list_vmas(task, filter_func=vma_filter_func): flags = vma.get_protection() page_offset = vma.get_page_offset() major = 0 @@ -51,20 +198,71 @@ class Maps(plugins.PluginInterface): major = inode_object.i_sb.major minor = inode_object.i_sb.minor inode = inode_object.i_ino - path = vma.get_name(self.context, task) - yield (0, (task.pid, name, format_hints.Hex(vma.vm_start), format_hints.Hex(vma.vm_end), flags, - format_hints.Hex(page_offset), major, minor, inode, path)) + file_output = "Disabled" + if self.config["dump"]: + file_output = "Error outputting file" + try: + vm_start = vma.vm_start + vm_end = vma.vm_end + except AttributeError: + vollog.debug( + f"Unable to find the vm_start and vm_end for vma at {vma.vol.offset:#x} for pid {task.pid}" + ) + vm_start = None + vm_end = None + if vm_start and vm_end: + # only attempt to dump the memory if we have vm_start and vm_end + file_handle = self.vma_dump( + self.context, + task, + vm_start, + vm_end, + self.open, + self.config["maxsize"], + ) + + if file_handle: + file_handle.close() + file_output = file_handle.preferred_filename + yield ( + 0, + ( + task.pid, + name, + format_hints.Hex(vma.vm_start), + format_hints.Hex(vma.vm_end), + flags, + format_hints.Hex(page_offset), + major, + minor, + inode, + path, + file_output, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - return renderers.TreeGrid([("PID", int), ("Process", str), - ("Start", format_hints.Hex), ("End", format_hints.Hex), ("Flags", str), - ("PgOff", format_hints.Hex), ("Major", int), ("Minor", int), ("Inode", int), - ("File Path", str)], - self._generator( - pslist.PsList.list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Flags", str), + ("PgOff", format_hints.Hex), + ("Major", int), + ("Minor", int), + ("Inode", int), + ("File Path", str), + ("File output", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/psaux.py b/volatility3/framework/plugins/linux/psaux.py index c62712907..a4a23498f 100644 --- a/volatility3/framework/plugins/linux/psaux.py +++ b/volatility3/framework/plugins/linux/psaux.py @@ -4,14 +4,15 @@ from typing import Optional +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework import symbols, exceptions, renderers, interfaces +from volatility3.framework.interfaces import plugins from volatility3.framework.objects import utility from volatility3.plugins.linux import pslist -from volatility3.framework.interfaces import plugins + class PsAux(plugins.PluginInterface): - """ Lists processes with their command line arguments """ + """Lists processes with their command line arguments""" _required_framework_version = (2, 0, 0) @@ -19,17 +20,25 @@ class PsAux(plugins.PluginInterface): def get_requirements(cls): # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] - def _get_command_line_args(self, task: interfaces.objects.ObjectInterface, - name: str) -> Optional[str]: + def _get_command_line_args( + self, task: interfaces.objects.ObjectInterface, name: str + ) -> Optional[str]: """ Reads the command line arguments of a process These are stored on the userland stack @@ -40,7 +49,7 @@ class PsAux(plugins.PluginInterface): name: string name of the process (from task.comm) """ - # kernel theads never have an mm as they do not have userland mappings + # kernel threads never have an mm as they do not have userland mappings try: mm = task.mm except exceptions.InvalidAddressException: @@ -68,7 +77,7 @@ class PsAux(plugins.PluginInterface): return renderers.UnreadableValue() # the arguments are null byte terminated, replace the nulls with spaces - s = argv.decode().split('\x00') + s = argv.decode().split("\x00") args = " ".join(s) else: # kernel thread @@ -83,7 +92,7 @@ class PsAux(plugins.PluginInterface): return args def _generator(self, tasks): - """ Generates a listing of processes along with command line arguments """ + """Generates a listing of processes along with command line arguments""" # walk the process list and report the arguments for task in tasks: @@ -101,11 +110,13 @@ class PsAux(plugins.PluginInterface): yield (0, (pid, ppid, name, args)) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - - return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str), ("ARGS", str)], - self._generator( - pslist.PsList.list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + return renderers.TreeGrid( + [("PID", int), ("PPID", int), ("COMM", str), ("ARGS", str)], + self._generator( + pslist.PsList.list_tasks( + self.context, self.config["kernel"], filter_func=filter_func + ) + ), + ) diff --git a/volatility3/framework/plugins/linux/pslist.py b/volatility3/framework/plugins/linux/pslist.py index f9a1fe2a0..9afd13e5a 100644 --- a/volatility3/framework/plugins/linux/pslist.py +++ b/volatility3/framework/plugins/linux/pslist.py @@ -1,12 +1,15 @@ # 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 # -from typing import Callable, Iterable, List, Any, Tuple +from typing import Any, Callable, Iterable, List, Tuple -from volatility3.framework import renderers, interfaces +from volatility3.framework import interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.linux.extensions import elf +from volatility3.plugins.linux import elfs class PsList(interfaces.plugins.PluginInterface): @@ -14,25 +17,43 @@ class PsList(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) - _version = (2, 1, 0) + _version = (2, 2, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), - requirements.BooleanRequirement(name="threads", - description="Include user threads", - optional=True, - default=False), - requirements.BooleanRequirement(name="decorate_comm", - description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", - optional=True, - default=False), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="elfs", plugin=elfs.Elfs, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="threads", + description="Include user threads", + optional=True, + default=False, + ), + requirements.BooleanRequirement( + name="decorate_comm", + description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", + optional=True, + default=False, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed processes", + optional=True, + default=False, + ), ] @classmethod @@ -57,12 +78,11 @@ class PsList(interfaces.plugins.PluginInterface): else: return lambda _: False - def _get_task_fields( - self, - task: interfaces.objects.ObjectInterface, - decorate_comm: bool = False) -> Tuple[int, int, int, str]: + @classmethod + def get_task_fields( + cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False + ) -> Tuple[int, int, int, str]: """Extract the fields needed for the final output - Args: task: A task object from where to get the fields. decorate_comm: If True, it decorates the comm string of @@ -82,14 +102,55 @@ class PsList(interfaces.plugins.PluginInterface): elif task.is_user_thread: name = f"{{{name}}}" - task_fields = (format_hints.Hex(task.vol.offset), pid, tid, ppid, name) + task_fields = (task.vol.offset, pid, tid, ppid, name) return task_fields + def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str: + """Extract the elf for the process if requested + Args: + task: A task object to extract from. + Returns: + A string showing the results of the extraction, either + the filename used or an error. + """ + elf_table_name = intermed.IntermediateSymbolTable.create( + self.context, + self.config_path, + "linux", + "elf", + class_types=elf.class_types, + ) + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + # if we can't build a proc layer we can't + # extract the elf + return renderers.NotApplicableValue() + else: + # Find the vma that belongs to the main ELF of the process + file_output = "Error outputting file" + for v in task.mm.get_mmap_iter(): + if v.vm_start == task.mm.start_code: + file_handle = elfs.Elfs.elf_dump( + self.context, + proc_layer_name, + elf_table_name, + v, + task, + self.open, + ) + if file_handle: + file_output = str(file_handle.preferred_filename) + file_handle.close() + break + return file_output + def _generator( - self, - pid_filter: Callable[[Any], bool], - include_threads: bool = False, - decorate_comm: bool = False): + self, + pid_filter: Callable[[Any], bool], + include_threads: bool = False, + decorate_comm: bool = False, + dump: bool = False, + ): """Generates the tasks list. Args: @@ -101,23 +162,38 @@ class PsList(interfaces.plugins.PluginInterface): - User threads: in curly brackets, - Kernel threads: in square brackets Defaults to False. + dump: If True, the main executable of the process is written to a file + Defaults to False. Yields: Each rows """ - for task in self.list_tasks(self.context, - self.config['kernel'], - pid_filter, - include_threads): - row = self._get_task_fields(task, decorate_comm) - yield (0, row) + for task in self.list_tasks( + self.context, self.config["kernel"], pid_filter, include_threads + ): + if dump: + file_output = self._get_file_output(task) + else: + file_output = "Disabled" + + offset, pid, tid, ppid, name = self.get_task_fields(task, decorate_comm) + + yield 0, ( + format_hints.Hex(offset), + pid, + tid, + ppid, + name, + file_output, + ) @classmethod def list_tasks( - cls, - context: interfaces.context.ContextInterface, - vmlinux_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False, - include_threads: bool = False) -> Iterable[interfaces.objects.ObjectInterface]: + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + include_threads: bool = False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer. Args: @@ -130,7 +206,7 @@ class PsList(interfaces.plugins.PluginInterface): """ vmlinux = context.modules[vmlinux_module_name] - init_task = vmlinux.object_from_symbol(symbol_name = "init_task") + init_task = vmlinux.object_from_symbol(symbol_name="init_task") # Note that the init_task itself is not yielded, since "ps" also never shows it. for task in init_task.tasks: @@ -143,10 +219,20 @@ class PsList(interfaces.plugins.PluginInterface): yield from task.get_threads() def run(self): - pids = self.config.get('pid') - include_threads = self.config.get('threads') - decorate_comm = self.config.get('decorate_comm') + pids = self.config.get("pid") + include_threads = self.config.get("threads") + decorate_comm = self.config.get("decorate_comm") + dump = self.config.get("dump") filter_func = self.create_pid_filter(pids) - columns = [("OFFSET (V)", format_hints.Hex), ("PID", int), ("TID", int), ("PPID", int), ("COMM", str)] - return renderers.TreeGrid(columns, self._generator(filter_func, include_threads, decorate_comm)) + columns = [ + ("OFFSET (V)", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ("File output", str), + ] + return renderers.TreeGrid( + columns, self._generator(filter_func, include_threads, decorate_comm, dump) + ) diff --git a/volatility3/framework/plugins/linux/psscan.py b/volatility3/framework/plugins/linux/psscan.py new file mode 100644 index 000000000..462577e58 --- /dev/null +++ b/volatility3/framework/plugins/linux/psscan.py @@ -0,0 +1,182 @@ +# This file is Copyright 2023 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 +from typing import Iterable, List, Tuple +import struct +from enum import Enum + +from volatility3.framework import renderers, interfaces, symbols, constants, exceptions +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.layers import scanners +from volatility3.framework.renderers import format_hints + +vollog = logging.getLogger(__name__) + + +class DescExitStateEnum(Enum): + """Enum for linux task exit_state as defined in include/linux/sched.h""" + + TASK_RUNNING = 0x00000000 + EXIT_DEAD = 0x00000010 + EXIT_ZOMBIE = 0x00000020 + EXIT_TRACE = EXIT_ZOMBIE | EXIT_DEAD + + +class PsScan(interfaces.plugins.PluginInterface): + """Scans for processes present in a particular linux image.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def _get_task_fields( + self, task: interfaces.objects.ObjectInterface + ) -> Tuple[int, int, int, str, str]: + """Extract the fields needed for the final output + + Args: + task: A task object from where to get the fields. + Returns: + A tuple with the fields to show in the plugin output. + """ + pid = task.tgid + tid = task.pid + ppid = 0 + + if task.parent.is_readable(): + ppid = task.parent.tgid + name = utility.array_to_string(task.comm) + exit_state = DescExitStateEnum(task.exit_state).name + + task_fields = ( + format_hints.Hex(task.vol.offset), + pid, + tid, + ppid, + name, + exit_state, + ) + return task_fields + + def _generator(self): + """Generates the tasks found from scanning.""" + + vmlinux_module_name = self.config["kernel"] + vmlinux = self.context.modules[vmlinux_module_name] + + for task in self.scan_tasks( + self.context, vmlinux_module_name, vmlinux.layer_name + ): + row = self._get_task_fields(task) + yield (0, row) + + @classmethod + def scan_tasks( + cls, + context: interfaces.context.ContextInterface, + vmlinux_module_name: str, + kernel_layer_name: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: + """Scans for tasks in the memory layer. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + vmlinux_module_name: The name of the kernel module on which to operate + kernel_layer_name: The name for the kernel layer + Yields: + Task objects + """ + vmlinux = context.modules[vmlinux_module_name] + + # check if this image is 32bit or 64bit + is_32bit = not symbols.symbol_table_is_64bit(context, vmlinux.symbol_table_name) + if is_32bit: + pack_format = "I" + else: + pack_format = "Q" + # get task_struct to find the offset to the sched_class pointer + sched_class_offset = vmlinux.get_type("task_struct").members["sched_class"][0] + kernel_layer = context.layers[kernel_layer_name] + + needles = [] + for symbol in vmlinux.symbols: + # find all sched_class names by searching by if they include '_sched_class', e.g. 'fair_sched_class' + if "_sched_class" in symbol: + # use canonicalize to set the appropriate sign extension for the addr + addr = kernel_layer.canonicalize( + vmlinux.get_symbol(symbol).address + vmlinux.offset + ) + packed_addr = struct.pack(pack_format, addr) + + # debug message to show needles being searched for and symbol names + vollog.debug( + f"Found a sched_class named {symbol} at offset {hex(addr)}. Will scan for these bytes: {packed_addr.hex()}" + ) + + # append to needles list the packed hex for searching + needles.append(packed_addr) + # find the memory layer to scan + if len(kernel_layer.dependencies) > 1: + vollog.warning( + f"Kernel layer depends on multiple layers however only {kernel_layer.dependencies[0]} will be scanned by this plugin." + ) + elif len(kernel_layer.dependencies) == 0: + vollog.error( + f"Kernel layer has no dependencies, meaning there is no memory layer for this plugin to scan." + ) + raise exceptions.LayerException( + kernel_layer_name, f"Layer {kernel_layer_name} has no dependencies" + ) + memory_layer_name = kernel_layer.dependencies[0] + memory_layer = context.layers[kernel_layer.dependencies[0]] + + # scan the memory_layer for these needles + for address, _ in memory_layer.scan( + context, scanners.MultiStringScanner(needles) + ): + # create task in the memory_layer + ptask = context.object( + vmlinux.symbol_table_name + constants.BANG + "task_struct", + offset=address - sched_class_offset, + layer_name=memory_layer_name, + native_layer_name=kernel_layer_name, + ) + + # sanity check exit_state + try: + # attempt tp parse the exist_state using the enum + DescExitStateEnum(ptask.exit_state) + except ValueError: + vollog.debug( + f"Skipping task_struct at {hex(ptask.vol.offset)} as exit_state {ptask.exit_state} is likely not valid" + ) + continue + # sanity check pid + if not (0 < ptask.pid < 65535): + vollog.debug( + f"Skipping task_struct at {hex(ptask.vol.offset)} as pid {ptask.pid} is likely not valid" + ) + continue + yield ptask + + def run(self): + columns = [ + ("OFFSET (P)", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ("EXIT_STATE", str), + ] + return renderers.TreeGrid(columns, self._generator()) diff --git a/volatility3/framework/plugins/linux/pstree.py b/volatility3/framework/plugins/linux/pstree.py index a44310147..efe5223df 100644 --- a/volatility3/framework/plugins/linux/pstree.py +++ b/volatility3/framework/plugins/linux/pstree.py @@ -2,24 +2,55 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints from volatility3.plugins.linux import pslist -class PsTree(pslist.PsList): +class PsTree(interfaces.plugins.PluginInterface): """Plugin for listing processes in a tree based on their parent process ID.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._tasks = {} - self._levels = {} - self._children = {} + _required_framework_version = (2, 0, 0) + + @classmethod + def get_requirements(cls): + # Since we're calling the plugin, make sure we have the plugin's requirements + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 2, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="threads", + description="Include user threads", + optional=True, + default=False, + ), + requirements.BooleanRequirement( + name="decorate_comm", + description="Show `user threads` comm in curly brackets, and `kernel threads` comm in square brackets", + optional=True, + default=False, + ), + ] def find_level(self, pid: int) -> None: """Finds how deep the PID is in the tasks hierarchy. Args: - pid: PID to find the level in the hierachy + pid: PID to find the level in the hierarchy """ seen = set([pid]) level = 0 @@ -39,17 +70,14 @@ class PsTree(pslist.PsList): self._levels[pid] = level def _generator( - self, - pid_filter, - include_threads: bool = False, - decorate_com: bool = False): + self, + tasks: list, + decorate_comm: bool = False, + ): """Generates the tasks hierarchy tree. Args: - pid_filter: A function which takes a process object and returns True if the process should be ignored/filtered - include_threads: If True, the output will also show the user threads - If False, only the thread group leaders will be shown - Defaults to False. + tasks: A list of task objects to be displayed decorate_comm: If True, it decorates the comm string of - User threads: in curly brackets, - Kernel threads: in square brackets @@ -57,11 +85,12 @@ class PsTree(pslist.PsList): Yields: Each rows """ - vmlinux = self.context.modules[self.config['kernel']] - for proc in self.list_tasks(self.context, - vmlinux.name, - filter_func=pid_filter, - include_threads=include_threads): + + self._tasks = {} + self._levels = {} + self._children = {} + + for proc in tasks: self._tasks[proc.pid] = proc # Build the child/level maps @@ -71,7 +100,10 @@ class PsTree(pslist.PsList): def yield_processes(pid): task = self._tasks[pid] - row = self._get_task_fields(task, decorate_com) + row = pslist.PsList.get_task_fields(task, decorate_comm) + # update the first element, the offset, in the row tuple to use format_hints.Hex + # as a simple int is returned from get_task_fields. + row = (format_hints.Hex(row[0]),) + row[1:] tid = task.pid yield (self._levels[tid] - 1, row) @@ -82,3 +114,27 @@ class PsTree(pslist.PsList): for pid, level in self._levels.items(): if level == 1: yield from yield_processes(pid) + + def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + include_threads = self.config.get("threads") + decorate_comm = self.config.get("decorate_comm") + + return renderers.TreeGrid( + [ + ("OFFSET (V)", format_hints.Hex), + ("PID", int), + ("TID", int), + ("PPID", int), + ("COMM", str), + ], + self._generator( + pslist.PsList.list_tasks( + self.context, + self.config["kernel"], + filter_func=filter_func, + include_threads=include_threads, + ), + decorate_comm=decorate_comm, + ), + ) diff --git a/volatility3/framework/plugins/linux/sockstat.py b/volatility3/framework/plugins/linux/sockstat.py new file mode 100644 index 000000000..e9c98a227 --- /dev/null +++ b/volatility3/framework/plugins/linux/sockstat.py @@ -0,0 +1,655 @@ +# 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 logging +from typing import Callable, Tuple, List, Dict + +from volatility3.framework import interfaces, exceptions, constants, objects +from volatility3.framework.renderers import TreeGrid, NotAvailableValue, format_hints +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility +from volatility3.framework.symbols import linux +from volatility3.plugins.linux import lsof + + +vollog = logging.getLogger(__name__) + + +class SockHandlers(interfaces.configuration.VersionableInterface): + """Handles several socket families extracting the sockets information.""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + def __init__(self, vmlinux, task): + self._vmlinux = vmlinux + self._task = task + + try: + netns_id = task.nsproxy.net_ns.get_inode() + except AttributeError: + netns_id = NotAvailableValue() + + self._netdevices = self._build_network_devices_map(netns_id) + + self._sock_family_handlers = { + "AF_UNIX": self._unix_sock, + "AF_INET": self._inet_sock, + "AF_INET6": self._inet_sock, + "AF_NETLINK": self._netlink_sock, + "AF_VSOCK": self._vsock_sock, + "AF_PACKET": self._packet_sock, + "AF_XDP": self._xdp_sock, + "AF_BLUETOOTH": self._bluetooth_sock, + } + + def _build_network_devices_map(self, netns_id: int) -> Dict: + """Given a namespace ID it returns a dictionary mapping each network + interface index (ifindex) to its network interface name: + + Args: + netns_id: The network namespace ID + + Returns: + netdevices_map: Mapping network interface index (ifindex) to network + interface name + """ + netdevices_map = {} + nethead = self._vmlinux.object_from_symbol(symbol_name="net_namespace_list") + net_symname = self._vmlinux.symbol_table_name + constants.BANG + "net" + for net in nethead.to_list(net_symname, "list"): + net_device_symname = ( + self._vmlinux.symbol_table_name + constants.BANG + "net_device" + ) + for net_dev in net.dev_base_head.to_list(net_device_symname, "dev_list"): + if ( + isinstance(netns_id, NotAvailableValue) + or net.get_inode() != netns_id + ): + continue + dev_name = utility.array_to_string(net_dev.name) + netdevices_map[net_dev.ifindex] = dev_name + return netdevices_map + + def process_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str], Dict]: + """Takes a kernel generic `sock` object and processes it with its respective socket family + + Args: + sock: Kernel generic `sock` object + + Returns a tuple with: + sock: The respective kernel's \*_sock object for that socket family + sock_stat: A tuple with the source and destination (address and port) along with its state string + socket_filter: A dictionary with information about the socket filter + """ + family = sock.get_family() + socket_filter = {} + sock_handler = self._sock_family_handlers.get(family) + if sock_handler: + try: + unix_sock, sock_stat = sock_handler(sock) + self._update_socket_filters_info(sock, socket_filter) + + return unix_sock, sock_stat, socket_filter + except exceptions.SymbolError as e: + # Cannot finds the *_sock type in the symbols + vollog.log( + constants.LOGLEVEL_V, + "Error processing socket family '%s': %s", + family, + e, + ) + else: + vollog.log(constants.LOGLEVEL_V, "Unsupported family '%s'", family) + + # Even if the sock family is not supported, or the required types + # are not present in the symbols, we can still show some general + # information about the socket that may be helpful. + src_addr = src_port = dst_addr = dst_port = None + state = sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + + return sock, sock_stat, socket_filter + + def _update_socket_filters_info( + self, sock: objects.Pointer, socket_filter: dict + ) -> None: + """Get information from the socket and reuseport filters + + Args: + sock: The kernel sock (sk) struct + socket_filter: A dictionary with information about the socket filter + """ + if sock.has_member("sk_filter") and sock.sk_filter: + sock_filter = sock.sk_filter + socket_filter["filter_type"] = "socket_filter" + self._extract_socket_filter_info(sock_filter, socket_filter) + + if sock.has_member("sk_reuseport_cb") and sock.sk_reuseport_cb: + sock_reuseport_cb = sock.sk_reuseport_cb + socket_filter["filter_type"] = "reuseport_filter" + self._extract_socket_filter_info(sock_reuseport_cb, socket_filter) + + def _extract_socket_filter_info( + self, sock_filter: objects.Pointer, socket_filter: dict + ) -> None: + """Get specific information for each type of filter + + Args: + socket_filter: A dictionary with information about the socket filter + """ + socket_filter["bpf_filter_type"] = "cBPF" + + if not sock_filter.has_member("prog") or not sock_filter.prog: + return None + + bpfprog = sock_filter.prog + + BPF_PROG_TYPE_UNSPEC = 0 # cBPF filter + try: + bpfprog_type = bpfprog.get_type() + if bpfprog_type == BPF_PROG_TYPE_UNSPEC: + return # cBPF filter + except AttributeError: + # kernel < 3.18.140, it's a cBPF filter + return None + + BPF_PROG_TYPE_SOCKET_FILTER = 1 # eBPF filter + if bpfprog_type != BPF_PROG_TYPE_SOCKET_FILTER: + socket_filter["bpf_filter_type"] = f"UNK({bpfprog_type})" + vollog.warning(f"Unexpected BPF type {bpfprog_type} for a socket") + return None + + socket_filter["bpf_filter_type"] = "eBPF" + if not bpfprog.has_member("aux") or not bpfprog.aux: + return # kernel < 3.18.140 + bpfprog_aux = bpfprog.aux + + if bpfprog_aux.has_member("id"): + # `id` member was added to `bpf_prog_aux` in kernels 4.13.16 + socket_filter["bpf_filter_id"] = str(bpfprog_aux.id) + if bpfprog_aux.has_member("name"): + # `name` was added to `bpf_prog_aux` in kernels 4.15.18 + bpfprog_name = utility.array_to_string(bpfprog_aux.name) + if bpfprog_name: + socket_filter["bpf_filter_name"] = bpfprog_name + + def _unix_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_UNIX socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + unix_sock: The kernel's `unix_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + unix_sock = sock.cast("unix_sock") + state = unix_sock.get_state() + src_addr = unix_sock.get_name() + src_port = unix_sock.get_inode() + + if unix_sock.peer: + peer = unix_sock.peer.dereference().cast("unix_sock") + dst_addr = peer.get_name() + dst_port = peer.get_inode() + else: + dst_addr = dst_port = None + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return unix_sock, sock_stat + + def _inet_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_INET/6 socket families + + Args: + sock: Kernel generic `sock` object + + Returns: + inet_sock: The kernel's `inet_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + inet_sock = sock.cast("inet_sock") + src_addr = inet_sock.get_src_addr() + src_port = inet_sock.get_src_port() + dst_addr = inet_sock.get_dst_addr() + dst_port = inet_sock.get_dst_port() + state = inet_sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return inet_sock, sock_stat + + def _netlink_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_NETLINK socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + netlink_sock: The kernel's `netlink_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + netlink_sock = sock.cast("netlink_sock") + + src_addr = None + if netlink_sock.groups: + groups_bitmap = netlink_sock.groups.dereference() + src_addr = f"groups:0x{groups_bitmap:08x}" + + try: + # Kernel >= 3.7.10 + src_port = netlink_sock.get_portid() + except AttributeError: + src_port = NotAvailableValue() + + dst_addr = f"group:0x{netlink_sock.dst_group:08x}" + module = netlink_sock.module + if module and module.name: + module_name_str = utility.array_to_string(module.name) + dst_addr = f"{dst_addr},lkm:{module_name_str}" + try: + dst_port = netlink_sock.get_dst_portid() + except AttributeError: + dst_port = NotAvailableValue() + + state = netlink_sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return netlink_sock, sock_stat + + def _vsock_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_VSOCK socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + vsock_sock: The kernel `vsock_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + vsock_sock = sock.cast("vsock_sock") + src_addr = vsock_sock.local_addr.svm_cid + src_port = vsock_sock.local_addr.svm_port + dst_addr = vsock_sock.remote_addr.svm_cid + dst_port = vsock_sock.remote_addr.svm_port + state = vsock_sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return vsock_sock, sock_stat + + def _packet_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_PACKET socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + packet_sock: The kernel's `packet_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + packet_sock = sock.cast("packet_sock") + ifindex = packet_sock.ifindex + dev_name = self._netdevices.get(ifindex) if ifindex > 0 else "ANY" + + src_addr = dev_name + src_port = dst_addr = dst_port = None + state = packet_sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return packet_sock, sock_stat + + def _xdp_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_XDP socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + xdp_sock: The kernel's `xdp_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + xdp_sock = sock.cast("xdp_sock") + device = xdp_sock.dev + if not device: + return None + + src_addr = utility.array_to_string(device.name) + src_port = dst_addr = dst_port = None + + bpfprog = device.xdp_prog + if not bpfprog: + return None + + if not bpfprog.has_member("aux") or not bpfprog.aux: + return None + + bpfprog_aux = bpfprog.aux + if bpfprog_aux.has_member("id"): + # `id` member was added to `bpf_prog_aux` in kernels 4.13 + bpfprog_id = bpfprog_aux.id + dst_port = f"ebpf_prog_id:{bpfprog_id}" + if bpfprog_aux.has_member("name"): + # `name` was added to `bpf_prog_aux` in kernels 4.15 + bpf_name = utility.array_to_string(bpfprog_aux.name) + if bpf_name: + dst_addr = f"ebpf_prog_name:{bpf_name}" + + xsk_state = xdp_sock.get_state() + state = xsk_state.replace("XSK_", "") + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return xdp_sock, sock_stat + + def _bluetooth_sock( + self, sock: objects.StructType + ) -> Tuple[objects.StructType, Tuple[str, str, str]]: + """Handles the AF_BLUETOOTH socket family + + Args: + sock: Kernel generic `sock` object + + Returns: + bt_sock: The kernel's `bt_sock` object + sock_stat: A tuple with the source and destination (address and port) along with its state string + """ + bt_sock = sock.cast("bt_sock") + + def bt_addr(addr): + return ":".join(reversed(["%02x" % x for x in addr.b])) + + src_addr = src_port = dst_addr = dst_port = None + bt_protocol = bt_sock.get_protocol() + if bt_protocol == "HCI": + if self._vmlinux.has_type("hci_pinfo"): + pinfo = bt_sock.cast("hci_pinfo") + if ( + pinfo.has_member("hdev") + and self._vmlinux.has_type("hci_dev") + and pinfo.hdev.has_member("dev_name") + ): + src_addr = utility.array_to_string(pinfo.hdev.dev_name) + else: + vollog.log( + constants.LOGLEVEL_V, + "Type definition for 'hci_pinfo' is not available in the symbols", + ) + elif bt_protocol == "L2CAP": + if self._vmlinux.has_type("l2cap_pinfo"): + pinfo = bt_sock.cast("l2cap_pinfo") + src_addr = bt_addr(pinfo.chan.src) + dst_addr = bt_addr(pinfo.chan.dst) + src_port = pinfo.chan.sport + dst_port = pinfo.chan.psm + else: + vollog.log( + constants.LOGLEVEL_V, + "Type definition for 'l2cap_pinfo' is not available in the symbols", + ) + elif bt_protocol == "RFCOMM": + if self._vmlinux.has_type("rfcomm_pinfo"): + pinfo = bt_sock.cast("rfcomm_pinfo") + src_addr = bt_addr(pinfo.src) + dst_addr = bt_addr(pinfo.dst) + src_port = pinfo.channel + else: + vollog.log( + constants.LOGLEVEL_V, + "Type definition for 'rfcomm_pinfo' is not available in the symbols", + ) + elif bt_protocol == "SCO": + if self._vmlinux.has_type("sco_pinfo"): + pinfo = bt_sock.cast("sco_pinfo") + src_addr = bt_addr(pinfo.src) + dst_addr = bt_addr(pinfo.dst) + else: + vollog.log( + constants.LOGLEVEL_V, + "Type definition for 'sco_pinfo' is not available in the symbols", + ) + else: + vollog.log( + constants.LOGLEVEL_V, "Unsupported bluetooth protocol '%s'", bt_protocol + ) + + state = bt_sock.get_state() + + sock_stat = src_addr, src_port, dst_addr, dst_port, state + return bt_sock, sock_stat + + +class Sockstat(plugins.PluginInterface): + """Lists all network connections for all processes.""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="SockHandlers", component=SockHandlers, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="lsof", plugin=lsof.Lsof, version=(1, 1, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="unix", + description=("Show UNIX domain Sockets only"), + default=False, + optional=True, + ), + requirements.ListRequirement( + name="pids", + description="Filter results by process IDs. " + "It takes the root PID namespace identifiers.", + element_type=int, + optional=True, + ), + requirements.IntRequirement( + name="netns", + description="Filter results by network namespace. " + "Otherwise, all of them are shown.", + optional=True, + ), + ] + + @classmethod + def list_sockets( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + filter_func: Callable[[int], bool] = lambda _: False, + ): + """Returns every single socket descriptor + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + symbol_table: The name of the kernel module on which to operate + filter_func: A function which takes a task object and returns True if the task should be ignored/filtered + + Yields: + task: Kernel's task object + netns_id: Network namespace ID + fd_num: File descriptor number + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + sock_fields: A tuple with the \*_sock object, the sock stats and the extended info dictionary + """ + vmlinux = context.modules[symbol_table] + + sfop_addr = vmlinux.object_from_symbol("socket_file_ops").vol.offset + dfop_addr = vmlinux.object_from_symbol("sockfs_dentry_operations").vol.offset + + fd_generator = lsof.Lsof.list_fds(context, vmlinux.name, filter_func) + for _pid, _task_comm, task, fd_fields in fd_generator: + fd_num, filp, _full_path = fd_fields + + if filp.f_op not in (sfop_addr, dfop_addr): + continue + + dentry = filp.get_dentry() + if not dentry: + continue + + d_inode = dentry.d_inode + if not d_inode: + continue + + socket_alloc = linux.LinuxUtilities.container_of( + d_inode, "socket_alloc", "vfs_inode", vmlinux + ) + socket = socket_alloc.socket + + if not (socket and socket.sk): + continue + + sock = socket.sk.dereference() + + sock_type = sock.get_type() + family = sock.get_family() + + sock_handler = SockHandlers(vmlinux, task) + sock_fields = sock_handler.process_sock(sock) + if not sock_fields: + continue + + child_sock = sock_fields[0] + protocol = child_sock.get_protocol() + + net = task.nsproxy.net_ns + try: + netns_id = net.get_inode() + except AttributeError: + netns_id = NotAvailableValue() + + yield task, netns_id, fd_num, family, sock_type, protocol, sock_fields + + def _format_fields(self, sock_stat, protocol): + """Prepare the socket fields to be rendered + + Args: + sock_stat: A tuple with the source and destination (address and port) along with its state string + protocol: Protocol string (UDP, TCP, etc) + + Returns: + `sock_stat` and `protocol` formatted. + """ + sock_stat = [ + NotAvailableValue() if field is None else str(field) for field in sock_stat + ] + if protocol is None: + protocol = NotAvailableValue() + + return tuple(sock_stat), protocol + + def _generator(self, pids: List[int], netns_id_arg: int, symbol_table: str): + """Enumerate tasks sockets. Each row represents a kernel socket. + + Args: + pids: List of PIDs to filter. If a empty list or + netns_id_arg: If a network namespace ID is set, it will only show this namespace. + symbol_table: The name of the kernel module on which to operate + + Yields: + netns_id: Network namespace ID + family: Socket family string (AF_UNIX, AF_INET, etc) + sock_type: Socket type string (STREAM, DGRAM, etc) + protocol: Protocol string (UDP, TCP, etc) + source addr: Source address string + source port: Source port string (not all of them are int) + destination addr: Destination address string + destination port: Destination port (not all of them are int) + state: State strings (LISTEN, CONNECTED, etc) + tasks: String with a list of tasks and FDs using a socket. It can also have + extended information such as socket filters, bpf info, etc. + """ + filter_func = lsof.pslist.PsList.create_pid_filter(pids) + socket_generator = self.list_sockets( + self.context, symbol_table, filter_func=filter_func + ) + + for ( + task, + netns_id, + fd_num, + family, + sock_type, + protocol, + sock_fields, + ) in socket_generator: + if netns_id_arg and netns_id_arg != netns_id: + continue + + sock, sock_stat, extended = sock_fields + sock_stat, protocol = self._format_fields(sock_stat, protocol) + + socket_filter_str = ( + ",".join(f"{k}={v}" for k, v in extended.items()) + if extended + else NotAvailableValue() + ) + + fields = ( + netns_id, + task.pid, + fd_num, + format_hints.Hex(sock.vol.offset), + family, + sock_type, + protocol, + *sock_stat, + socket_filter_str, + ) + + yield (0, fields) + + def run(self): + pids = self.config.get("pids") + netns_id = self.config["netns"] + symbol_table = self.config["kernel"] + + tree_grid_args = [ + ("NetNS", int), + ("Pid", int), + ("FD", int), + ("Sock Offset", format_hints.Hex), + ("Family", str), + ("Type", str), + ("Proto", str), + ("Source Addr", str), + ("Source Port", str), + ("Destination Addr", str), + ("Destination Port", str), + ("State", str), + ("Filter", str), + ] + + return TreeGrid(tree_grid_args, self._generator(pids, netns_id, symbol_table)) diff --git a/volatility3/framework/plugins/linux/tty_check.py b/volatility3/framework/plugins/linux/tty_check.py index e1d8339e8..45238ef8c 100644 --- a/volatility3/framework/plugins/linux/tty_check.py +++ b/volatility3/framework/plugins/linux/tty_check.py @@ -24,18 +24,27 @@ class tty_check(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Linux kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'linuxutils', component = linux.LinuxUtilities, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="linuxutils", component=linux.LinuxUtilities, version=(2, 0, 0) + ), ] def _generator(self): - vmlinux = self.context.modules[self.config['kernel']] + vmlinux = self.context.modules[self.config["kernel"]] modules = lsmod.Lsmod.list_modules(self.context, vmlinux.name) - handlers = linux.LinuxUtilities.generate_kernel_handler_info(self.context, vmlinux.name, modules) + handlers = linux.LinuxUtilities.generate_kernel_handler_info( + self.context, vmlinux.name, modules + ) try: tty_drivers = vmlinux.object_from_symbol("tty_drivers").cast("list_head") @@ -49,18 +58,20 @@ class tty_check(plugins.PluginInterface): "This means you are either analyzing an unsupported kernel version or that your symbol table is corrupt." ) - for tty in tty_drivers.to_list(vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers"): - + for tty in tty_drivers.to_list( + vmlinux.symbol_table_name + constants.BANG + "tty_driver", "tty_drivers" + ): try: - ttys = utility.array_of_pointers(tty.ttys.dereference(), - count = tty.num, - subtype = vmlinux.symbol_table_name + constants.BANG + "tty_struct", - context = self.context) + ttys = utility.array_of_pointers( + tty.ttys.dereference(), + count=tty.num, + subtype=vmlinux.symbol_table_name + constants.BANG + "tty_struct", + context=self.context, + ) except exceptions.PagedInvalidAddressException: continue for tty_dev in ttys: - if tty_dev == 0: continue @@ -68,10 +79,19 @@ class tty_check(plugins.PluginInterface): recv_buf = tty_dev.ldisc.ops.receive_buf - module_name, symbol_name = linux.LinuxUtilities.lookup_module_address(vmlinux, handlers, recv_buf) + module_name, symbol_name = linux.LinuxUtilities.lookup_module_address( + vmlinux, handlers, recv_buf + ) yield (0, (name, format_hints.Hex(recv_buf), module_name, symbol_name)) def run(self): - return renderers.TreeGrid([("Name", str), ("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], - self._generator()) + return renderers.TreeGrid( + [ + ("Name", str), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/linux/vmayarascan.py b/volatility3/framework/plugins/linux/vmayarascan.py new file mode 100644 index 000000000..eda0d7dca --- /dev/null +++ b/volatility3/framework/plugins/linux/vmayarascan.py @@ -0,0 +1,113 @@ +# This file is Copyright 2023 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 Iterable, List, Tuple + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins import yarascan +from volatility3.plugins.linux import pslist + + +class VmaYaraScan(interfaces.plugins.PluginInterface): + """Scans all virtual memory areas for tasks using yara.""" + + _required_framework_version = (2, 4, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + # create a list of requirements for vmayarascan + vmayarascan_requirements = [ + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0) + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vmayarascan_requirements + + def _generator(self): + # use yarascan to parse the yara options provided and create the rules + rules = yarascan.YaraScan.process_yara_options(dict(self.config)) + + # filter based on the pid option if provided + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + for task in pslist.PsList.list_tasks( + context=self.context, + vmlinux_module_name=self.config["kernel"], + filter_func=filter_func, + ): + # attempt to create a process layer for each task and skip those + # that cannot (e.g. kernel threads) + proc_layer_name = task.add_process_layer() + if not proc_layer_name: + continue + + # get the proc_layer object from the context + proc_layer = self.context.layers[proc_layer_name] + + # scan the process layer with the yarascanner + for offset, rule_name, name, value in proc_layer.scan( + context=self.context, + scanner=yarascan.YaraScanner(rules=rules), + sections=self.get_vma_maps(task), + ): + yield 0, ( + format_hints.Hex(offset), + task.tgid, + rule_name, + name, + value, + ) + + @staticmethod + def get_vma_maps( + task: interfaces.objects.ObjectInterface, + ) -> Iterable[Tuple[int, int]]: + """Creates a map of start/end addresses for each virtual memory area in a task. + + Args: + task: The task object of which to read the vmas from + + Returns: + An iterable of tuples containing start and end addresses for each descriptor + """ + if task.mm: + for vma in task.mm.get_vma_iter(): + vm_size = vma.vm_end - vma.vm_start + yield (vma.vm_start, vm_size) + + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("PID", int), + ("Rule", str), + ("Component", str), + ("Value", bytes), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/bash.py b/volatility3/framework/plugins/mac/bash.py index e16a0d79f..a52ae616a 100644 --- a/volatility3/framework/plugins/mac/bash.py +++ b/volatility3/framework/plugins/mac/bash.py @@ -25,18 +25,27 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _generator(self, tasks): - darwin = self.context.modules[self.config['kernel']] - is_32bit = not symbols.symbol_table_is_64bit(self.context, darwin.symbol_table_name) + darwin = self.context.modules[self.config["kernel"]] + is_32bit = not symbols.symbol_table_is_64bit( + self.context, darwin.symbol_table_name + ) if is_32bit: pack_format = "I" bash_json_file = "bash32" @@ -44,10 +53,13 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): pack_format = "Q" bash_json_file = "bash64" - bash_table_name = BashIntermedSymbols.create(self.context, self.config_path, "linux", bash_json_file) + bash_table_name = BashIntermedSymbols.create( + self.context, self.config_path, "linux", bash_json_file + ) - ts_offset = self.context.symbol_space.get_type(bash_table_name + constants.BANG + - "hist_entry").relative_child_offset("timestamp") + ts_offset = self.context.symbol_space.get_type( + bash_table_name + constants.BANG + "hist_entry" + ).relative_child_offset("timestamp") for task in tasks: task_name = utility.array_to_string(task.p_comm) @@ -63,49 +75,71 @@ class Bash(plugins.PluginInterface, timeliner.TimeLinerInterface): bang_addrs = [] # find '#' values on the heap - for address in proc_layer.scan(self.context, - scanners.BytesScanner(b"#"), - sections = task.get_process_memory_sections(self.context, - self.config['kernel'], - rw_no_file = True)): + for address in proc_layer.scan( + self.context, + scanners.BytesScanner(b"#"), + sections=task.get_process_memory_sections( + self.context, self.config["kernel"], rw_no_file=True + ), + ): bang_addrs.append(struct.pack(pack_format, address)) history_entries = [] - for address, _ in proc_layer.scan(self.context, - scanners.MultiStringScanner(bang_addrs), - sections = task.get_process_memory_sections(self.context, - self.config['kernel'], - rw_no_file = True)): - hist = self.context.object(bash_table_name + constants.BANG + "hist_entry", - offset = address - ts_offset, - layer_name = proc_layer_name) + for address, _ in proc_layer.scan( + self.context, + scanners.MultiStringScanner(bang_addrs), + sections=task.get_process_memory_sections( + self.context, self.config["kernel"], rw_no_file=True + ), + ): + hist = self.context.object( + bash_table_name + constants.BANG + "hist_entry", + offset=address - ts_offset, + layer_name=proc_layer_name, + ) if hist.is_valid(): history_entries.append(hist) - for hist in sorted(history_entries, key = lambda x: x.get_time_as_integer()): - yield (0, (int(task.p_pid), task_name, hist.get_time_object(), hist.get_command())) + for hist in sorted(history_entries, key=lambda x: x.get_time_as_integer()): + yield ( + 0, + ( + int(task.p_pid), + task_name, + hist.get_time_object(), + hist.get_command(), + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - return renderers.TreeGrid([("PID", int), ("Process", str), ("CommandTime", datetime.datetime), - ("Command", str)], - self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("CommandTime", datetime.datetime), + ("Command", str), + ], + self._generator( + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ), + ) def generate_timeline(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) for row in self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func)): + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ): _depth, row_data = row - description = f"{row_data[0]} ({row_data[1]}): \"{row_data[3]}\"" + description = f'{row_data[0]} ({row_data[1]}): "{row_data[3]}"' yield (description, timeliner.TimeLinerType.CREATED, row_data[2]) diff --git a/volatility3/framework/plugins/mac/check_syscall.py b/volatility3/framework/plugins/mac/check_syscall.py index 9072e76d1..5c22e6463 100644 --- a/volatility3/framework/plugins/mac/check_syscall.py +++ b/volatility3/framework/plugins/mac/check_syscall.py @@ -23,22 +23,31 @@ class Check_syscall(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) - table = kernel.object_from_symbol(symbol_name = "sysent") + table = kernel.object_from_symbol(symbol_name="sysent") - for (i, ent) in enumerate(table): + for i, ent in enumerate(table): try: call_addr = ent.sy_call.dereference().vol.offset except exceptions.InvalidAddressException: @@ -47,13 +56,31 @@ class Check_syscall(plugins.PluginInterface): if not call_addr or call_addr == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, - call_addr, self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, call_addr, self.config["kernel"] + ) - yield (0, (format_hints.Hex(table.vol.offset), "SysCall", i, format_hints.Hex(call_addr), module_name, - symbol_name)) + yield ( + 0, + ( + format_hints.Hex(table.vol.offset), + "SysCall", + i, + format_hints.Hex(call_addr), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int), - ("Handler Address", format_hints.Hex), ("Handler Module", str), - ("Handler Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Table Address", format_hints.Hex), + ("Table Name", str), + ("Index", int), + ("Handler Address", format_hints.Hex), + ("Handler Module", str), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/check_sysctl.py b/volatility3/framework/plugins/mac/check_sysctl.py index fc6ab5c37..4f64eaed8 100644 --- a/volatility3/framework/plugins/mac/check_sysctl.py +++ b/volatility3/framework/plugins/mac/check_sysctl.py @@ -25,10 +25,17 @@ class Check_sysctl(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _parse_global_variable_sysctls(self, kernel, name): @@ -43,7 +50,7 @@ class Check_sysctl(plugins.PluginInterface): var_name = known_sysctls[name] try: - var_array = kernel.object_from_symbol(symbol_name = var_name) + var_array = kernel.object_from_symbol(symbol_name=var_name) except exceptions.SymbolError: var_array = None @@ -52,7 +59,7 @@ class Check_sysctl(plugins.PluginInterface): return var_str - def _process_sysctl_list(self, kernel, sysctl_list, recursive = 0): + def _process_sysctl_list(self, kernel, sysctl_list, recursive=0): if type(sysctl_list) == volatility3.framework.objects.Pointer: sysctl_list = sysctl_list.dereference().cast("sysctl_oid_list") @@ -62,7 +69,7 @@ class Check_sysctl(plugins.PluginInterface): try: sysctl = sysctl.oid_link.sle_next.dereference() except exceptions.InvalidAddressException: - return + return None while sysctl: try: @@ -84,20 +91,22 @@ class Check_sysctl(plugins.PluginInterface): if arg1 == 0 or arg1_ptr == 0: val = self._parse_global_variable_sysctls(kernel, name) - elif ctltype == 'CTLTYPE_NODE': + elif ctltype == "CTLTYPE_NODE": if sysctl.oid_handler == 0: - for info in self._process_sysctl_list(kernel, sysctl.oid_arg1, recursive = 1): + for info in self._process_sysctl_list( + kernel, sysctl.oid_arg1, recursive=1 + ): yield info val = "Node" - elif ctltype in ['CTLTYPE_INT', 'CTLTYPE_QUAD', 'CTLTYPE_OPAQUE']: + elif ctltype in ["CTLTYPE_INT", "CTLTYPE_QUAD", "CTLTYPE_OPAQUE"]: try: val = str(arg1.dereference().cast("int")) except exceptions.InvalidAddressException: val = "-1" - elif ctltype == 'CTLTYPE_STRING': + elif ctltype == "CTLTYPE_STRING": try: val = utility.pointer_to_string(sysctl.oid_arg1, 64) except exceptions.InvalidAddressException: @@ -113,13 +122,15 @@ class Check_sysctl(plugins.PluginInterface): break def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) - sysctl_list = kernel.object_from_symbol(symbol_name = "sysctl__children") + sysctl_list = kernel.object_from_symbol(symbol_name="sysctl__children") for sysctl, name, val in self._process_sysctl_list(kernel, sysctl_list): try: @@ -127,13 +138,33 @@ class Check_sysctl(plugins.PluginInterface): except exceptions.InvalidAddressException: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, check_addr, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, check_addr, self.config["kernel"] + ) - yield (0, (name, sysctl.oid_number, sysctl.get_perms(), format_hints.Hex(check_addr), val, module_name, - symbol_name)) + yield ( + 0, + ( + name, + sysctl.oid_number, + sysctl.get_perms(), + format_hints.Hex(check_addr), + val, + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Name", str), ("Number", int), ("Perms", str), - ("Handler Address", format_hints.Hex), ("Value", str), ("Handler Module", str), - ("Handler Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Name", str), + ("Number", int), + ("Perms", str), + ("Handler Address", format_hints.Hex), + ("Value", str), + ("Handler Module", str), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/check_trap_table.py b/volatility3/framework/plugins/mac/check_trap_table.py index 47d0ed57d..60f237208 100644 --- a/volatility3/framework/plugins/mac/check_trap_table.py +++ b/volatility3/framework/plugins/mac/check_trap_table.py @@ -24,20 +24,29 @@ class Check_trap_table(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) - table = kernel.object_from_symbol(symbol_name = "mach_trap_table") + table = kernel.object_from_symbol(symbol_name="mach_trap_table") for i, ent in enumerate(table): try: @@ -48,13 +57,31 @@ class Check_trap_table(plugins.PluginInterface): if not call_addr or call_addr == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, call_addr, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, call_addr, self.config["kernel"] + ) - yield (0, (format_hints.Hex(table.vol.offset), "TrapTable", i, format_hints.Hex(call_addr), module_name, - symbol_name)) + yield ( + 0, + ( + format_hints.Hex(table.vol.offset), + "TrapTable", + i, + format_hints.Hex(call_addr), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Table Address", format_hints.Hex), ("Table Name", str), ("Index", int), - ("Handler Address", format_hints.Hex), ("Handler Module", str), - ("Handler Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Table Address", format_hints.Hex), + ("Table Name", str), + ("Index", int), + ("Handler Address", format_hints.Hex), + ("Handler Module", str), + ("Handler Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/ifconfig.py b/volatility3/framework/plugins/mac/ifconfig.py index 330c13f07..6634dda2e 100644 --- a/volatility3/framework/plugins/mac/ifconfig.py +++ b/volatility3/framework/plugins/mac/ifconfig.py @@ -16,18 +16,23 @@ class Ifconfig(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] try: - list_head = kernel.object_from_symbol(symbol_name = "ifnet_head") + list_head = kernel.object_from_symbol(symbol_name="ifnet_head") except exceptions.SymbolError: - list_head = kernel.object_from_symbol(symbol_name = "dlil_ifnet_head") + list_head = kernel.object_from_symbol(symbol_name="dlil_ifnet_head") for ifnet in mac.MacUtilities.walk_tailq(list_head, "if_link"): name = utility.pointer_to_string(ifnet.if_name, 32) @@ -46,5 +51,12 @@ class Ifconfig(plugins.PluginInterface): yield (0, (f"{name}{unit}", ip, mac_addr, prom)) def run(self): - return renderers.TreeGrid([("Interface", str), ("IP Address", str), ("Mac Address", str), - ("Promiscuous", bool)], self._generator()) + return renderers.TreeGrid( + [ + ("Interface", str), + ("IP Address", str), + ("Mac Address", str), + ("Promiscuous", bool), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/kauth_listeners.py b/volatility3/framework/plugins/mac/kauth_listeners.py index 7002d88e2..ed43bfb42 100644 --- a/volatility3/framework/plugins/mac/kauth_listeners.py +++ b/volatility3/framework/plugins/mac/kauth_listeners.py @@ -1,4 +1,4 @@ -# This file is opyright 2020 Volatility Foundation and licensed under the Volatility Software License 1.0 +# 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 # @@ -11,34 +11,44 @@ from volatility3.plugins.mac import lsmod, kauth_scopes class Kauth_listeners(interfaces.plugins.PluginInterface): - """ Lists kauth listeners and their status """ + """Lists kauth listeners and their status""" _required_framework_version = (2, 0, 0) @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 1, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'kauth_scopes', - plugin = kauth_scopes.Kauth_scopes, - version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 1, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="kauth_scopes", plugin=kauth_scopes.Kauth_scopes, version=(2, 0, 0) + ), ] def _generator(self): """ Enumerates the listeners for each kauth scope """ - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) - - for scope in kauth_scopes.Kauth_scopes.list_kauth_scopes(self.context, self.config['kernel']): + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) + for scope in kauth_scopes.Kauth_scopes.list_kauth_scopes( + self.context, self.config["kernel"] + ): scope_name = utility.pointer_to_string(scope.ks_identifier, 128) for listener in scope.get_listeners(): @@ -46,12 +56,29 @@ class Kauth_listeners(interfaces.plugins.PluginInterface): if callback == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, callback, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, callback, self.config["kernel"] + ) - yield (0, (scope_name, format_hints.Hex(listener.kll_idata), format_hints.Hex(callback), module_name, - symbol_name)) + yield ( + 0, + ( + scope_name, + format_hints.Hex(listener.kll_idata), + format_hints.Hex(callback), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Name", str), ("IData", format_hints.Hex), ("Callback Address", format_hints.Hex), - ("Module", str), ("Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Name", str), + ("IData", format_hints.Hex), + ("Callback Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/kauth_scopes.py b/volatility3/framework/plugins/mac/kauth_scopes.py index f1a2ad345..afb320a07 100644 --- a/volatility3/framework/plugins/mac/kauth_scopes.py +++ b/volatility3/framework/plugins/mac/kauth_scopes.py @@ -15,7 +15,7 @@ vollog = logging.getLogger(__name__) class Kauth_scopes(interfaces.plugins.PluginInterface): - """ Lists kauth scopes and their status """ + """Lists kauth scopes and their status""" _version = (2, 0, 0) _required_framework_version = (2, 0, 0) @@ -23,18 +23,26 @@ class Kauth_scopes(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 1, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 1, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] @classmethod - def list_kauth_scopes(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_kauth_scopes( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """ Enumerates the registered kauth scopes and yields each object Uses smear-safe enumeration API @@ -48,27 +56,46 @@ class Kauth_scopes(interfaces.plugins.PluginInterface): yield scope def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) - - for scope in self.list_kauth_scopes(self.context, self.config['kernel']): + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) + for scope in self.list_kauth_scopes(self.context, self.config["kernel"]): callback = scope.ks_callback if callback == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, callback, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, callback, self.config["kernel"] + ) identifier = utility.pointer_to_string(scope.ks_identifier, 128) - yield (0, (identifier, format_hints.Hex(scope.ks_idata), len([l for l in scope.get_listeners()]), - format_hints.Hex(callback), module_name, symbol_name)) + yield ( + 0, + ( + identifier, + format_hints.Hex(scope.ks_idata), + len([l for l in scope.get_listeners()]), + format_hints.Hex(callback), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Name", str), ("IData", format_hints.Hex), ("Listeners", int), - ("Callback Address", format_hints.Hex), ("Module", str), ("Symbol", str)], - self._generator()) + return renderers.TreeGrid( + [ + ("Name", str), + ("IData", format_hints.Hex), + ("Listeners", int), + ("Callback Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/kevents.py b/volatility3/framework/plugins/mac/kevents.py index 6f82c75cd..2a8692b77 100644 --- a/volatility3/framework/plugins/mac/kevents.py +++ b/volatility3/framework/plugins/mac/kevents.py @@ -12,7 +12,7 @@ from volatility3.plugins.mac import pslist class Kevents(interfaces.plugins.PluginInterface): - """ Lists event handlers registered by processes """ + """Lists event handlers registered by processes""" _required_framework_version = (2, 0, 0) _version = (1, 0, 0) @@ -28,34 +28,61 @@ class Kevents(interfaces.plugins.PluginInterface): 8: "EVFILT_MACHPORT", 9: "EVFILT_FS", 10: "EVFILT_USER", - 12: "EVFILT_VM" + 12: "EVFILT_VM", } - vnode_filters = [("NOTE_DELETE", 1), ("NOTE_WRITE", 2), ("NOTE_EXTEND", 4), ("NOTE_ATTRIB", 8), ("NOTE_LINK", 0x10), - ("NOTE_RENAME", 0x20), ("NOTE_REVOKE", 0x40)] + vnode_filters = [ + ("NOTE_DELETE", 1), + ("NOTE_WRITE", 2), + ("NOTE_EXTEND", 4), + ("NOTE_ATTRIB", 8), + ("NOTE_LINK", 0x10), + ("NOTE_RENAME", 0x20), + ("NOTE_REVOKE", 0x40), + ] - proc_filters = [("NOTE_EXIT", 0x80000000), ("NOTE_EXITSTATUS", 0x04000000), ("NOTE_FORK", 0x40000000), - ("NOTE_EXEC", 0x20000000), ("NOTE_SIGNAL", 0x08000000), ("NOTE_REAP", 0x10000000)] + proc_filters = [ + ("NOTE_EXIT", 0x80000000), + ("NOTE_EXITSTATUS", 0x04000000), + ("NOTE_FORK", 0x40000000), + ("NOTE_EXEC", 0x20000000), + ("NOTE_SIGNAL", 0x08000000), + ("NOTE_REAP", 0x10000000), + ] - timer_filters = [("NOTE_SECONDS", 1), ("NOTE_USECONDS", 2), ("NOTE_NSECONDS", 4), ("NOTE_ABSOLUTE", 8)] + timer_filters = [ + ("NOTE_SECONDS", 1), + ("NOTE_USECONDS", 2), + ("NOTE_NSECONDS", 4), + ("NOTE_ABSOLUTE", 8), + ] all_filters = { 4: vnode_filters, # EVFILT_VNODE 5: proc_filters, # EVFILT_PROC - 7: timer_filters # EVFILT_TIMER + 7: timer_filters, # EVFILT_TIMER } @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 2, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 2, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _parse_flags(self, filter_index, filter_flags): @@ -74,20 +101,22 @@ class Kevents(interfaces.plugins.PluginInterface): @classmethod def _walk_klist_array(cls, kernel, fdp, array_pointer_member, array_size_member): """ - Convience wrapper for walking an array of lists of kernel events + Convenience wrapper for walking an array of lists of kernel events Handles invalid address references """ try: klist_array_pointer = getattr(fdp, array_pointer_member) array_size = getattr(fdp, array_size_member) - klist_array = kernel.object(object_type = "array", - offset = klist_array_pointer, - count = array_size + 1, - subtype = kernel.get_type("klist")) + klist_array = kernel.object( + object_type="array", + offset=klist_array_pointer, + count=array_size + 1, + subtype=kernel.get_type("klist"), + ) except exceptions.InvalidAddressException: - return + return None for klist in klist_array: for kn in mac.MacUtilities.walk_slist(klist, "kn_link"): @@ -111,19 +140,24 @@ class Kevents(interfaces.plugins.PluginInterface): try: p_klist = task.p_klist except exceptions.InvalidAddressException: - return + return None for kn in mac.MacUtilities.walk_slist(p_klist, "kn_link"): yield kn @classmethod - def list_kernel_events(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[Tuple[interfaces.objects.ObjectInterface, - interfaces.objects.ObjectInterface, - interfaces.objects.ObjectInterface]]: + def list_kernel_events( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[ + Tuple[ + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + ] + ]: """ Returns the kernel event filters registered @@ -145,12 +179,11 @@ class Kevents(interfaces.plugins.PluginInterface): yield task_name, pid, kn def _generator(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - - for task_name, pid, kn in self.list_kernel_events(self.context, - self.config['kernel'], - filter_func = filter_func): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + for task_name, pid, kn in self.list_kernel_events( + self.context, self.config["kernel"], filter_func=filter_func + ): filter_index = kn.kn_kevent.filter * -1 if filter_index in self.event_types: filter_name = self.event_types[filter_index] @@ -167,5 +200,13 @@ class Kevents(interfaces.plugins.PluginInterface): yield (0, (pid, task_name, ident, filter_name, context)) def run(self): - return renderers.TreeGrid([("PID", int), ("Process", str), ("Ident", int), ("Filter", str), ("Context", str)], - self._generator()) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Ident", int), + ("Filter", str), + ("Context", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/list_files.py b/volatility3/framework/plugins/mac/list_files.py index 8bae986b7..c18b0b7a2 100644 --- a/volatility3/framework/plugins/mac/list_files.py +++ b/volatility3/framework/plugins/mac/list_files.py @@ -23,9 +23,14 @@ class List_Files(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'mount', plugin = mount.Mount, version = (2, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="mount", plugin=mount.Mount, version=(2, 0, 0) + ), ] @classmethod @@ -50,8 +55,9 @@ class List_Files(plugins.PluginInterface): except exceptions.InvalidAddressException: return None - if parent and not context.layers[vnode.vol.native_layer_name].is_valid(parent.vol.offset, - parent.vol.size): + if parent and not context.layers[vnode.vol.native_layer_name].is_valid( + parent.vol.offset, parent.vol.size + ): return None return parent @@ -65,8 +71,9 @@ class List_Files(plugins.PluginInterface): and holds its name, parent address, and object """ - if not context.layers[vnode.vol.native_layer_name].is_valid(vnode.vol.offset, - vnode.vol.size): + if not context.layers[vnode.vol.native_layer_name].is_valid( + vnode.vol.offset, vnode.vol.size + ): return False key = vnode.vol.offset @@ -104,7 +111,7 @@ class List_Files(plugins.PluginInterface): if not cls._add_vnode(context, vnode, loop_vnodes): break - + added = True parent = cls._get_parent(context, vnode) @@ -127,11 +134,9 @@ class List_Files(plugins.PluginInterface): cls._walk_vnode(context, vnode, loop_vnodes) @classmethod - def _walk_mounts(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: - + def _walk_mounts( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> Iterable[interfaces.objects.ObjectInterface]: loop_vnodes = {} # iterate each vnode source from each mount @@ -177,11 +182,9 @@ class List_Files(plugins.PluginInterface): return path @classmethod - def list_files(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: - + def list_files( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ) -> Iterable[interfaces.objects.ObjectInterface]: vnodes = cls._walk_mounts(context, kernel_module_name) for voff, (vnode_name, parent_offset, vnode) in vnodes.items(): @@ -190,9 +193,10 @@ class List_Files(plugins.PluginInterface): yield vnode, full_path def _generator(self): - for vnode, full_path in self.list_files(self.context, self.config['kernel']): - + for vnode, full_path in self.list_files(self.context, self.config["kernel"]): yield (0, (format_hints.Hex(vnode.vol.offset), full_path)) def run(self): - return renderers.TreeGrid([("Address", format_hints.Hex), ("File Path", str)], self._generator()) + return renderers.TreeGrid( + [("Address", format_hints.Hex), ("File Path", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/mac/lsmod.py b/volatility3/framework/plugins/mac/lsmod.py index 095fbc663..c6f57f889 100644 --- a/volatility3/framework/plugins/mac/lsmod.py +++ b/volatility3/framework/plugins/mac/lsmod.py @@ -22,12 +22,17 @@ class Lsmod(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), ] @classmethod - def list_modules(cls, context: interfaces.context.ContextInterface, darwin_module_name: str): + def list_modules( + cls, context: interfaces.context.ContextInterface, darwin_module_name: str + ): """Lists all the modules in the primary layer. Args: @@ -41,26 +46,23 @@ class Lsmod(plugins.PluginInterface): kernel = context.modules[darwin_module_name] kernel_layer = context.layers[kernel.layer_name] - kmod_ptr = kernel.object_from_symbol(symbol_name = "kmod") + kmod_ptr = kernel.object_from_symbol(symbol_name="kmod") try: kmod = kmod_ptr.dereference().cast("kmod_info") except exceptions.InvalidAddressException: - return [] + return # Generation finished yield kmod try: kmod = kmod.next except exceptions.InvalidAddressException: - return [] + return # Generation finished seen: Set = set() - while kmod != 0 and \ - kmod not in seen and \ - len(seen) < 1024: - + while kmod != 0 and kmod not in seen and len(seen) < 1024: kmod_obj = kmod.dereference() if not kernel_layer.is_valid(kmod_obj.vol.offset, kmod_obj.vol.size): @@ -73,15 +75,18 @@ class Lsmod(plugins.PluginInterface): try: kmod = kmod.next except exceptions.InvalidAddressException: - return + return None + return # Generation finished def _generator(self): - for module in self.list_modules(self.context, self.config['kernel']): - + for module in self.list_modules(self.context, self.config["kernel"]): mod_name = utility.array_to_string(module.name) mod_size = module.size yield 0, (format_hints.Hex(module.vol.offset), mod_name, mod_size) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Name", str), ("Size", int)], self._generator()) + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Name", str), ("Size", int)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/lsof.py b/volatility3/framework/plugins/mac/lsof.py index c3941ec27..6832b837f 100644 --- a/volatility3/framework/plugins/mac/lsof.py +++ b/volatility3/framework/plugins/mac/lsof.py @@ -21,33 +21,45 @@ class Lsof(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _generator(self, tasks): - darwin = self.context.modules[self.config['kernel']] + darwin = self.context.modules[self.config["kernel"]] for task in tasks: pid = task.p_pid - for _, filepath, fd in mac.MacUtilities.files_descriptors_for_process(self.context, - darwin.symbol_table_name, - task): + for _, filepath, fd in mac.MacUtilities.files_descriptors_for_process( + self.context, darwin.symbol_table_name, task + ): if filepath and len(filepath) > 0: yield (0, (pid, fd, filepath)) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - return renderers.TreeGrid([("PID", int), ("File Descriptor", int), ("File Path", str)], - self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [("PID", int), ("File Descriptor", int), ("File Path", str)], + self._generator( + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ), + ) diff --git a/volatility3/framework/plugins/mac/malfind.py b/volatility3/framework/plugins/mac/malfind.py index 7a42a0c5f..3094ada85 100644 --- a/volatility3/framework/plugins/mac/malfind.py +++ b/volatility3/framework/plugins/mac/malfind.py @@ -18,13 +18,20 @@ class Malfind(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _list_injections(self, task): @@ -33,18 +40,21 @@ class Malfind(interfaces.plugins.PluginInterface): proc_layer_name = task.add_process_layer() if proc_layer_name is None: - return + return None proc_layer = self.context.layers[proc_layer_name] for vma in task.get_map_iter(): - if not vma.is_suspicious(self.context, self.context.modules[self.config['kernel']].symbol_table_name): - data = proc_layer.read(vma.links.start, 64, pad = True) + if not vma.is_suspicious( + self.context, + self.context.modules[self.config["kernel"]].symbol_table_name, + ): + data = proc_layer.read(vma.links.start, 64, pad=True) yield vma, data def _generator(self, tasks): # determine if we're on a 32 or 64 bit kernel - if self.context.modules[self.config['kernel']].get_type("pointer").size == 4: + if self.context.modules[self.config["kernel"]].get_type("pointer").size == 4: is_32bit_arch = True else: is_32bit_arch = False @@ -58,19 +68,40 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly(data, vma.links.start, architecture) + disasm = interfaces.renderers.Disassembly( + data, vma.links.start, architecture + ) - yield (0, (task.p_pid, process_name, format_hints.Hex(vma.links.start), format_hints.Hex(vma.links.end), - vma.get_perms(), format_hints.HexBytes(data), disasm)) + yield ( + 0, + ( + task.p_pid, + process_name, + format_hints.Hex(vma.links.start), + format_hints.Hex(vma.links.end), + vma.get_perms(), + format_hints.HexBytes(data), + disasm, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex), - ("End", format_hints.Hex), ("Protection", str), ("Hexdump", format_hints.HexBytes), - ("Disasm", interfaces.renderers.Disassembly)], - self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Protection", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", interfaces.renderers.Disassembly), + ], + self._generator( + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ), + ) diff --git a/volatility3/framework/plugins/mac/mount.py b/volatility3/framework/plugins/mac/mount.py index ba3ab83c8..ff654e1a7 100644 --- a/volatility3/framework/plugins/mac/mount.py +++ b/volatility3/framework/plugins/mac/mount.py @@ -21,13 +21,20 @@ class Mount(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), ] @classmethod - def list_mounts(cls, context: interfaces.context.ContextInterface, kernel_module_name: str): + def list_mounts( + cls, context: interfaces.context.ContextInterface, kernel_module_name: str + ): """Lists all the mount structures in the primary layer. Args: @@ -40,13 +47,13 @@ class Mount(plugins.PluginInterface): """ kernel = context.modules[kernel_module_name] - list_head = kernel.object_from_symbol(symbol_name = "mountlist") + list_head = kernel.object_from_symbol(symbol_name="mountlist") for mount in mac.MacUtilities.walk_tailq(list_head, "mnt_list"): yield mount def _generator(self): - for mount in self.list_mounts(self.context, self.config['kernel']): + for mount in self.list_mounts(self.context, self.config["kernel"]): vfs = mount.mnt_vfsstat device_name = utility.array_to_string(vfs.f_mntonname) mount_point = utility.array_to_string(vfs.f_mntfromname) @@ -55,4 +62,6 @@ class Mount(plugins.PluginInterface): yield 0, (device_name, mount_point, mount_type) def run(self): - return renderers.TreeGrid([("Device", str), ("Mount Point", str), ("Type", str)], self._generator()) + return renderers.TreeGrid( + [("Device", str), ("Mount Point", str), ("Type", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/mac/netstat.py b/volatility3/framework/plugins/mac/netstat.py index e231b8082..76bba25f6 100644 --- a/volatility3/framework/plugins/mac/netstat.py +++ b/volatility3/framework/plugins/mac/netstat.py @@ -24,24 +24,38 @@ class Netstat(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] @classmethod - def list_sockets(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[Tuple[interfaces.objects.ObjectInterface, - interfaces.objects.ObjectInterface, - interfaces.objects.ObjectInterface]]: + def list_sockets( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[ + Tuple[ + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + ] + ]: """ Returns the open socket descriptors of a process @@ -54,18 +68,18 @@ class Netstat(plugins.PluginInterface): # This is hardcoded, since a change in the default method would change the expected results list_tasks = pslist.PsList.get_list_tasks(pslist.PsList.pslist_methods[0]) for task in list_tasks(context, kernel_module_name, filter_func): - task_name = utility.array_to_string(task.p_comm) pid = task.p_pid - for filp, _, _ in mac.MacUtilities.files_descriptors_for_process(context, context.modules[ - kernel_module_name].symbol_table_name, task): + for filp, _, _ in mac.MacUtilities.files_descriptors_for_process( + context, context.modules[kernel_module_name].symbol_table_name, task + ): try: ftype = filp.f_fglob.get_fg_type() except exceptions.InvalidAddressException: continue - if ftype != 'SOCKET': + if ftype != "SOCKET": continue try: @@ -73,19 +87,19 @@ class Netstat(plugins.PluginInterface): except exceptions.InvalidAddressException: continue - if not context.layers[task.vol.native_layer_name].is_valid(socket.vol.offset, - socket.vol.size): + if not context.layers[task.vol.native_layer_name].is_valid( + socket.vol.offset, socket.vol.size + ): continue yield task_name, pid, socket def _generator(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - - for task_name, pid, socket in self.list_sockets(self.context, - self.config['kernel'], - filter_func = filter_func): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + for task_name, pid, socket in self.list_sockets( + self.context, self.config["kernel"], filter_func=filter_func + ): family = socket.get_family() if family == 1: @@ -95,8 +109,19 @@ class Netstat(plugins.PluginInterface): except exceptions.InvalidAddressException: continue - yield (0, (format_hints.Hex(socket.vol.offset), "UNIX", path, 0, "", 0, "", - f"{task_name}/{pid:d}")) + yield ( + 0, + ( + format_hints.Hex(socket.vol.offset), + "UNIX", + path, + 0, + "", + 0, + "", + f"{task_name}/{pid:d}", + ), + ) elif family in [2, 30]: state = socket.get_state() @@ -107,10 +132,31 @@ class Netstat(plugins.PluginInterface): if vals: (lip, lport, rip, rport) = vals - yield (0, (format_hints.Hex(socket.vol.offset), proto, lip, lport, rip, rport, state, - f"{task_name}/{pid:d}")) + yield ( + 0, + ( + format_hints.Hex(socket.vol.offset), + proto, + lip, + lport, + rip, + rport, + state, + f"{task_name}/{pid:d}", + ), + ) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Proto", str), ("Local IP", str), ("Local Port", int), - ("Remote IP", str), ("Remote Port", int), ("State", str), ("Process", str)], - self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Proto", str), + ("Local IP", str), + ("Local Port", int), + ("Remote IP", str), + ("Remote Port", int), + ("State", str), + ("Process", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/proc_maps.py b/volatility3/framework/plugins/mac/proc_maps.py index 70c9684cd..781b3ed66 100644 --- a/volatility3/framework/plugins/mac/proc_maps.py +++ b/volatility3/framework/plugins/mac/proc_maps.py @@ -17,13 +17,20 @@ class Maps(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] def _generator(self, tasks): @@ -32,20 +39,41 @@ class Maps(interfaces.plugins.PluginInterface): process_pid = task.p_pid for vma in task.get_map_iter(): - path = vma.get_path(self.context, self.context.modules[self.config['kernel']].symbol_table_name) + path = vma.get_path( + self.context, + self.context.modules[self.config["kernel"]].symbol_table_name, + ) if path == "": path = vma.get_special_path() - yield (0, (process_pid, process_name, format_hints.Hex(vma.links.start), - format_hints.Hex(vma.links.end), vma.get_perms(), path)) + yield ( + 0, + ( + process_pid, + process_name, + format_hints.Hex(vma.links.start), + format_hints.Hex(vma.links.end), + vma.get_perms(), + path, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Start", format_hints.Hex), - ("End", format_hints.Hex), ("Protection", str), ("Map Name", str)], - self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Protection", str), + ("Map Name", str), + ], + self._generator( + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ), + ) diff --git a/volatility3/framework/plugins/mac/psaux.py b/volatility3/framework/plugins/mac/psaux.py index e3fcdc0bb..28c238263 100644 --- a/volatility3/framework/plugins/mac/psaux.py +++ b/volatility3/framework/plugins/mac/psaux.py @@ -19,16 +19,25 @@ class Psaux(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] - def _generator(self, tasks: Iterator[Any]) -> Generator[Tuple[int, Tuple[int, str, int, str]], None, None]: + def _generator( + self, tasks: Iterator[Any] + ) -> Generator[Tuple[int, Tuple[int, str, int, str]], None, None]: for task in tasks: proc_layer_name = task.add_process_layer() if proc_layer_name is None: @@ -38,7 +47,11 @@ class Psaux(plugins.PluginInterface): argsstart = task.user_stack - task.p_argslen - if not proc_layer.is_valid(argsstart) or task.p_argslen == 0 or task.p_argc == 0: + if ( + not proc_layer.is_valid(argsstart) + or task.p_argslen == 0 + or task.p_argc == 0 + ): continue # Add one because the first two are usually duplicates @@ -58,7 +71,7 @@ class Psaux(plugins.PluginInterface): except exceptions.InvalidAddressException: break - idx = arg.find(b'\x00') + idx = arg.find(b"\x00") if idx != -1: arg = arg[:idx] @@ -85,16 +98,19 @@ class Psaux(plugins.PluginInterface): argc = argc - 1 - args_str = " ".join([s.decode("utf-8", errors = 'replace') for s in args]) + args_str = " ".join([s.decode("utf-8", errors="replace") for s in args]) yield (0, (task.p_pid, task_name, task.p_argc, args_str)) def run(self) -> renderers.TreeGrid: - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Argc", int), ("Arguments", str)], - self._generator( - list_tasks(self.context, - self.config['kernel'], - filter_func = filter_func))) + return renderers.TreeGrid( + [("PID", int), ("Process", str), ("Argc", int), ("Arguments", str)], + self._generator( + list_tasks(self.context, self.config["kernel"], filter_func=filter_func) + ), + ) diff --git a/volatility3/framework/plugins/mac/pslist.py b/volatility3/framework/plugins/mac/pslist.py index e92609b3a..9b570f3f9 100644 --- a/volatility3/framework/plugins/mac/pslist.py +++ b/volatility3/framework/plugins/mac/pslist.py @@ -2,12 +2,14 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # +import datetime import logging -from typing import Callable, Iterable, List, Dict +from typing import Callable, Dict, Iterable, List -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import mac vollog = logging.getLogger(__name__) @@ -18,30 +20,39 @@ class PsList(interfaces.plugins.PluginInterface): _required_framework_version = (2, 0, 0) _version = (3, 0, 0) - pslist_methods = ['tasks', 'allproc', 'process_group', 'sessions', 'pid_hash_table'] + pslist_methods = ["tasks", "allproc", "process_group", "sessions", "pid_hash_table"] @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 1, 0)), - requirements.ChoiceRequirement(name = 'pslist_method', - description = 'Method to determine for processes', - choices = cls.pslist_methods, - default = cls.pslist_methods[0], - optional = True), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 1, 0) + ), + requirements.ChoiceRequirement( + name="pslist_method", + description="Method to determine for processes", + choices=cls.pslist_methods, + default=cls.pslist_methods[0], + optional=True, + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), ] @classmethod - def get_list_tasks( - cls, method: str - ) -> Callable[[interfaces.context.ContextInterface, str, Callable[[int], bool]], - Iterable[interfaces.objects.ObjectInterface]]: + def get_list_tasks(cls, method: str) -> Callable[ + [interfaces.context.ContextInterface, str, Callable[[int], bool]], + Iterable[interfaces.objects.ObjectInterface], + ]: """Returns the list_tasks method based on the selector Args: @@ -49,20 +60,20 @@ class PsList(interfaces.plugins.PluginInterface): Returns: list_tasks method for listing tasks - """ + """ # Ensure method is one of the suitable choices if method not in cls.pslist_methods: method = cls.pslist_methods[0] - if method == 'allproc': + if method == "allproc": list_tasks = cls.list_tasks_allproc - elif method == 'tasks': + elif method == "tasks": list_tasks = cls.list_tasks_tasks - elif method == 'process_group': + elif method == "process_group": list_tasks = cls.list_tasks_process_group - elif method == 'sessions': + elif method == "sessions": list_tasks = cls.list_tasks_sessions - elif method == 'pid_hash_table': + elif method == "pid_hash_table": list_tasks = cls.list_tasks_pid_hash_table else: raise ValueError("Impossible method choice chosen") @@ -72,7 +83,6 @@ class PsList(interfaces.plugins.PluginInterface): @classmethod def create_pid_filter(cls, pid_list: List[int] = None) -> Callable[[int], bool]: - filter_func = lambda _: False # FIXME: mypy #4973 or #2608 pid_list = pid_list or [] @@ -86,22 +96,37 @@ class PsList(interfaces.plugins.PluginInterface): return filter_func def _generator(self): - list_tasks = self.get_list_tasks(self.config.get('pslist_method', self.pslist_methods[0])) + list_tasks = self.get_list_tasks( + self.config.get("pslist_method", self.pslist_methods[0]) + ) - for task in list_tasks(self.context, - self.config['kernel'], - filter_func = self.create_pid_filter(self.config.get('pid', None))): - pid = task.p_pid - ppid = task.p_ppid + for task in list_tasks( + self.context, + self.config["kernel"], + filter_func=self.create_pid_filter(self.config.get("pid", None)), + ): + offset = format_hints.Hex(task.vol.offset) name = utility.array_to_string(task.p_comm) - yield (0, (pid, ppid, name)) + pid = task.p_pid + uid = task.p_uid + gid = task.p_gid + start_time_seconds = task.p_start.tv_sec + start_time_microseconds = task.p_start.tv_usec + start_time = datetime.datetime.fromtimestamp( + start_time_seconds + start_time_microseconds / 1e6 + ) + + ppid = task.p_ppid + + yield (0, (offset, name, pid, uid, gid, start_time, ppid)) @classmethod - def list_tasks_allproc(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_tasks_allproc( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the processes in the primary layer based on the allproc method Args: @@ -117,17 +142,22 @@ class PsList(interfaces.plugins.PluginInterface): kernel_layer = context.layers[kernel.layer_name] - proc = kernel.object_from_symbol(symbol_name = "allproc").lh_first + proc = kernel.object_from_symbol(symbol_name="allproc").lh_first seen: Dict[int, int] = {} while proc is not None and proc.vol.offset != 0: if proc.vol.offset in seen: - vollog.log(logging.INFO, "Recursive process list detected (a result of non-atomic acquisition).") + vollog.log( + logging.INFO, + "Recursive process list detected (a result of non-atomic acquisition).", + ) break else: seen[proc.vol.offset] = 1 - if kernel_layer.is_valid(proc.vol.offset, proc.vol.size) and not filter_func(proc): + if kernel_layer.is_valid( + proc.vol.offset, proc.vol.size + ) and not filter_func(proc): yield proc try: @@ -136,11 +166,12 @@ class PsList(interfaces.plugins.PluginInterface): break @classmethod - def list_tasks_tasks(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_tasks_tasks( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer based on the tasks queue Args: @@ -155,12 +186,15 @@ class PsList(interfaces.plugins.PluginInterface): kernel_layer = context.layers[kernel.layer_name] - queue_entry = kernel.object_from_symbol(symbol_name = "tasks") + queue_entry = kernel.object_from_symbol(symbol_name="tasks") seen: Dict[int, int] = {} for task in queue_entry.walk_list(queue_entry, "tasks", "task"): if task.vol.offset in seen: - vollog.log(logging.INFO, "Recursive process list detected (a result of non-atomic acquisition).") + vollog.log( + logging.INFO, + "Recursive process list detected (a result of non-atomic acquisition).", + ) break else: seen[task.vol.offset] = 1 @@ -170,15 +204,18 @@ class PsList(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: continue - if kernel_layer.is_valid(proc.vol.offset, proc.vol.size) and not filter_func(proc): + if kernel_layer.is_valid( + proc.vol.offset, proc.vol.size + ) and not filter_func(proc): yield proc @classmethod - def list_tasks_sessions(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_tasks_sessions( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer using sessions Args: @@ -191,14 +228,16 @@ class PsList(interfaces.plugins.PluginInterface): """ kernel = context.modules[kernel_module_name] - table_size = kernel.object_from_symbol(symbol_name = "sesshash") + table_size = kernel.object_from_symbol(symbol_name="sesshash") - sesshashtbl = kernel.object_from_symbol(symbol_name = "sesshashtbl") + sesshashtbl = kernel.object_from_symbol(symbol_name="sesshashtbl") - proc_array = kernel.object(object_type = "array", - offset = sesshashtbl, - count = table_size + 1, - subtype = kernel.get_type("sesshashhead")) + proc_array = kernel.object( + object_type="array", + offset=sesshashtbl, + count=table_size + 1, + subtype=kernel.get_type("sesshashhead"), + ) for proc_list in proc_array: for proc in mac.MacUtilities.walk_list_head(proc_list, "s_hash"): @@ -206,11 +245,12 @@ class PsList(interfaces.plugins.PluginInterface): yield proc.s_leader @classmethod - def list_tasks_process_group(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_tasks_process_group( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer using process groups Args: @@ -223,27 +263,32 @@ class PsList(interfaces.plugins.PluginInterface): """ kernel = context.modules[kernel_module_name] - table_size = kernel.object_from_symbol(symbol_name = "pgrphash") + table_size = kernel.object_from_symbol(symbol_name="pgrphash") - pgrphashtbl = kernel.object_from_symbol(symbol_name = "pgrphashtbl") + pgrphashtbl = kernel.object_from_symbol(symbol_name="pgrphashtbl") - proc_array = kernel.object(object_type = "array", - offset = pgrphashtbl, - count = table_size + 1, - subtype = kernel.get_type("pgrphashhead")) + proc_array = kernel.object( + object_type="array", + offset=pgrphashtbl, + count=table_size + 1, + subtype=kernel.get_type("pgrphashhead"), + ) for proc_list in proc_array: for pgrp in mac.MacUtilities.walk_list_head(proc_list, "pg_hash"): - for proc in mac.MacUtilities.walk_list_head(pgrp.pg_members, "p_pglist"): + for proc in mac.MacUtilities.walk_list_head( + pgrp.pg_members, "p_pglist" + ): if not filter_func(proc): yield proc @classmethod - def list_tasks_pid_hash_table(cls, - context: interfaces.context.ContextInterface, - kernel_module_name: str, - filter_func: Callable[[int], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_tasks_pid_hash_table( + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + filter_func: Callable[[int], bool] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the tasks in the primary layer using the pid hash table Args: @@ -257,14 +302,16 @@ class PsList(interfaces.plugins.PluginInterface): kernel = context.modules[kernel_module_name] - table_size = kernel.object_from_symbol(symbol_name = "pidhash") + table_size = kernel.object_from_symbol(symbol_name="pidhash") - pidhashtbl = kernel.object_from_symbol(symbol_name = "pidhashtbl") + pidhashtbl = kernel.object_from_symbol(symbol_name="pidhashtbl") - proc_array = kernel.object(object_type = "array", - offset = pidhashtbl, - count = table_size + 1, - subtype = kernel.get_type("pidhashhead")) + proc_array = kernel.object( + object_type="array", + offset=pidhashtbl, + count=table_size + 1, + subtype=kernel.get_type("pidhashhead"), + ) for proc_list in proc_array: for proc in mac.MacUtilities.walk_list_head(proc_list, "p_hash"): @@ -272,4 +319,15 @@ class PsList(interfaces.plugins.PluginInterface): yield proc def run(self): - return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str)], self._generator()) + return renderers.TreeGrid( + [ + ("OFFSET", format_hints.Hex), + ("NAME", str), + ("PID", int), + ("UID", int), + ("GID", int), + ("Start Time", datetime.datetime), + ("PPID", int), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/pstree.py b/volatility3/framework/plugins/mac/pstree.py index d7fb0eab4..e62d5eb72 100644 --- a/volatility3/framework/plugins/mac/pstree.py +++ b/volatility3/framework/plugins/mac/pstree.py @@ -24,9 +24,14 @@ class PsTree(plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (3, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(3, 0, 0) + ), ] def _find_level(self, pid): @@ -35,7 +40,12 @@ class PsTree(plugins.PluginInterface): seen.add(pid) level = 0 proc = self._processes.get(pid, None) - while proc is not None and proc.vol.offset != 0 and proc.p_ppid != 0 and proc.p_ppid not in seen: + while ( + proc is not None + and proc.vol.offset != 0 + and proc.p_ppid != 0 + and proc.p_ppid not in seen + ): ppid = int(proc.p_ppid) child_list = self._children.get(ppid, set([])) child_list.add(proc.p_pid) @@ -46,9 +56,11 @@ class PsTree(plugins.PluginInterface): def _generator(self): """Generates the tree list of processes""" - list_tasks = pslist.PsList.get_list_tasks(self.config.get('pslist_method', pslist.PsList.pslist_methods[0])) + list_tasks = pslist.PsList.get_list_tasks( + self.config.get("pslist_method", pslist.PsList.pslist_methods[0]) + ) - for proc in list_tasks(self.context, self.config['kernel']): + for proc in list_tasks(self.context, self.config["kernel"]): self._processes[proc.p_pid] = proc # Build the child/level maps @@ -68,4 +80,6 @@ class PsTree(plugins.PluginInterface): yield from yield_processes(pid) def run(self): - return renderers.TreeGrid([("PID", int), ("PPID", int), ("COMM", str)], self._generator()) + return renderers.TreeGrid( + [("PID", int), ("PPID", int), ("COMM", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/mac/socket_filters.py b/volatility3/framework/plugins/mac/socket_filters.py index a6e9d11fd..49e77163e 100644 --- a/volatility3/framework/plugins/mac/socket_filters.py +++ b/volatility3/framework/plugins/mac/socket_filters.py @@ -24,31 +24,54 @@ class Socket_filters(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) members_to_check = [ - "sf_unregistered", "sf_attach", "sf_detach", "sf_notify", "sf_getpeername", "sf_getsockname", "sf_data_in", - "sf_data_out", "sf_connect_in", "sf_connect_out", "sf_bind", "sf_setoption", "sf_getoption", "sf_listen", - "sf_ioctl" + "sf_unregistered", + "sf_attach", + "sf_detach", + "sf_notify", + "sf_getpeername", + "sf_getsockname", + "sf_data_in", + "sf_data_out", + "sf_connect_in", + "sf_connect_out", + "sf_bind", + "sf_setoption", + "sf_getoption", + "sf_listen", + "sf_ioctl", ] - filter_list = kernel.object_from_symbol(symbol_name = "sock_filter_head") + filter_list = kernel.object_from_symbol(symbol_name="sock_filter_head") - for filter_container in mac.MacUtilities.walk_tailq(filter_list, "sf_global_next"): + for filter_container in mac.MacUtilities.walk_tailq( + filter_list, "sf_global_next" + ): current_filter = filter_container.sf_filter - filter_name = utility.pointer_to_string(current_filter.sf_name, count = 128) + filter_name = utility.pointer_to_string(current_filter.sf_name, count=128) try: filter_socket = filter_container.sf_entry_head.sfe_socket.vol.offset @@ -56,16 +79,37 @@ class Socket_filters(plugins.PluginInterface): filter_socket = 0 for member in members_to_check: - check_addr = current_filter.member(attr = member) + check_addr = current_filter.member(attr=member) if check_addr == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, check_addr) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, check_addr + ) - yield (0, (format_hints.Hex(current_filter.vol.offset), filter_name, member, \ - format_hints.Hex(filter_socket), format_hints.Hex(check_addr), module_name, symbol_name)) + yield ( + 0, + ( + format_hints.Hex(current_filter.vol.offset), + filter_name, + member, + format_hints.Hex(filter_socket), + format_hints.Hex(check_addr), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Filter", format_hints.Hex), ("Name", str), ("Member", str), - ("Socket", format_hints.Hex), ("Handler", format_hints.Hex), ("Module", str), - ("Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Filter", format_hints.Hex), + ("Name", str), + ("Member", str), + ("Socket", format_hints.Hex), + ("Handler", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/timers.py b/volatility3/framework/plugins/mac/timers.py index 7bc5fd5d0..8a267bd55 100644 --- a/volatility3/framework/plugins/mac/timers.py +++ b/volatility3/framework/plugins/mac/timers.py @@ -23,33 +23,46 @@ class Timers(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 3, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 3, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - mods = lsmod.Lsmod.list_modules(self.context, self.config['kernel']) + mods = lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) - real_ncpus = kernel.object_from_symbol(symbol_name = "real_ncpus") + real_ncpus = kernel.object_from_symbol(symbol_name="real_ncpus") cpu_data_ptrs_ptr = kernel.get_symbol("cpu_data_ptr").address # Returns the a pointer to the absolute address - cpu_data_ptrs_addr = kernel.object(object_type = "pointer", - offset = cpu_data_ptrs_ptr, - subtype = kernel.get_type('long unsigned int')) + cpu_data_ptrs_addr = kernel.object( + object_type="pointer", + offset=cpu_data_ptrs_ptr, + subtype=kernel.get_type("long unsigned int"), + ) - cpu_data_ptrs = kernel.object(object_type = "array", - offset = cpu_data_ptrs_addr, - absolute = True, - subtype = kernel.get_type('cpu_data'), - count = real_ncpus) + cpu_data_ptrs = kernel.object( + object_type="array", + offset=cpu_data_ptrs_addr, + absolute=True, + subtype=kernel.get_type("cpu_data"), + count=real_ncpus, + ) for cpu_data_ptr in cpu_data_ptrs: try: @@ -68,13 +81,33 @@ class Timers(plugins.PluginInterface): else: entry_time = -1 - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, handler, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, handler, self.config["kernel"] + ) - yield (0, (format_hints.Hex(handler), format_hints.Hex(timer.param0), format_hints.Hex(timer.param1), - timer.deadline, entry_time, module_name, symbol_name)) + yield ( + 0, + ( + format_hints.Hex(handler), + format_hints.Hex(timer.param0), + format_hints.Hex(timer.param1), + timer.deadline, + entry_time, + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Function", format_hints.Hex), ("Param 0", format_hints.Hex), - ("Param 1", format_hints.Hex), ("Deadline", int), ("Entry Time", int), - ("Module", str), ("Symbol", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Function", format_hints.Hex), + ("Param 0", format_hints.Hex), + ("Param 1", format_hints.Hex), + ("Deadline", int), + ("Entry Time", int), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/mac/trustedbsd.py b/volatility3/framework/plugins/mac/trustedbsd.py index 9efb1b9d6..a03e2a903 100644 --- a/volatility3/framework/plugins/mac/trustedbsd.py +++ b/volatility3/framework/plugins/mac/trustedbsd.py @@ -25,24 +25,37 @@ class Trustedbsd(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'macutils', component = mac.MacUtilities, version = (1, 3, 0)), - requirements.PluginRequirement(name = 'lsmod', plugin = lsmod.Lsmod, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="macutils", component=mac.MacUtilities, version=(1, 3, 0) + ), + requirements.PluginRequirement( + name="lsmod", plugin=lsmod.Lsmod, version=(2, 0, 0) + ), ] def _generator(self, mods: Iterator[Any]): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - handlers = mac.MacUtilities.generate_kernel_handler_info(self.context, kernel.layer_name, kernel, mods) + handlers = mac.MacUtilities.generate_kernel_handler_info( + self.context, kernel.layer_name, kernel, mods + ) - policy_list = kernel.object_from_symbol(symbol_name = "mac_policy_list").cast("mac_policy_list") + policy_list = kernel.object_from_symbol(symbol_name="mac_policy_list").cast( + "mac_policy_list" + ) - entries = kernel.object(object_type = "array", - offset = policy_list.entries.dereference().vol.offset, - subtype = kernel.get_type('mac_policy_list_element'), - absolute = True, - count = policy_list.staticmax + 1) + entries = kernel.object( + object_type="array", + offset=policy_list.entries.dereference().vol.offset, + subtype=kernel.get_type("mac_policy_list_element"), + absolute=True, + count=policy_list.staticmax + 1, + ) for i, ent in enumerate(entries): # I don't know how this can happen, but the kernel makes this check all over the place @@ -64,13 +77,31 @@ class Trustedbsd(plugins.PluginInterface): if call_addr is None or call_addr == 0: continue - module_name, symbol_name = mac.MacUtilities.lookup_module_address(self.context, handlers, call_addr, - self.config['kernel']) + module_name, symbol_name = mac.MacUtilities.lookup_module_address( + self.context, handlers, call_addr, self.config["kernel"] + ) - yield (0, (check, ent_name, format_hints.Hex(call_addr), module_name, symbol_name)) + yield ( + 0, + ( + check, + ent_name, + format_hints.Hex(call_addr), + module_name, + symbol_name, + ), + ) def run(self): - return renderers.TreeGrid([("Member", str), ("Policy Name", str), ("Handler Address", format_hints.Hex), - ("Handler Module", str), ("Handler Symbol", str)], - self._generator( - lsmod.Lsmod.list_modules(self.context, self.config['kernel']))) + return renderers.TreeGrid( + [ + ("Member", str), + ("Policy Name", str), + ("Handler Address", format_hints.Hex), + ("Handler Module", str), + ("Handler Symbol", str), + ], + self._generator( + lsmod.Lsmod.list_modules(self.context, self.config["kernel"]) + ), + ) diff --git a/volatility3/framework/plugins/mac/vfsevents.py b/volatility3/framework/plugins/mac/vfsevents.py index bc5668495..5aca76467 100644 --- a/volatility3/framework/plugins/mac/vfsevents.py +++ b/volatility3/framework/plugins/mac/vfsevents.py @@ -8,20 +8,34 @@ from volatility3.framework.objects import utility class VFSevents(interfaces.plugins.PluginInterface): - """ Lists processes that are filtering file system events """ + """Lists processes that are filtering file system events""" _required_framework_version = (2, 0, 0) event_types = [ - "CREATE_FILE", "DELETE", "STAT_CHANGED", "RENAME", "CONTENT_MODIFIED", "EXCHANGE", "FINDER_INFO_CHANGED", - "CREATE_DIR", "CHOWN", "XATTR_MODIFIED", "XATTR_REMOVED", "DOCID_CREATED", "DOCID_CHANGED" + "CREATE_FILE", + "DELETE", + "STAT_CHANGED", + "RENAME", + "CONTENT_MODIFIED", + "EXCHANGE", + "FINDER_INFO_CHANGED", + "CREATE_DIR", + "CHOWN", + "XATTR_MODIFIED", + "XATTR_REMOVED", + "DOCID_CREATED", + "DOCID_CHANGED", ] @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Kernel module for the OS', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Kernel module for the OS", + architectures=["Intel32", "Intel64"], + ), ] def _generator(self): @@ -30,7 +44,7 @@ class VFSevents(interfaces.plugins.PluginInterface): Also lists which event(s) a process is registered for """ - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] watcher_table = kernel.object_from_symbol("watcher_table") @@ -44,11 +58,13 @@ class VFSevents(interfaces.plugins.PluginInterface): events = [] try: - event_array = kernel.object(object_type = "array", - offset = watcher.event_list, - absolute = True, - count = 13, - subtype = kernel.get_type("unsigned char")) + event_array = kernel.object( + object_type="array", + offset=watcher.event_list, + absolute=True, + count=13, + subtype=kernel.get_type("unsigned char"), + ) except exceptions.InvalidAddressException: continue @@ -61,4 +77,6 @@ class VFSevents(interfaces.plugins.PluginInterface): yield (0, (task_name, task_pid, ",".join(events))) def run(self): - return renderers.TreeGrid([("Name", str), ("PID", int), ("Events", str)], self._generator()) + return renderers.TreeGrid( + [("Name", str), ("PID", int), ("Events", str)], self._generator() + ) diff --git a/volatility3/framework/plugins/timeliner.py b/volatility3/framework/plugins/timeliner.py index 8785f62e1..d1c1c0f70 100644 --- a/volatility3/framework/plugins/timeliner.py +++ b/volatility3/framework/plugins/timeliner.py @@ -25,12 +25,14 @@ class TimeLinerType(enum.IntEnum): CHANGED = 4 -class TimeLinerInterface(metaclass = abc.ABCMeta): +class TimeLinerInterface(metaclass=abc.ABCMeta): """Interface defining methods that timeliner will use to generate a body file.""" @abc.abstractmethod - def generate_timeline(self) -> Generator[Tuple[str, TimeLinerType, datetime.datetime], None, None]: + def generate_timeline( + self, + ) -> Generator[Tuple[str, TimeLinerType, datetime.datetime], None, None]: """Method generates Tuples of (description, timestamp_type, timestamp) These need not be generated in any particular order, sorting @@ -69,72 +71,106 @@ class Timeliner(interfaces.plugins.PluginInterface): else: selected_list = [] - return [plugin_class for plugin_class in plugin_list if filter_func(plugin_class.__name__, selected_list)] + return [ + plugin_class + for plugin_class in plugin_list + if filter_func(plugin_class.__name__, selected_list) + ] @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ requirements.BooleanRequirement( - name = 'record-config', - description = "Whether to record the state of all the plugins once complete", - optional = True, - default = False), - requirements.ListRequirement(name = 'plugin-filter', - description = "Only run plugins featuring this substring", - element_type = str, - optional = True, - default = []), - requirements.BooleanRequirement(name = 'create-bodyfile', - description = "Whether to create a body file whilst producing results", - optional = True, - default = False) + name="record-config", + description="Whether to record the state of all the plugins once complete", + optional=True, + default=False, + ), + requirements.ListRequirement( + name="plugin-filter", + description="Only run plugins featuring this substring", + element_type=str, + optional=True, + default=[], + ), + requirements.BooleanRequirement( + name="create-bodyfile", + description="Whether to create a body file whilst producing results", + optional=True, + default=False, + ), ] def _sort_function(self, item): data = item[1] def sortable(timestamp): - max_date = datetime.datetime(day = 1, month = 12, year = datetime.MAXYEAR) + max_date = datetime.datetime(day=1, month=12, year=datetime.MAXYEAR) if isinstance(timestamp, interfaces.renderers.BaseAbsentValue): return max_date return timestamp return [sortable(timestamp) for timestamp in data[2:]] - def _generator(self, runable_plugins: List[TimeLinerInterface]) -> Optional[Iterable[Tuple[int, Tuple]]]: + def _generator( + self, runnable_plugins: List[TimeLinerInterface] + ) -> Optional[Iterable[Tuple[int, Tuple]]]: """Takes a timeline, sorts it and output the data from each relevant row from each plugin.""" # Generate the results for each plugin data = [] # Open the bodyfile now, so we can start outputting to it immediately - if self.config.get('create-bodyfile', True): + if self.config.get("create-bodyfile", True): file_data = self.open("volatility.body") - fp = io.TextIOWrapper(file_data, write_through = True) + fp = io.TextIOWrapper(file_data, write_through=True) else: file_data = None fp = None - for plugin in runable_plugins: + for plugin in runnable_plugins: plugin_name = plugin.__class__.__name__ - self._progress_callback((runable_plugins.index(plugin) * 100) // len(runable_plugins), - f"Running plugin {plugin_name}...") + self._progress_callback( + (runnable_plugins.index(plugin) * 100) // len(runnable_plugins), + f"Running plugin {plugin_name}...", + ) try: vollog.log(logging.INFO, f"Running {plugin_name}") - for (item, timestamp_type, timestamp) in plugin.generate_timeline(): + for item, timestamp_type, timestamp in plugin.generate_timeline(): times = self.timeline.get((plugin_name, item), {}) if times.get(timestamp_type, None) is not None: - vollog.debug("Multiple timestamps for the same plugin/file combination found: {} {}".format( - plugin_name, item)) + vollog.debug( + "Multiple timestamps for the same plugin/file combination found: {} {}".format( + plugin_name, item + ) + ) times[timestamp_type] = timestamp self.timeline[(plugin_name, item)] = times - data.append((0, [ - plugin_name, item, - times.get(TimeLinerType.CREATED, renderers.NotApplicableValue()), - times.get(TimeLinerType.MODIFIED, renderers.NotApplicableValue()), - times.get(TimeLinerType.ACCESSED, renderers.NotApplicableValue()), - times.get(TimeLinerType.CHANGED, renderers.NotApplicableValue()) - ])) + data.append( + ( + 0, + [ + plugin_name, + item, + times.get( + TimeLinerType.CREATED, + renderers.NotApplicableValue(), + ), + times.get( + TimeLinerType.MODIFIED, + renderers.NotApplicableValue(), + ), + times.get( + TimeLinerType.ACCESSED, + renderers.NotApplicableValue(), + ), + times.get( + TimeLinerType.CHANGED, + renderers.NotApplicableValue(), + ), + ], + ) + ) # Write each entry because the body file doesn't need to be sorted if fp: @@ -142,21 +178,35 @@ class Timeliner(interfaces.plugins.PluginInterface): # Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime if self._any_time_present(times): - fp.write("|{} - {}|0|0|0|0|0|{}|{}|{}|{}\n".format( - plugin_name, self._sanitize_body_format(item), - self._text_format(times.get(TimeLinerType.ACCESSED, "")), - self._text_format(times.get(TimeLinerType.MODIFIED, "")), - self._text_format(times.get(TimeLinerType.CHANGED, "")), - self._text_format(times.get(TimeLinerType.CREATED, "")))) + fp.write( + "|{} - {}|0|0|0|0|0|{}|{}|{}|{}\n".format( + plugin_name, + self._sanitize_body_format(item), + self._text_format( + times.get(TimeLinerType.ACCESSED, "") + ), + self._text_format( + times.get(TimeLinerType.MODIFIED, "") + ), + self._text_format( + times.get(TimeLinerType.CHANGED, "") + ), + self._text_format( + times.get(TimeLinerType.CREATED, "") + ), + ) + ) except Exception: - vollog.log(logging.INFO, f"Exception occurred running plugin: {plugin_name}") + vollog.log( + logging.INFO, f"Exception occurred running plugin: {plugin_name}" + ) vollog.log(logging.DEBUG, traceback.format_exc()) - for data_item in sorted(data, key = self._sort_function): + for data_item in sorted(data, key=self._sort_function): yield data_item # Write out a body file if necessary - if self.config.get('create-bodyfile', True): + if self.config.get("create-bodyfile", True): if fp: fp.close() file_data.close() @@ -166,7 +216,10 @@ class Timeliner(interfaces.plugins.PluginInterface): def _any_time_present(self, times): for time in TimeLinerType: - if not isinstance(times.get(time, renderers.NotApplicableValue), interfaces.renderers.BaseAbsentValue): + if not isinstance( + times.get(time, renderers.NotApplicableValue), + interfaces.renderers.BaseAbsentValue, + ): return True return False @@ -187,7 +240,7 @@ class Timeliner(interfaces.plugins.PluginInterface): plugins_to_run = [] requirement_configs = {} - filter_list = self.config['plugin-filter'] + filter_list = self.config["plugin-filter"] # Identify plugins that we can run which output datetimes for plugin_class in self.usable_plugins: try: @@ -197,42 +250,72 @@ class Timeliner(interfaces.plugins.PluginInterface): if requirement.name in requirement_configs: config_req, config_value = requirement_configs[requirement.name] if requirement == config_req: - self.context.config[interfaces.configuration.path_join( - self.config_path, plugin_class.__name__)] = config_value + self.context.config[ + interfaces.configuration.path_join( + self.config_path, plugin_class.__name__ + ) + ] = config_value - plugin = plugins.construct_plugin(self.context, automagics, plugin_class, self.config_path, - self._progress_callback, self.open) + plugin = plugins.construct_plugin( + self.context, + automagics, + plugin_class, + self.config_path, + self._progress_callback, + self.open, + ) for requirement in plugin.get_requirements(): if requirement.name not in requirement_configs: config_value = plugin.config.get(requirement.name, None) if config_value: - requirement_configs[requirement.name] = (requirement, config_value) + requirement_configs[requirement.name] = ( + requirement, + config_value, + ) if isinstance(plugin, TimeLinerInterface): if not len(filter_list) or any( - [filter in plugin.__module__ + '.' + plugin.__class__.__name__ for filter in filter_list]): + [ + filter + in plugin.__module__ + "." + plugin.__class__.__name__ + for filter in filter_list + ] + ): plugins_to_run.append(plugin) except exceptions.UnsatisfiedException as excp: # Remove the failed plugin from the list and continue - vollog.debug(f"Unable to satisfy {plugin_class.__name__}: {excp.unsatisfied}") + vollog.debug( + f"Unable to satisfy {plugin_class.__name__}: {excp.unsatisfied}" + ) continue - if self.config.get('record-config', False): + if self.config.get("record-config", False): total_config = {} for plugin in plugins_to_run: old_dict = dict(plugin.build_configuration()) for entry in old_dict: - total_config[interfaces.configuration.path_join(plugin.__class__.__name__, entry)] = old_dict[entry] + total_config[ + interfaces.configuration.path_join( + plugin.__class__.__name__, entry + ) + ] = old_dict[entry] with self.open("config.json") as file_data: - with io.TextIOWrapper(file_data, write_through = True) as fp: - json.dump(total_config, fp, sort_keys = True, indent = 2) + with io.TextIOWrapper(file_data, write_through=True) as fp: + json.dump(total_config, fp, sort_keys=True, indent=2) - return renderers.TreeGrid(columns = [("Plugin", str), ("Description", str), ("Created Date", datetime.datetime), - ("Modified Date", datetime.datetime), ("Accessed Date", datetime.datetime), - ("Changed Date", datetime.datetime)], - generator = self._generator(plugins_to_run)) + return renderers.TreeGrid( + columns=[ + ("Plugin", str), + ("Description", str), + ("Created Date", datetime.datetime), + ("Modified Date", datetime.datetime), + ("Accessed Date", datetime.datetime), + ("Changed Date", datetime.datetime), + ], + generator=self._generator(plugins_to_run), + ) def build_configuration(self): """Builds the configuration to save for the plugin such that it can be diff --git a/volatility3/framework/plugins/windows/bigpools.py b/volatility3/framework/plugins/windows/bigpools.py index 9e120446f..393c2a417 100644 --- a/volatility3/framework/plugins/windows/bigpools.py +++ b/volatility3/framework/plugins/windows/bigpools.py @@ -27,25 +27,34 @@ class BigPools(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.StringRequirement(name = 'tags', - description = "Comma separated list of pool tags to filter pools returned", - optional = True, - default = None), - requirements.BooleanRequirement(name = 'show-free', - description = 'Show freed regions (otherwise only show allocations in use)', - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.StringRequirement( + name="tags", + description="Comma separated list of pool tags to filter pools returned", + optional=True, + default=None, + ), + requirements.BooleanRequirement( + name="show-free", + description="Show freed regions (otherwise only show allocations in use)", + default=False, + optional=True, + ), ] @classmethod - def list_big_pools(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - tags: Optional[list] = None, - show_free: bool = False): + def list_big_pools( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + tags: Optional[list] = None, + show_free: bool = False, + ): """Returns the big page pool objects from the kernel PoolBigPageTable array. Args: @@ -57,14 +66,18 @@ class BigPools(interfaces.plugins.PluginInterface): Yields: A big page pool object """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) big_page_table_offset = ntkrnlmp.get_symbol("PoolBigPageTable").address - big_page_table = ntkrnlmp.object(object_type = "unsigned long long", offset = big_page_table_offset) + big_page_table = ntkrnlmp.object( + object_type="unsigned long long", offset=big_page_table_offset + ) big_page_table_size_offset = ntkrnlmp.get_symbol("PoolBigPageTableSize").address - big_page_table_size = ntkrnlmp.object(object_type = "unsigned long", offset = big_page_table_size_offset) + big_page_table_size = ntkrnlmp.object( + object_type="unsigned long", offset=big_page_table_size_offset + ) try: big_page_table_type = ntkrnlmp.get_type("_POOL_TRACKER_BIG_PAGES") @@ -85,39 +98,49 @@ class BigPools(interfaces.plugins.PluginInterface): big_pools_json_filename += "-x86" new_table_name = intermed.IntermediateSymbolTable.create( - context = context, - config_path = configuration.path_join(context.symbol_space[symbol_table].config_path, "bigpools"), - sub_path = os.path.join("windows", "bigpools"), - filename = big_pools_json_filename, - table_mapping = {'nt_symbols': symbol_table}, - class_types = {'_POOL_TRACKER_BIG_PAGES': extensions.pool.POOL_TRACKER_BIG_PAGES}) - module = context.module(new_table_name, layer_name, offset = 0) + context=context, + config_path=configuration.path_join( + context.symbol_space[symbol_table].config_path, "bigpools" + ), + sub_path=os.path.join("windows", "bigpools"), + filename=big_pools_json_filename, + table_mapping={"nt_symbols": symbol_table}, + class_types={ + "_POOL_TRACKER_BIG_PAGES": extensions.pool.POOL_TRACKER_BIG_PAGES + }, + ) + module = context.module(new_table_name, layer_name, offset=0) big_page_table_type = module.get_type("_POOL_TRACKER_BIG_PAGES") - big_pools = ntkrnlmp.object(object_type = "array", - offset = big_page_table, - subtype = big_page_table_type, - count = big_page_table_size, - absolute = True) + big_pools = ntkrnlmp.object( + object_type="array", + offset=big_page_table, + subtype=big_page_table_type, + count=big_page_table_size, + absolute=True, + ) for big_pool in big_pools: if big_pool.is_valid(): - if (tags is None or big_pool.get_key() in tags) and (show_free or not big_pool.is_free()): + if (tags is None or big_pool.get_key() in tags) and ( + show_free or not big_pool.is_free() + ): yield big_pool def _generator(self) -> Iterator[Tuple[int, Tuple[int, str]]]: # , str, int]]]: if self.config.get("tags"): - tags = [tag for tag in self.config["tags"].split(',')] + tags = [tag for tag in self.config["tags"].split(",")] else: tags = None - kernel = self.context.modules[self.config['kernel']] - - for big_pool in self.list_big_pools(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - tags = tags, - show_free = self.config.get("show-free")): + kernel = self.context.modules[self.config["kernel"]] + for big_pool in self.list_big_pools( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + tags=tags, + show_free=self.config.get("show-free"), + ): num_bytes = big_pool.get_number_of_bytes() if not isinstance(num_bytes, interfaces.renderers.BaseAbsentValue): num_bytes = format_hints.Hex(num_bytes) @@ -127,13 +150,25 @@ class BigPools(interfaces.plugins.PluginInterface): else: status = "Allocated" - yield (0, (format_hints.Hex(big_pool.Va), big_pool.get_key(), big_pool.get_pool_type(), num_bytes, status)) + yield ( + 0, + ( + format_hints.Hex(big_pool.Va), + big_pool.get_key(), + big_pool.get_pool_type(), + num_bytes, + status, + ), + ) def run(self): - return renderers.TreeGrid([ - ('Allocation', format_hints.Hex), - ('Tag', str), - ('PoolType', str), - ('NumberOfBytes', format_hints.Hex), - ('Status', str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Allocation", format_hints.Hex), + ("Tag", str), + ("PoolType", str), + ("NumberOfBytes", format_hints.Hex), + ("Status", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/cachedump.py b/volatility3/framework/plugins/windows/cachedump.py index ddfa856b9..6e667984a 100644 --- a/volatility3/framework/plugins/windows/cachedump.py +++ b/volatility3/framework/plugins/windows/cachedump.py @@ -27,16 +27,29 @@ class Cachedump(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'lsadump', plugin = lsadump.Lsadump, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'hashdump', plugin = hashdump.Hashdump, version = (1, 1, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="lsadump", plugin=lsadump.Lsadump, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="hashdump", plugin=hashdump.Hashdump, version=(1, 1, 0) + ), ] @staticmethod - def get_nlkm(sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool): - return lsadump.Lsadump.get_secret_by_name(sechive, 'NL$KM', lsakey, is_vista_or_later) + def get_nlkm( + sechive: registry.RegistryHive, lsakey: bytes, is_vista_or_later: bool + ): + return lsadump.Lsadump.get_secret_by_name( + sechive, "NL$KM", lsakey, is_vista_or_later + ) @staticmethod def decrypt_hash(edata: bytes, nlkm: bytes, ch, xp: bool): @@ -44,13 +57,13 @@ class Cachedump(interfaces.plugins.PluginInterface): hmac_md5 = HMAC.new(nlkm, ch) rc4key = hmac_md5.digest() rc4 = ARC4.new(rc4key) - data = rc4.encrypt(edata) # lgtm [py/weak-cryptographic-algorithm] + data = rc4.encrypt(edata) # lgtm [py/weak-cryptographic-algorithm] else: - # based on Based on code from http://lab.mediaservice.net/code/cachedump.rb + # Based on code from http://lab.mediaservice.net/code/cachedump.rb aes = AES.new(nlkm[16:32], AES.MODE_CBC, ch) data = b"" for i in range(0, len(edata), 16): - buf = edata[i:i + 16] + buf = edata[i : i + 16] if len(buf) < 16: buf += (16 - len(buf)) * b"\00" data += aes.decrypt(buf) @@ -60,15 +73,16 @@ class Cachedump(interfaces.plugins.PluginInterface): def parse_cache_entry(cache_data: bytes) -> Tuple[int, int, int, bytes, bytes]: (uname_len, domain_len) = unpack(" Tuple[str, str, str, bytes]: + def parse_decrypted_cache( + dec_data: bytes, uname_len: int, domain_len: int, domain_name_len: int + ) -> Tuple[str, str, str, bytes]: """Get the data from the cache and separate it into the username, domain name, and hash data""" uname_offset = 72 pad = 2 * ((uname_len / 2) % 2) @@ -76,37 +90,51 @@ class Cachedump(interfaces.plugins.PluginInterface): pad = 2 * ((domain_len / 2) % 2) domain_name_offset = int(domain_offset + domain_len + pad) hashh = dec_data[:0x10] - username = dec_data[uname_offset:uname_offset + uname_len].decode('utf-16-le', 'replace') - domain = dec_data[domain_offset:domain_offset + domain_len].decode('utf-16-le', 'replace') - domain_name = dec_data[domain_name_offset:domain_name_offset + domain_name_len].decode('utf-16-le', 'replace') + username = dec_data[uname_offset : uname_offset + uname_len].decode( + "utf-16-le", "replace" + ) + domain = dec_data[domain_offset : domain_offset + domain_len].decode( + "utf-16-le", "replace" + ) + domain_name = dec_data[ + domain_name_offset : domain_name_offset + domain_name_len + ].decode("utf-16-le", "replace") return (username, domain, domain_name, hashh) def _generator(self, syshive, sechive): + if not syshive or not sechive: + if syshive is None: + vollog.warning("Unable to locate SYSTEM hive") + if sechive is None: + vollog.warning("Unable to locate SECURITY hive") + return None + bootkey = hashdump.Hashdump.get_bootkey(syshive) if not bootkey: - vollog.warning('Unable to find bootkey') - return + vollog.warning("Unable to find bootkey") + return None - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - vista_or_later = versions.is_vista_or_later(context = self.context, - symbol_table = kernel.symbol_table_name) + vista_or_later = versions.is_vista_or_later( + context=self.context, symbol_table=kernel.symbol_table_name + ) lsakey = lsadump.Lsadump.get_lsa_key(sechive, bootkey, vista_or_later) if not lsakey: - vollog.warning('Unable to find lsa key') - return + vollog.warning("Unable to find lsa key") + return None nlkm = self.get_nlkm(sechive, lsakey, vista_or_later) if not nlkm: - vollog.warning('Unable to find nlkma key') - return + vollog.warning("Unable to find nlkma key") + return None cache = hashdump.Hashdump.get_hive_key(sechive, "Cache") if not cache: - vollog.warning('Unable to find cache key') - return + vollog.warning("Unable to find cache key") + return None for cache_item in cache.get_values(): if cache_item.Name == "NL$Control": @@ -115,39 +143,42 @@ class Cachedump(interfaces.plugins.PluginInterface): data = sechive.read(cache_item.Data + 4, cache_item.DataLength) if data is None: continue - (uname_len, domain_len, domain_name_len, enc_data, ch) = self.parse_cache_entry(data) + ( + uname_len, + domain_len, + domain_name_len, + enc_data, + ch, + ) = self.parse_cache_entry(data) # Skip if nothing in this cache entry if uname_len == 0 or len(ch) == 0: continue dec_data = self.decrypt_hash(enc_data, nlkm, ch, not vista_or_later) - (username, domain, domain_name, hashh) = self.parse_decrypted_cache(dec_data, uname_len, domain_len, - domain_name_len) + (username, domain, domain_name, hashh) = self.parse_decrypted_cache( + dec_data, uname_len, domain_len, domain_name_len + ) yield (0, (username, domain, domain_name, hashh)) def run(self): - offset = self.config.get('offset', None) + offset = self.config.get("offset", None) syshive = sechive = None - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - for hive in hivelist.HiveList.list_hives(self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - hive_offsets = None if offset is None else [offset]): - - if hive.get_name().split('\\')[-1].upper() == 'SYSTEM': + for hive in hivelist.HiveList.list_hives( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + hive_offsets=None if offset is None else [offset], + ): + if hive.get_name().split("\\")[-1].upper() == "SYSTEM": syshive = hive - if hive.get_name().split('\\')[-1].upper() == 'SECURITY': + if hive.get_name().split("\\")[-1].upper() == "SECURITY": sechive = hive - if syshive is None or sechive is None: - if syshive is None: - vollog.warning('Unable to locate SYSTEM hive') - if sechive is None: - vollog.warning('Unable to locate SECURITY hive') - return - - return renderers.TreeGrid([("Username", str), ("Domain", str), ("Domain name", str), ('Hash', bytes)], - self._generator(syshive, sechive)) + return renderers.TreeGrid( + [("Username", str), ("Domain", str), ("Domain name", str), ("Hash", bytes)], + self._generator(syshive, sechive), + ) diff --git a/volatility3/framework/plugins/windows/callbacks.py b/volatility3/framework/plugins/windows/callbacks.py index 2195671df..fcc333b9f 100644 --- a/volatility3/framework/plugins/windows/callbacks.py +++ b/volatility3/framework/plugins/windows/callbacks.py @@ -3,6 +3,7 @@ # import logging +import contextlib from typing import List, Iterable, Tuple, Optional, Union from volatility3.framework import constants, exceptions, renderers, interfaces, symbols @@ -24,13 +25,22 @@ class Callbacks(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'ssdt', plugin = ssdt.SSDT, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + ), ] @staticmethod - def create_callback_table(context: interfaces.context.ContextInterface, symbol_table: str, config_path: str) -> str: + def create_callback_table( + context: interfaces.context.ContextInterface, + symbol_table: str, + config_path: str, + ) -> str: """Creates a symbol table for a set of callbacks. Args: @@ -50,39 +60,50 @@ class Callbacks(interfaces.plugins.PluginInterface): else: symbol_filename = "callbacks-x86" - return intermed.IntermediateSymbolTable.create(context, - config_path, - "windows", - symbol_filename, - native_types = native_types, - table_mapping = table_mapping) + return intermed.IntermediateSymbolTable.create( + context, + config_path, + "windows", + symbol_filename, + native_types=native_types, + table_mapping=table_mapping, + ) @classmethod - def list_notify_routines(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - callback_table_name: str) -> Iterable[Tuple[str, int, Optional[str]]]: + def list_notify_routines( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, Optional[str]]]: """Lists all kernel notification routines. Args: context: The context to retrieve required elements (layers, symbol tables) from layer_name: The name of the layer on which to operate symbol_table: The name of the table containing the kernel symbols - callback_table_name: The nae of the table containing the callback symbols + callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) - is_vista_or_later = versions.is_vista_or_later(context = context, symbol_table = symbol_table) + is_vista_or_later = versions.is_vista_or_later( + context=context, symbol_table=symbol_table + ) full_type_name = callback_table_name + constants.BANG + "_GENERIC_CALLBACK" - symbol_names = [("PspLoadImageNotifyRoutine", False), ("PspCreateThreadNotifyRoutine", True), - ("PspCreateProcessNotifyRoutine", True)] + symbol_names = [ + ("PspLoadImageNotifyRoutine", False), + ("PspCreateThreadNotifyRoutine", True), + ("PspCreateProcessNotifyRoutine", True), + ] for symbol_name, extended_list in symbol_names: - try: symbol_offset = ntkrnlmp.get_symbol(symbol_name).address except exceptions.SymbolError: @@ -94,10 +115,12 @@ class Callbacks(interfaces.plugins.PluginInterface): else: count = 8 - fast_refs = ntkrnlmp.object(object_type = "array", - offset = symbol_offset, - subtype = ntkrnlmp.get_type("_EX_FAST_REF"), - count = count) + fast_refs = ntkrnlmp.object( + object_type="array", + offset=symbol_offset, + subtype=ntkrnlmp.get_type("_EX_FAST_REF"), + count=count, + ) for fast_ref in fast_refs: try: @@ -109,29 +132,39 @@ class Callbacks(interfaces.plugins.PluginInterface): yield symbol_name, callback.Callback, None @classmethod - def _list_registry_callbacks_legacy(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - callback_table_name: str) -> Iterable[Tuple[str, int, None]]: + def _list_registry_callbacks_legacy( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, None]]: """ Lists all registry callbacks from the old format via the CmpCallBackVector. """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) - full_type_name = callback_table_name + constants.BANG + "_EX_CALLBACK_ROUTINE_BLOCK" + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) + full_type_name = ( + callback_table_name + constants.BANG + "_EX_CALLBACK_ROUTINE_BLOCK" + ) symbol_offset = ntkrnlmp.get_symbol("CmpCallBackVector").address symbol_count_offset = ntkrnlmp.get_symbol("CmpCallBackCount").address - - callback_count = ntkrnlmp.object(object_type = "unsigned int", offset = symbol_count_offset) + callback_count = ntkrnlmp.object( + object_type="unsigned int", offset=symbol_count_offset + ) if callback_count == 0: - return + return None - fast_refs = ntkrnlmp.object(object_type = "array", - offset = symbol_offset, - subtype = ntkrnlmp.get_type("_EX_FAST_REF"), - count = callback_count) + fast_refs = ntkrnlmp.object( + object_type="array", + offset=symbol_offset, + subtype=ntkrnlmp.get_type("_EX_FAST_REF"), + count=callback_count, + ) for fast_ref in fast_refs: try: @@ -143,52 +176,79 @@ class Callbacks(interfaces.plugins.PluginInterface): yield "CmRegisterCallback", callback.Function, None @classmethod - def _list_registry_callbacks_new(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - callback_table_name: str) -> Iterable[Tuple[str, int, None]]: + def _list_registry_callbacks_new( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, Optional[str]]]: """ Lists all registry callbacks via the CallbackListHead. """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) full_type_name = callback_table_name + constants.BANG + "_CM_CALLBACK_ENTRY" symbol_offset = ntkrnlmp.get_symbol("CallbackListHead").address symbol_count_offset = ntkrnlmp.get_symbol("CmpCallBackCount").address - callback_count = ntkrnlmp.object(object_type = "unsigned int", offset = symbol_count_offset) + callback_count = ntkrnlmp.object( + object_type="unsigned int", offset=symbol_count_offset + ) if callback_count == 0: - return + return None - callback_list = ntkrnlmp.object(object_type = "_LIST_ENTRY", offset = symbol_offset) + callback_list = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=symbol_offset) for callback in callback_list.to_list(full_type_name, "Link"): - yield "CmRegisterCallbackEx", callback.Function, f"Altitude: {callback.Altitude.String}" + altitude = None + with contextlib.suppress(exceptions.InvalidAddressException): + altitude = callback.Altitude.String + yield "CmRegisterCallbackEx", callback.Function, f"Altitude: {altitude}" @classmethod - def list_registry_callbacks(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - callback_table_name: str) -> Iterable[Tuple[str, int, None]]: + def list_registry_callbacks( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, Optional[str]]]: """Lists all registry callbacks. Args: context: The context to retrieve required elements (layers, symbol tables) from layer_name: The name of the layer on which to operate symbol_table: The name of the table containing the kernel symbols - callback_table_name: The nae of the table containing the callback symbols + callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) - if ntkrnlmp.has_symbol("CmpCallBackVector") and ntkrnlmp.has_symbol("CmpCallBackCount"): - yield from cls._list_registry_callbacks_legacy(context, layer_name, symbol_table, callback_table_name) - elif ntkrnlmp.has_symbol("CallbackListHead") and ntkrnlmp.has_symbol("CmpCallBackCount"): - yield from cls._list_registry_callbacks_new(context, layer_name, symbol_table, callback_table_name) + if ntkrnlmp.has_symbol("CmpCallBackVector") and ntkrnlmp.has_symbol( + "CmpCallBackCount" + ): + yield from cls._list_registry_callbacks_legacy( + context, layer_name, symbol_table, callback_table_name + ) + elif ntkrnlmp.has_symbol("CallbackListHead") and ntkrnlmp.has_symbol( + "CmpCallBackCount" + ): + yield from cls._list_registry_callbacks_new( + context, layer_name, symbol_table, callback_table_name + ) else: - symbols_to_check = ["CmpCallBackVector", "CmpCallBackCount", "CallbackListHead"] + symbols_to_check = [ + "CmpCallBackVector", + "CmpCallBackCount", + "CallbackListHead", + ] vollog.debug("Failed to get registry callbacks!") for symbol_name in symbols_to_check: symbol_status = "does not exist" @@ -196,36 +256,45 @@ class Callbacks(interfaces.plugins.PluginInterface): symbol_status = "exists" vollog.debug(f"symbol {symbol_name} {symbol_status}.") - return + return None @classmethod - def list_bugcheck_reason_callbacks(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str, callback_table_name: str) -> Iterable[Tuple[str, int, str]]: + def list_bugcheck_reason_callbacks( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, str]]: """Lists all kernel bugcheck reason callbacks. Args: context: The context to retrieve required elements (layers, symbol tables) from layer_name: The name of the layer on which to operate symbol_table: The name of the table containing the kernel symbols - callback_table_name: The nae of the table containing the callback symbols + callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) try: - list_offset = ntkrnlmp.get_symbol("KeBugCheckReasonCallbackListHead").address + list_offset = ntkrnlmp.get_symbol( + "KeBugCheckReasonCallbackListHead" + ).address except exceptions.SymbolError: vollog.debug("Cannot find KeBugCheckReasonCallbackListHead") - return + return None - full_type_name = callback_table_name + constants.BANG + "_KBUGCHECK_REASON_CALLBACK_RECORD" - callback_record = context.object(object_type = full_type_name, - offset = kvo + list_offset, - layer_name = layer_name) + full_type_name = ( + callback_table_name + constants.BANG + "_KBUGCHECK_REASON_CALLBACK_RECORD" + ) + callback_record = context.object( + object_type=full_type_name, offset=kvo + list_offset, layer_name=layer_name + ) for callback in callback_record.Entry: if not context.layers[layer_name].is_valid(callback.CallbackRoutine, 64): @@ -233,8 +302,14 @@ class Callbacks(interfaces.plugins.PluginInterface): try: component: Union[ - interfaces.renderers.BaseAbsentValue, interfaces.objects.ObjectInterface] = ntkrnlmp.object( - "string", absolute = True, offset = callback.Component, max_length = 64, errors = "replace" + interfaces.renderers.BaseAbsentValue, + interfaces.objects.ObjectInterface, + ] = ntkrnlmp.object( + "string", + absolute=True, + offset=callback.Component, + max_length=64, + errors="replace", ) except exceptions.InvalidAddressException: component = renderers.UnreadableValue() @@ -242,72 +317,91 @@ class Callbacks(interfaces.plugins.PluginInterface): yield "KeBugCheckReasonCallbackListHead", callback.CallbackRoutine, component @classmethod - def list_bugcheck_callbacks(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str, - callback_table_name: str) -> Iterable[Tuple[str, int, str]]: + def list_bugcheck_callbacks( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + callback_table_name: str, + ) -> Iterable[Tuple[str, int, str]]: """Lists all kernel bugcheck callbacks. Args: context: The context to retrieve required elements (layers, symbol tables) from layer_name: The name of the layer on which to operate symbol_table: The name of the table containing the kernel symbols - callback_table_name: The nae of the table containing the callback symbols + callback_table_name: The name of the table containing the callback symbols Yields: A name, location and optional detail string """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) try: list_offset = ntkrnlmp.get_symbol("KeBugCheckCallbackListHead").address except exceptions.SymbolError: vollog.debug("Cannot find KeBugCheckCallbackListHead") - return + return None - full_type_name = callback_table_name + constants.BANG + "_KBUGCHECK_CALLBACK_RECORD" - callback_record = context.object(full_type_name, offset = kvo + list_offset, layer_name = layer_name) + full_type_name = ( + callback_table_name + constants.BANG + "_KBUGCHECK_CALLBACK_RECORD" + ) + callback_record = context.object( + full_type_name, offset=kvo + list_offset, layer_name=layer_name + ) for callback in callback_record.Entry: - if not context.layers[layer_name].is_valid(callback.CallbackRoutine, 64): continue try: - component = context.object(symbol_table + constants.BANG + "string", - layer_name = layer_name, - offset = callback.Component, - max_length = 64, - errors = "replace") + component = context.object( + symbol_table + constants.BANG + "string", + layer_name=layer_name, + offset=callback.Component, + max_length=64, + errors="replace", + ) except exceptions.InvalidAddressException: component = renderers.UnreadableValue() yield "KeBugCheckCallbackListHead", callback.CallbackRoutine, component def _generator(self): + kernel = self.context.modules[self.config["kernel"]] - kernel = self.context.modules[self.config['kernel']] + callback_table_name = self.create_callback_table( + self.context, kernel.symbol_table_name, self.config_path + ) - callback_table_name = self.create_callback_table(self.context, kernel.symbol_table_name, - self.config_path) + collection = ssdt.SSDT.build_module_collection( + self.context, kernel.layer_name, kernel.symbol_table_name + ) - collection = ssdt.SSDT.build_module_collection(self.context, kernel.layer_name, kernel.symbol_table_name) - - callback_methods = (self.list_notify_routines, self.list_bugcheck_callbacks, - self.list_bugcheck_reason_callbacks, self.list_registry_callbacks) + callback_methods = ( + self.list_notify_routines, + self.list_bugcheck_callbacks, + self.list_bugcheck_reason_callbacks, + self.list_registry_callbacks, + ) for callback_method in callback_methods: - for callback_type, callback_address, callback_detail in callback_method(self.context, - kernel.layer_name, - kernel.symbol_table_name, - callback_table_name): - + for callback_type, callback_address, callback_detail in callback_method( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + callback_table_name, + ): if callback_detail is None: detail = renderers.NotApplicableValue() else: detail = callback_detail - module_symbols = list(collection.get_module_symbols_by_absolute_location(callback_address)) + module_symbols = list( + collection.get_module_symbols_by_absolute_location(callback_address) + ) if module_symbols: for module_name, symbol_generator in module_symbols: @@ -316,19 +410,50 @@ class Callbacks(interfaces.plugins.PluginInterface): # we might have multiple symbols pointing to the same location for symbol in symbol_generator: symbols_found = True - yield (0, (callback_type, format_hints.Hex(callback_address), module_name, - symbol.split(constants.BANG)[1], detail)) + yield ( + 0, + ( + callback_type, + format_hints.Hex(callback_address), + module_name, + symbol.split(constants.BANG)[1], + detail, + ), + ) # no symbols, but we at least can report the module name if not symbols_found: - yield (0, (callback_type, format_hints.Hex(callback_address), module_name, - renderers.NotAvailableValue(), detail)) + yield ( + 0, + ( + callback_type, + format_hints.Hex(callback_address), + module_name, + renderers.NotAvailableValue(), + detail, + ), + ) else: # no module was found at the absolute location - yield (0, (callback_type, format_hints.Hex(callback_address), renderers.NotAvailableValue(), - renderers.NotAvailableValue(), detail)) + yield ( + 0, + ( + callback_type, + format_hints.Hex(callback_address), + renderers.NotAvailableValue(), + renderers.NotAvailableValue(), + detail, + ), + ) def run(self): - - return renderers.TreeGrid([("Type", str), ("Callback", format_hints.Hex), ("Module", str), ("Symbol", str), - ("Detail", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Type", str), + ("Callback", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ("Detail", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/cmdline.py b/volatility3/framework/plugins/windows/cmdline.py index af6035abb..8cfb5576c 100644 --- a/volatility3/framework/plugins/windows/cmdline.py +++ b/volatility3/framework/plugins/windows/cmdline.py @@ -22,17 +22,26 @@ class CmdLine(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows 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="Windows 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, + ), ] @classmethod - def get_cmdline(cls, context: interfaces.context.ContextInterface, kernel_table_name: str, proc): + def get_cmdline( + cls, context: interfaces.context.ContextInterface, kernel_table_name: str, proc + ): """Extracts the cmdline from PEB Args: @@ -46,15 +55,17 @@ class CmdLine(interfaces.plugins.PluginInterface): proc_layer_name = proc.add_process_layer() - peb = context.object(kernel_table_name + constants.BANG + "_PEB", - layer_name = proc_layer_name, - offset = proc.Peb) + peb = context.object( + kernel_table_name + constants.BANG + "_PEB", + layer_name=proc_layer_name, + offset=proc.Peb, + ) result_text = peb.ProcessParameters.CommandLine.get_string() return result_text def _generator(self, procs): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] for proc in procs: process_name = utility.array_to_string(proc.ImageFileName) @@ -62,7 +73,9 @@ class CmdLine(interfaces.plugins.PluginInterface): try: proc_id = proc.UniqueProcessId - result_text = self.get_cmdline(self.context, kernel.symbol_table_name, proc) + result_text = self.get_cmdline( + self.context, kernel.symbol_table_name, proc + ) except exceptions.SwappedInvalidAddressException as exp: result_text = f"Required memory at {exp.invalid_address:#x} is inaccessible (swapped)" @@ -72,17 +85,23 @@ class CmdLine(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException as exp: result_text = "Process {}: Required memory at {:#x} is not valid (incomplete layer {}?)".format( - proc_id, exp.invalid_address, exp.layer_name) + proc_id, exp.invalid_address, exp.layer_name + ) yield (0, (proc.UniqueProcessId, process_name, result_text)) def run(self): - kernel = self.context.modules[self.config['kernel']] - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + kernel = self.context.modules[self.config["kernel"]] + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - return renderers.TreeGrid([("PID", int), ("Process", str), ("Args", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [("PID", int), ("Process", str), ("Args", str)], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/crashinfo.py b/volatility3/framework/plugins/windows/crashinfo.py index d66d86cd1..862eb6080 100644 --- a/volatility3/framework/plugins/windows/crashinfo.py +++ b/volatility3/framework/plugins/windows/crashinfo.py @@ -14,19 +14,23 @@ vollog = logging.getLogger(__name__) class Crashinfo(interfaces.plugins.PluginInterface): + """Lists the information from a Windows crash dump.""" + _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.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ), ] def _generator(self, layer: crash.WindowsCrashDump32Layer): header = layer.get_header() - uptime = datetime.timedelta(microseconds = int(header.SystemUpTime) / 10) + uptime = datetime.timedelta(microseconds=int(header.SystemUpTime) / 10) if header.DumpType == 0x1: dump_type = "Full Dump (0x1)" @@ -42,27 +46,32 @@ class Crashinfo(interfaces.plugins.PluginInterface): bitmap_size = format_hints.Hex(summary_header.BitmapSize) bitmap_pages = format_hints.Hex(summary_header.Pages) else: - bitmap_header_size = bitmap_size = bitmap_pages = renderers.NotApplicableValue() + bitmap_header_size = bitmap_size = bitmap_pages = ( + renderers.NotApplicableValue() + ) - yield (0, ( - utility.array_to_string(header.Signature), - header.MajorVersion, - header.MinorVersion, - format_hints.Hex(header.DirectoryTableBase), - format_hints.Hex(header.PfnDataBase), - format_hints.Hex(header.PsLoadedModuleList), - format_hints.Hex(header.PsActiveProcessHead), - header.MachineImageType, - header.NumberProcessors, - format_hints.Hex(header.KdDebuggerDataBlock), - dump_type, - str(uptime), - utility.array_to_string(header.Comment), - conversion.wintime_to_datetime(header.SystemTime), - bitmap_header_size, - bitmap_size, - bitmap_pages, - )) + yield ( + 0, + ( + utility.array_to_string(header.Signature), + header.MajorVersion, + header.MinorVersion, + format_hints.Hex(header.DirectoryTableBase), + format_hints.Hex(header.PfnDataBase), + format_hints.Hex(header.PsLoadedModuleList), + format_hints.Hex(header.PsActiveProcessHead), + header.MachineImageType, + header.NumberProcessors, + format_hints.Hex(header.KdDebuggerDataBlock), + dump_type, + str(uptime), + utility.array_to_string(header.Comment), + conversion.wintime_to_datetime(header.SystemTime), + bitmap_header_size, + bitmap_size, + bitmap_pages, + ), + ) def run(self): crash_layer = None @@ -76,22 +85,25 @@ class Crashinfo(interfaces.plugins.PluginInterface): vollog.error("This plugin requires a Windows crash dump") raise - return renderers.TreeGrid([ - ("Signature", str), - ("MajorVersion", int), - ("MinorVersion", int), - ("DirectoryTableBase", format_hints.Hex), - ("PfnDataBase", format_hints.Hex), - ("PsLoadedModuleList", format_hints.Hex), - ("PsActiveProcessHead", format_hints.Hex), - ("MachineImageType", int), - ("NumberProcessors", int), - ("KdDebuggerDataBlock", format_hints.Hex), - ("DumpType", str), - ("SystemUpTime", str), - ("Comment", str), - ("SystemTime", datetime.datetime), - ("BitmapHeaderSize", format_hints.Hex), - ("BitmapSize", format_hints.Hex), - ("BitmapPages", format_hints.Hex), - ], self._generator(crash_layer)) + return renderers.TreeGrid( + [ + ("Signature", str), + ("MajorVersion", int), + ("MinorVersion", int), + ("DirectoryTableBase", format_hints.Hex), + ("PfnDataBase", format_hints.Hex), + ("PsLoadedModuleList", format_hints.Hex), + ("PsActiveProcessHead", format_hints.Hex), + ("MachineImageType", int), + ("NumberProcessors", int), + ("KdDebuggerDataBlock", format_hints.Hex), + ("DumpType", str), + ("SystemUpTime", str), + ("Comment", str), + ("SystemTime", datetime.datetime), + ("BitmapHeaderSize", format_hints.Hex), + ("BitmapSize", format_hints.Hex), + ("BitmapPages", format_hints.Hex), + ], + self._generator(crash_layer), + ) diff --git a/volatility3/framework/plugins/windows/devicetree.py b/volatility3/framework/plugins/windows/devicetree.py index 8e92de0cc..6f39799c1 100644 --- a/volatility3/framework/plugins/windows/devicetree.py +++ b/volatility3/framework/plugins/windows/devicetree.py @@ -12,156 +12,190 @@ from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import driverscan DEVICE_CODES = { - 0x00000027 : "FILE_DEVICE_8042_PORT", - 0x00000032 : "FILE_DEVICE_ACPI", - 0x00000029 : "FILE_DEVICE_BATTERY", - 0x00000001 : "FILE_DEVICE_BEEP", - 0x0000002a : "FILE_DEVICE_BUS_EXTENDER", - 0x00000002 : "FILE_DEVICE_CD_ROM", - 0x00000003 : "FILE_DEVICE_CD_ROM_FILE_SYSTEM", - 0x00000030 : "FILE_DEVICE_CHANGER", - 0x00000004 : "FILE_DEVICE_CONTROLLER", - 0x00000005 : "FILE_DEVICE_DATALINK", - 0x00000006 : "FILE_DEVICE_DFS", - 0x00000035 : "FILE_DEVICE_DFS_FILE_SYSTEM", - 0x00000036 : "FILE_DEVICE_DFS_VOLUME", - 0x00000007 : "FILE_DEVICE_DISK", - 0x00000008 : "FILE_DEVICE_DISK_FILE_SYSTEM", - 0x00000033 : "FILE_DEVICE_DVD", - 0x00000009 : "FILE_DEVICE_FILE_SYSTEM", - 0x0000003a : "FILE_DEVICE_FIPS", - 0x00000034 : "FILE_DEVICE_FULLSCREEN_VIDEO", - 0x0000000a : "FILE_DEVICE_INPORT_PORT", - 0x0000000b : "FILE_DEVICE_KEYBOARD", - 0x0000002f : "FILE_DEVICE_KS", - 0x00000039 : "FILE_DEVICE_KSEC", - 0x0000000c : "FILE_DEVICE_MAILSLOT", - 0x0000002d : "FILE_DEVICE_MASS_STORAGE", - 0x0000000d : "FILE_DEVICE_MIDI_IN", - 0x0000000e : "FILE_DEVICE_MIDI_OUT", - 0x0000002b : "FILE_DEVICE_MODEM", - 0x0000000f : "FILE_DEVICE_MOUSE", - 0x00000010 : "FILE_DEVICE_MULTI_UNC_PROVIDER", - 0x00000011 : "FILE_DEVICE_NAMED_PIPE", - 0x00000012 : "FILE_DEVICE_NETWORK", - 0x00000013 : "FILE_DEVICE_NETWORK_BROWSER", - 0x00000014 : "FILE_DEVICE_NETWORK_FILE_SYSTEM", - 0x00000028 : "FILE_DEVICE_NETWORK_REDIRECTOR", - 0x00000015 : "FILE_DEVICE_NULL", - 0x00000016 : "FILE_DEVICE_PARALLEL_PORT", - 0x00000017 : "FILE_DEVICE_PHYSICAL_NETCARD", - 0x00000018 : "FILE_DEVICE_PRINTER", - 0x00000019 : "FILE_DEVICE_SCANNER", - 0x0000001c : "FILE_DEVICE_SCREEN", - 0x00000037 : "FILE_DEVICE_SERENUM", - 0x0000001a : "FILE_DEVICE_SERIAL_MOUSE_PORT", - 0x0000001b : "FILE_DEVICE_SERIAL_PORT", - 0x00000031 : "FILE_DEVICE_SMARTCARD", - 0x0000002e : "FILE_DEVICE_SMB", - 0x0000001d : "FILE_DEVICE_SOUND", - 0x0000001e : "FILE_DEVICE_STREAMS", - 0x0000001f : "FILE_DEVICE_TAPE", - 0x00000020 : "FILE_DEVICE_TAPE_FILE_SYSTEM", - 0x00000038 : "FILE_DEVICE_TERMSRV", - 0x00000021 : "FILE_DEVICE_TRANSPORT", - 0x00000022 : "FILE_DEVICE_UNKNOWN", - 0x0000002c : "FILE_DEVICE_VDM", - 0x00000023 : "FILE_DEVICE_VIDEO", - 0x00000024 : "FILE_DEVICE_VIRTUAL_DISK", - 0x00000025 : "FILE_DEVICE_WAVE_IN", - 0x00000026 : "FILE_DEVICE_WAVE_OUT", + 0x00000027: "FILE_DEVICE_8042_PORT", + 0x00000032: "FILE_DEVICE_ACPI", + 0x00000029: "FILE_DEVICE_BATTERY", + 0x00000001: "FILE_DEVICE_BEEP", + 0x0000002A: "FILE_DEVICE_BUS_EXTENDER", + 0x00000002: "FILE_DEVICE_CD_ROM", + 0x00000003: "FILE_DEVICE_CD_ROM_FILE_SYSTEM", + 0x00000030: "FILE_DEVICE_CHANGER", + 0x00000004: "FILE_DEVICE_CONTROLLER", + 0x00000005: "FILE_DEVICE_DATALINK", + 0x00000006: "FILE_DEVICE_DFS", + 0x00000035: "FILE_DEVICE_DFS_FILE_SYSTEM", + 0x00000036: "FILE_DEVICE_DFS_VOLUME", + 0x00000007: "FILE_DEVICE_DISK", + 0x00000008: "FILE_DEVICE_DISK_FILE_SYSTEM", + 0x00000033: "FILE_DEVICE_DVD", + 0x00000009: "FILE_DEVICE_FILE_SYSTEM", + 0x0000003A: "FILE_DEVICE_FIPS", + 0x00000034: "FILE_DEVICE_FULLSCREEN_VIDEO", + 0x0000000A: "FILE_DEVICE_INPORT_PORT", + 0x0000000B: "FILE_DEVICE_KEYBOARD", + 0x0000002F: "FILE_DEVICE_KS", + 0x00000039: "FILE_DEVICE_KSEC", + 0x0000000C: "FILE_DEVICE_MAILSLOT", + 0x0000002D: "FILE_DEVICE_MASS_STORAGE", + 0x0000000D: "FILE_DEVICE_MIDI_IN", + 0x0000000E: "FILE_DEVICE_MIDI_OUT", + 0x0000002B: "FILE_DEVICE_MODEM", + 0x0000000F: "FILE_DEVICE_MOUSE", + 0x00000010: "FILE_DEVICE_MULTI_UNC_PROVIDER", + 0x00000011: "FILE_DEVICE_NAMED_PIPE", + 0x00000012: "FILE_DEVICE_NETWORK", + 0x00000013: "FILE_DEVICE_NETWORK_BROWSER", + 0x00000014: "FILE_DEVICE_NETWORK_FILE_SYSTEM", + 0x00000028: "FILE_DEVICE_NETWORK_REDIRECTOR", + 0x00000015: "FILE_DEVICE_NULL", + 0x00000016: "FILE_DEVICE_PARALLEL_PORT", + 0x00000017: "FILE_DEVICE_PHYSICAL_NETCARD", + 0x00000018: "FILE_DEVICE_PRINTER", + 0x00000019: "FILE_DEVICE_SCANNER", + 0x0000001C: "FILE_DEVICE_SCREEN", + 0x00000037: "FILE_DEVICE_SERENUM", + 0x0000001A: "FILE_DEVICE_SERIAL_MOUSE_PORT", + 0x0000001B: "FILE_DEVICE_SERIAL_PORT", + 0x00000031: "FILE_DEVICE_SMARTCARD", + 0x0000002E: "FILE_DEVICE_SMB", + 0x0000001D: "FILE_DEVICE_SOUND", + 0x0000001E: "FILE_DEVICE_STREAMS", + 0x0000001F: "FILE_DEVICE_TAPE", + 0x00000020: "FILE_DEVICE_TAPE_FILE_SYSTEM", + 0x00000038: "FILE_DEVICE_TERMSRV", + 0x00000021: "FILE_DEVICE_TRANSPORT", + 0x00000022: "FILE_DEVICE_UNKNOWN", + 0x0000002C: "FILE_DEVICE_VDM", + 0x00000023: "FILE_DEVICE_VIDEO", + 0x00000024: "FILE_DEVICE_VIRTUAL_DISK", + 0x00000025: "FILE_DEVICE_WAVE_IN", + 0x00000026: "FILE_DEVICE_WAVE_OUT", } vollog = logging.getLogger(__name__) + class DeviceTree(interfaces.plugins.PluginInterface): """Listing tree based on drivers and attached devices in a particular windows memory image.""" _required_framework_version = (2, 0, 3) - _version = (1, 0, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = "kernel", description = "Windows kernel", - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = "driverscan", plugin = driverscan.DriverScan, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) + ), ] def _generator(self) -> Iterator[Tuple]: kernel = self.context.modules[self.config["kernel"]] # Scan the Layer for drivers - for driver in driverscan.DriverScan.scan_drivers(self.context, kernel.layer_name, kernel.symbol_table_name): + for driver in driverscan.DriverScan.scan_drivers( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: try: driver_name = driver.get_driver_name() - except (ValueError, exceptions.PagedInvalidAddressException): - vollog.log(constants.LOGLEVEL_VVVV, - f"Failed to get Driver name : {driver.vol.offset:x}") + except (ValueError, exceptions.InvalidAddressException): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Failed to get Driver name : {driver.vol.offset:x}", + ) driver_name = renderers.UnparsableValue() - yield (0, ( - format_hints.Hex(driver.vol.offset), - "DRV", - driver_name, - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue() - )) + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + "DRV", + driver_name, + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + ), + ) # Scan to get the device information of driver. for device in driver.get_devices(): try: device_name = device.get_device_name() - except (ValueError, exceptions.PagedInvalidAddressException): - vollog.log(constants.LOGLEVEL_VVVV, - f"Failed to get Device name : {device.vol.offset:x}") + except (ValueError, exceptions.InvalidAddressException): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Failed to get Device name : {device.vol.offset:x}", + ) device_name = renderers.UnparsableValue() - + device_type = DEVICE_CODES.get(device.DeviceType, "UNKNOWN") - yield (1, ( - format_hints.Hex(driver.vol.offset), - "DEV", - driver_name, - device_name, - renderers.NotApplicableValue(), - device_type - )) - - # Scan to get the attached devices information of device. - for level, attached_device in enumerate(device.get_attached_devices(), start=2): - try: - device_name = attached_device.get_device_name() - except (ValueError, exceptions.PagedInvalidAddressException): - vollog.log(constants.LOGLEVEL_VVVV, - f"Failed to get Attached Device Name: {attached_device.vol.offset:x}") - device_name = renderers.UnparsableValue() - - attached_device_driver_name = attached_device.DriverObject.DriverName.get_string() - attached_device_type = DEVICE_CODES.get(attached_device.DeviceType, "UNKNOWN") - - yield (level, ( + yield ( + 1, + ( format_hints.Hex(driver.vol.offset), - "ATT", + "DEV", driver_name, device_name, - attached_device_driver_name, - attached_device_type - )) - - except(exceptions.PagedInvalidAddressException): - vollog.log(constants.LOGLEVEL_VVVV, - f"Invalid address identified in drivers and devices: {driver.vol.offset:x}") + renderers.NotApplicableValue(), + device_type, + ), + ) + + # Scan to get the attached devices information of device. + for level, attached_device in enumerate( + device.get_attached_devices(), start=2 + ): + try: + device_name = attached_device.get_device_name() + except (ValueError, exceptions.InvalidAddressException): + vollog.log( + constants.LOGLEVEL_VVVV, + f"Failed to get Attached Device Name: {attached_device.vol.offset:x}", + ) + device_name = renderers.UnparsableValue() + + attached_device_driver_name = ( + attached_device.DriverObject.DriverName.get_string() + ) + attached_device_type = DEVICE_CODES.get( + attached_device.DeviceType, "UNKNOWN" + ) + + yield ( + level, + ( + format_hints.Hex(driver.vol.offset), + "ATT", + driver_name, + device_name, + attached_device_driver_name, + attached_device_type, + ), + ) + + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVVV, + f"Invalid address identified in drivers and devices: {driver.vol.offset:x}", + ) continue def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("Type", str), - ("DriverName", str), - ("DeviceName", str), - ("DriverNameOfAttDevice", str), - ("DeviceType", str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Type", str), + ("DriverName", str), + ("DeviceName", str), + ("DriverNameOfAttDevice", str), + ("DeviceType", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/dlllist.py b/volatility3/framework/plugins/windows/dlllist.py index 2fd7deeaf..d73cea652 100644 --- a/volatility3/framework/plugins/windows/dlllist.py +++ b/volatility3/framework/plugins/windows/dlllist.py @@ -1,18 +1,19 @@ # 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 datetime import logging import ntpath from typing import List, Optional, Type -from volatility3.framework import exceptions, renderers, interfaces, constants +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints, conversion +from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins import timeliner -from volatility3.plugins.windows import pslist, info +from volatility3.plugins.windows import info, pslist vollog = logging.getLogger(__name__) @@ -27,28 +28,41 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'info', component = info.Info, version = (1, 0, 0)), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process IDs to include (all other processes are excluded)", - optional = True), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed DLLs", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(1, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed DLLs", + default=False, + optional=True, + ), ] @classmethod - def dump_pe(cls, - context: interfaces.context.ContextInterface, - pe_table_name: str, - dll_entry: interfaces.objects.ObjectInterface, - open_method: Type[interfaces.plugins.FileHandlerInterface], - layer_name: str = None, - prefix: str = '') -> Optional[interfaces.plugins.FileHandlerInterface]: + def dump_pe( + cls, + context: interfaces.context.ContextInterface, + pe_table_name: str, + dll_entry: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + layer_name: str = None, + prefix: str = "", + ) -> Optional[interfaces.plugins.FileHandlerInterface]: """Extracts the complete data for a process as a FileInterface Args: @@ -60,78 +74,93 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): Returns: An open FileHandlerInterface object containing the complete data for the DLL or None in the case of failure - """ + """ try: try: name = dll_entry.FullDllName.get_string() except exceptions.InvalidAddressException: - name = 'UnreadableDLLName' + name = "UnreadableDLLName" if layer_name is None: layer_name = dll_entry.vol.layer_name - file_handle = open_method("{}{}.{:#x}.{:#x}.dmp".format(prefix, ntpath.basename(name), dll_entry.vol.offset, - dll_entry.DllBase)) + file_handle = open_method( + "{}{}.{:#x}.{:#x}.dmp".format( + prefix, + ntpath.basename(name), + dll_entry.vol.offset, + dll_entry.DllBase, + ) + ) - dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = dll_entry.DllBase, - layer_name = layer_name) + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=dll_entry.DllBase, + layer_name=layer_name, + ) for offset, data in dos_header.reconstruct(): file_handle.seek(offset) file_handle.write(data) - except (IOError, exceptions.VolatilityException, OverflowError, ValueError) as excp: + except ( + IOError, + exceptions.VolatilityException, + OverflowError, + ValueError, + ) as excp: vollog.debug(f"Unable to dump dll at offset {dll_entry.DllBase}: {excp}") return None return file_handle def _generator(self, procs): - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - kuser = info.Info.get_kuser_structure(self.context, kernel.layer_name, kernel.symbol_table_name) + kuser = info.Info.get_kuser_structure( + self.context, kernel.layer_name, kernel.symbol_table_name + ) nt_major_version = int(kuser.NtMajorVersion) nt_minor_version = int(kuser.NtMinorVersion) # LoadTime only applies to versions higher or equal to Window 7 (6.1 and higher) - dll_load_time_field = (nt_major_version > 6) or (nt_major_version == 6 and nt_minor_version >= 1) + dll_load_time_field = (nt_major_version > 6) or ( + nt_major_version == 6 and nt_minor_version >= 1 + ) for proc in procs: - proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() for entry in proc.load_order_modules(): - BaseDllName = FullDllName = renderers.UnreadableValue() - try: + with contextlib.suppress(exceptions.InvalidAddressException): BaseDllName = entry.BaseDllName.get_string() # We assume that if the BaseDllName points to an invalid buffer, so will FullDllName FullDllName = entry.FullDllName.get_string() - except exceptions.InvalidAddressException: - pass if dll_load_time_field: # Versions prior to 6.1 won't have the LoadTime attribute # and 32bit version shouldn't have the Quadpart according to MSDN try: - DllLoadTime = conversion.wintime_to_datetime(entry.LoadTime.QuadPart) + DllLoadTime = conversion.wintime_to_datetime( + entry.LoadTime.QuadPart + ) except exceptions.InvalidAddressException: DllLoadTime = renderers.UnreadableValue() else: DllLoadTime = renderers.NotApplicableValue() file_output = "Disabled" - if self.config['dump']: - file_handle = self.dump_pe(self.context, - pe_table_name, - entry, - self.open, - proc_layer_name, - prefix = f"pid.{proc_id}.") + if self.config["dump"]: + file_handle = self.dump_pe( + self.context, + pe_table_name, + entry, + self.open, + proc_layer_name, + prefix=f"pid.{proc_id}.", + ) file_output = "Error outputting file" if file_handle: file_handle.close() @@ -146,34 +175,69 @@ class DllList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except exceptions.InvalidAddressException: size_of_image = renderers.NotAvailableValue() - yield (0, (proc.UniqueProcessId, - proc.ImageFileName.cast("string", - max_length = proc.ImageFileName.vol.count, - errors = 'replace'), dllbase, size_of_image, BaseDllName, - FullDllName, DllLoadTime, file_output)) + yield ( + 0, + ( + proc.UniqueProcessId, + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ), + dllbase, + size_of_image, + BaseDllName, + FullDllName, + DllLoadTime, + file_output, + ), + ) def generate_timeline(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] for row in self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name)): + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) + ): _depth, row_data = row if not isinstance(row_data[6], datetime.datetime): continue - description = "DLL Load: Process {} {} Loaded {} ({}) Size {} Offset {}".format( - row_data[0], row_data[1], row_data[4], row_data[5], row_data[3], row_data[2]) + description = ( + "DLL Load: Process {} {} Loaded {} ({}) Size {} Offset {}".format( + row_data[0], + row_data[1], + row_data[4], + row_data[5], + row_data[3], + row_data[2], + ) + ) yield (description, timeliner.TimeLinerType.CREATED, row_data[6]) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - kernel = self.context.modules[self.config['kernel']] + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid([("PID", int), ("Process", str), ("Base", format_hints.Hex), - ("Size", format_hints.Hex), ("Name", str), ("Path", str), - ("LoadTime", datetime.datetime), ("File output", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Base", format_hints.Hex), + ("Size", format_hints.Hex), + ("Name", str), + ("Path", str), + ("LoadTime", datetime.datetime), + ("File output", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/driverirp.py b/volatility3/framework/plugins/windows/driverirp.py index 7f9bc6b08..b5cd33db7 100644 --- a/volatility3/framework/plugins/windows/driverirp.py +++ b/volatility3/framework/plugins/windows/driverirp.py @@ -9,13 +9,34 @@ from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import ssdt, driverscan MAJOR_FUNCTIONS = [ - 'IRP_MJ_CREATE', 'IRP_MJ_CREATE_NAMED_PIPE', 'IRP_MJ_CLOSE', 'IRP_MJ_READ', 'IRP_MJ_WRITE', - 'IRP_MJ_QUERY_INFORMATION', 'IRP_MJ_SET_INFORMATION', 'IRP_MJ_QUERY_EA', 'IRP_MJ_SET_EA', 'IRP_MJ_FLUSH_BUFFERS', - 'IRP_MJ_QUERY_VOLUME_INFORMATION', 'IRP_MJ_SET_VOLUME_INFORMATION', 'IRP_MJ_DIRECTORY_CONTROL', - 'IRP_MJ_FILE_SYSTEM_CONTROL', 'IRP_MJ_DEVICE_CONTROL', 'IRP_MJ_INTERNAL_DEVICE_CONTROL', 'IRP_MJ_SHUTDOWN', - 'IRP_MJ_LOCK_CONTROL', 'IRP_MJ_CLEANUP', 'IRP_MJ_CREATE_MAILSLOT', 'IRP_MJ_QUERY_SECURITY', 'IRP_MJ_SET_SECURITY', - 'IRP_MJ_POWER', 'IRP_MJ_SYSTEM_CONTROL', 'IRP_MJ_DEVICE_CHANGE', 'IRP_MJ_QUERY_QUOTA', 'IRP_MJ_SET_QUOTA', - 'IRP_MJ_PNP' + "IRP_MJ_CREATE", + "IRP_MJ_CREATE_NAMED_PIPE", + "IRP_MJ_CLOSE", + "IRP_MJ_READ", + "IRP_MJ_WRITE", + "IRP_MJ_QUERY_INFORMATION", + "IRP_MJ_SET_INFORMATION", + "IRP_MJ_QUERY_EA", + "IRP_MJ_SET_EA", + "IRP_MJ_FLUSH_BUFFERS", + "IRP_MJ_QUERY_VOLUME_INFORMATION", + "IRP_MJ_SET_VOLUME_INFORMATION", + "IRP_MJ_DIRECTORY_CONTROL", + "IRP_MJ_FILE_SYSTEM_CONTROL", + "IRP_MJ_DEVICE_CONTROL", + "IRP_MJ_INTERNAL_DEVICE_CONTROL", + "IRP_MJ_SHUTDOWN", + "IRP_MJ_LOCK_CONTROL", + "IRP_MJ_CLEANUP", + "IRP_MJ_CREATE_MAILSLOT", + "IRP_MJ_QUERY_SECURITY", + "IRP_MJ_SET_SECURITY", + "IRP_MJ_POWER", + "IRP_MJ_SYSTEM_CONTROL", + "IRP_MJ_DEVICE_CHANGE", + "IRP_MJ_QUERY_QUOTA", + "IRP_MJ_SET_QUOTA", + "IRP_MJ_PNP", ] @@ -27,46 +48,78 @@ class DriverIrp(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'ssdt', plugin = ssdt.SSDT, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'driverscan', plugin = driverscan.DriverScan, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - collection = ssdt.SSDT.build_module_collection(self.context, kernel.layer_name, kernel.symbol_table_name) - - for driver in driverscan.DriverScan.scan_drivers(self.context, kernel.layer_name, kernel.symbol_table_name): + collection = ssdt.SSDT.build_module_collection( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + for driver in driverscan.DriverScan.scan_drivers( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: driver_name = driver.get_driver_name() except (ValueError, exceptions.InvalidAddressException): driver_name = renderers.NotApplicableValue() for i, address in enumerate(driver.MajorFunction): - module_symbols = collection.get_module_symbols_by_absolute_location(address) + module_symbols = collection.get_module_symbols_by_absolute_location( + address + ) for module_name, symbol_generator in module_symbols: symbols_found = False for symbol in symbol_generator: symbols_found = True - yield (0, (format_hints.Hex(driver.vol.offset), driver_name, MAJOR_FUNCTIONS[i], - format_hints.Hex(address), module_name, symbol.split(constants.BANG)[1])) + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + driver_name, + MAJOR_FUNCTIONS[i], + format_hints.Hex(address), + module_name, + symbol.split(constants.BANG)[1], + ), + ) if not symbols_found: - yield (0, (format_hints.Hex(driver.vol.offset), driver_name, MAJOR_FUNCTIONS[i], - format_hints.Hex(address), module_name, renderers.NotAvailableValue())) + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + driver_name, + MAJOR_FUNCTIONS[i], + format_hints.Hex(address), + module_name, + renderers.NotAvailableValue(), + ), + ) def run(self): - - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("Driver Name", str), - ("IRP", str), - ("Address", format_hints.Hex), - ("Module", str), - ("Symbol", str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Driver Name", str), + ("IRP", str), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/drivermodule.py b/volatility3/framework/plugins/windows/drivermodule.py new file mode 100644 index 000000000..de827602e --- /dev/null +++ b/volatility3/framework/plugins/windows/drivermodule.py @@ -0,0 +1,85 @@ +# 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 +# +from typing import Iterator, List, Tuple +from volatility3.framework import renderers, interfaces +from volatility3.framework.configuration import requirements +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import ssdt, driverscan + +# built in Windows-components that trigger false positives +KNOWN_DRIVERS = ["ACPI_HAL", "PnpManager", "RAW", "WMIxWDM", "Win32k", "Fs_Rec"] + + +class DriverModule(interfaces.plugins.PluginInterface): + """Determines if any loaded drivers were hidden by a rootkit""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="ssdt", plugin=ssdt.SSDT, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="driverscan", plugin=driverscan.DriverScan, version=(1, 0, 0) + ), + ] + + def _generator(self) -> Iterator[Tuple]: + """ + Attempt to match each driver's start code address to a known kernel module + A common rootkit technique is to register drivers from modules that are hidden, + which allows us to detect the disconnect between a malicious driver and its hidden module. + """ + kernel = self.context.modules[self.config["kernel"]] + + collection = ssdt.SSDT.build_module_collection( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + + for driver in driverscan.DriverScan.scan_drivers( + self.context, kernel.layer_name, kernel.symbol_table_name + ): + # we do not care about actual symbol names, we just want to know if the driver points to a known module + module_symbols = list( + collection.get_module_symbols_by_absolute_location(driver.DriverStart) + ) + if not module_symbols: + ( + driver_name, + service_key, + name, + ) = driverscan.DriverScan.get_names_for_driver(driver) + + known_exception = driver_name in KNOWN_DRIVERS + + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + known_exception, + driver_name, + service_key, + name, + ), + ) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Known Exception", bool), + ("Driver Name", str), + ("Service Key", str), + ("Alternative Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/driverscan.py b/volatility3/framework/plugins/windows/driverscan.py index 2cf309014..24d81c3d5 100644 --- a/volatility3/framework/plugins/windows/driverscan.py +++ b/volatility3/framework/plugins/windows/driverscan.py @@ -19,17 +19,23 @@ class DriverScan(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + ), ] @classmethod - def scan_drivers(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_drivers( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for drivers using the poolscanner module and constraints. Args: @@ -41,37 +47,74 @@ class DriverScan(interfaces.plugins.PluginInterface): A list of Driver objects as found from the `layer_name` layer based on Driver pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'Dri\xf6', b'Driv']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"Dri\xf6", b"Driv"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object + @classmethod + def get_names_for_driver(cls, driver): + """ + Convenience method for getting the commonly used + names associated with a driver + + Args: + driver: A Eriver object + + Returns: + A tuple of strings of (driver name, service key, driver alt. name) + """ + try: + driver_name = driver.get_driver_name() + except (ValueError, exceptions.InvalidAddressException): + driver_name = renderers.NotApplicableValue() + + try: + service_key = driver.DriverExtension.ServiceKeyName.String + except exceptions.InvalidAddressException: + service_key = renderers.NotApplicableValue() + + try: + name = driver.DriverName.String + except exceptions.InvalidAddressException: + name = renderers.NotApplicableValue() + + return driver_name, service_key, name + def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - for driver in self.scan_drivers(self.context, kernel.layer_name, kernel.symbol_table_name): + for driver in self.scan_drivers( + self.context, kernel.layer_name, kernel.symbol_table_name + ): + driver_name, service_key, name = self.get_names_for_driver(driver) - try: - driver_name = driver.get_driver_name() - except (ValueError, exceptions.InvalidAddressException): - driver_name = renderers.NotApplicableValue() - - try: - service_key = driver.DriverExtension.ServiceKeyName.String - except exceptions.InvalidAddressException: - service_key = renderers.NotApplicableValue() - - try: - name = driver.DriverName.String - except exceptions.InvalidAddressException: - name = renderers.NotApplicableValue() - - yield (0, (format_hints.Hex(driver.vol.offset), format_hints.Hex(driver.DriverStart), - format_hints.Hex(driver.DriverSize), service_key, driver_name, name)) + yield ( + 0, + ( + format_hints.Hex(driver.vol.offset), + format_hints.Hex(driver.DriverStart), + format_hints.Hex(driver.DriverSize), + service_key, + driver_name, + name, + ), + ) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), - ("Start", format_hints.Hex), ("Size", format_hints.Hex), ("Service Key", str), - ("Driver Name", str), ("Name", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Start", format_hints.Hex), + ("Size", format_hints.Hex), + ("Service Key", str), + ("Driver Name", str), + ("Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/dumpfiles.py b/volatility3/framework/plugins/windows/dumpfiles.py index 58166ee7f..48539c752 100755 --- a/volatility3/framework/plugins/windows/dumpfiles.py +++ b/volatility3/framework/plugins/windows/dumpfiles.py @@ -4,10 +4,12 @@ import logging import ntpath +import re from typing import List, Tuple, Type, Optional, Generator + from volatility3.framework import interfaces, renderers, exceptions, constants from volatility3.framework.configuration import requirements -from volatility3.framework.renderers import format_hints +from volatility3.framework.renderers import format_hints, UnreadableValue from volatility3.plugins.windows import handles from volatility3.plugins.windows import pslist @@ -32,27 +34,54 @@ class DumpFiles(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.IntRequirement(name = 'pid', - description = "Process ID to include (all other processes are excluded)", - optional = True), - requirements.IntRequirement(name = 'virtaddr', - description = "Dump a single _FILE_OBJECT at this virtual address", - optional = True), - requirements.IntRequirement(name = 'physaddr', - description = "Dump a single _FILE_OBJECT at this physical address", - optional = True), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'handles', component = handles.Handles, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.IntRequirement( + name="pid", + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + requirements.IntRequirement( + name="virtaddr", + description="Dump a single _FILE_OBJECT at this virtual address", + optional=True, + ), + requirements.IntRequirement( + name="physaddr", + description="Dump a single _FILE_OBJECT at this physical address", + optional=True, + ), + requirements.StringRequirement( + name="filter", + description="Dump files matching regular expression FILTER", + optional=True, + ), + requirements.BooleanRequirement( + name="ignore-case", + description="Ignore case in filter match", + default=False, + optional=True, + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="handles", component=handles.Handles, version=(1, 0, 0) + ), ] @classmethod - def dump_file_producer(cls, file_object: interfaces.objects.ObjectInterface, - memory_object: interfaces.objects.ObjectInterface, - open_method: Type[interfaces.plugins.FileHandlerInterface], - layer: interfaces.layers.DataLayerInterface, - desired_file_name: str) -> Optional[interfaces.plugins.FileHandlerInterface]: + def dump_file_producer( + cls, + file_object: interfaces.objects.ObjectInterface, + memory_object: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + layer: interfaces.layers.DataLayerInterface, + desired_file_name: str, + ) -> Optional[interfaces.plugins.FileHandlerInterface]: """Produce a file from the memory object's get_available_pages() interface. :param file_object: the parent _FILE_OBJECT @@ -63,34 +92,39 @@ class DumpFiles(interfaces.plugins.PluginInterface): :return: result status """ filedata = open_method(desired_file_name) - try: - # Description of these variables: - # memoffset: offset in the specified layer where the page begins - # fileoffset: write to this offset in the destination file - # datasize: size of the page + # Description of these variables: + # memoffset: offset in the specified layer where the page begins + # fileoffset: write to this offset in the destination file + # datasize: size of the page - # track number of bytes written so we don't write empty files to disk - bytes_written = 0 + # track number of bytes written so we don't write empty files to disk + bytes_written = 0 + try: for memoffset, fileoffset, datasize in memory_object.get_available_pages(): - data = layer.read(memoffset, datasize, pad = True) + data = layer.read(memoffset, datasize, pad=True) bytes_written += len(data) filedata.seek(fileoffset) filedata.write(data) - - if not bytes_written: - vollog.debug(f"No data is cached for the file at {file_object.vol.offset:#x}") - return None - else: - vollog.debug(f"Stored {filedata.preferred_filename}") - return filedata except exceptions.InvalidAddressException: vollog.debug(f"Unable to dump file at {file_object.vol.offset:#x}") return None + if not bytes_written: + vollog.debug( + f"No data is cached for the file at {file_object.vol.offset:#x}" + ) + return None + + vollog.debug(f"Stored {filedata.preferred_filename}") + return filedata @classmethod - def process_file_object(cls, context: interfaces.context.ContextInterface, primary_layer_name: str, - open_method: Type[interfaces.plugins.FileHandlerInterface], - file_obj: interfaces.objects.ObjectInterface) -> Generator[Tuple, None, None]: + def process_file_object( + cls, + context: interfaces.context.ContextInterface, + primary_layer_name: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], + file_obj: interfaces.objects.ObjectInterface, + ) -> Generator[Tuple, None, None]: """Given a FILE_OBJECT, dump data to separate files for each of the three file caches. :param context: the context to operate upon @@ -98,17 +132,21 @@ class DumpFiles(interfaces.plugins.PluginInterface): :param open_method: class for constructing output files :param file_obj: the FILE_OBJECT """ - # Filtering by these types of devices prevents us from processing other types of devices that # use the "File" object type, such as \Device\Tcp and \Device\NamedPipe. - if file_obj.DeviceObject.DeviceType not in [FILE_DEVICE_DISK, FILE_DEVICE_NETWORK_FILE_SYSTEM]: - vollog.log(constants.LOGLEVEL_VVV, - f"The file object at {file_obj.vol.offset:#x} is not a file on disk") - return + if file_obj.DeviceObject.DeviceType not in [ + FILE_DEVICE_DISK, + FILE_DEVICE_NETWORK_FILE_SYSTEM, + ]: + vollog.log( + constants.LOGLEVEL_VVV, + f"The file object at {file_obj.vol.offset:#x} is not a file on disk", + ) + return None # Depending on the type of object (DataSection, ImageSection, SharedCacheMap) we may need to # read from the memory layer or the primary layer. - memory_layer_name = context.layers[primary_layer_name].config['memory_layer'] + memory_layer_name = context.layers[primary_layer_name].config["memory_layer"] memory_layer = context.layers[memory_layer_name] primary_layer = context.layers[primary_layer_name] @@ -120,19 +158,24 @@ class DumpFiles(interfaces.plugins.PluginInterface): # layer to read from, # file extension to apply, # ) - dump_parameters = [] + dump_parameters = list() # The DataSectionObject and ImageSectionObject caches are handled in basically the same way. # We carve these "pages" from the memory_layer. - for member_name, extension in [("DataSectionObject", "dat"), ("ImageSectionObject", "img")]: + for member_name, extension in [ + ("DataSectionObject", "dat"), + ("ImageSectionObject", "img"), + ]: try: section_obj = getattr(file_obj.SectionObjectPointer, member_name) control_area = section_obj.dereference().cast("_CONTROL_AREA") if control_area.is_valid(): dump_parameters.append((control_area, memory_layer, extension)) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"{member_name} is unavailable for file {file_obj.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"{member_name} is unavailable for file {file_obj.vol.offset:#x}", + ) # The SharedCacheMap is handled differently than the caches above. # We carve these "pages" from the primary_layer. @@ -142,16 +185,24 @@ class DumpFiles(interfaces.plugins.PluginInterface): if shared_cache_map.is_valid(): dump_parameters.append((shared_cache_map, primary_layer, "vacb")) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"SharedCacheMap is unavailable for file {file_obj.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"SharedCacheMap is unavailable for file {file_obj.vol.offset:#x}", + ) for memory_object, layer, extension in dump_parameters: cache_name = EXTENSION_CACHE_MAP[extension] - desired_file_name = "file.{0:#x}.{1:#x}.{2}.{3}.{4}".format(file_obj.vol.offset, - memory_object.vol.offset, cache_name, - ntpath.basename(obj_name), extension) + desired_file_name = "file.{0:#x}.{1:#x}.{2}.{3}.{4}".format( + file_obj.vol.offset, + memory_object.vol.offset, + cache_name, + ntpath.basename(obj_name), + extension, + ) - file_handle = DumpFiles.dump_file_producer(file_obj, memory_object, open_method, layer, desired_file_name) + file_handle = cls.dump_file_producer( + file_obj, memory_object, open_method, layer, desired_file_name + ) file_output = "Error dumping file" if file_handle: @@ -161,32 +212,46 @@ class DumpFiles(interfaces.plugins.PluginInterface): yield ( cache_name, format_hints.Hex(file_obj.vol.offset), - ntpath.basename(obj_name), # temporary, so its easier to visualize output - file_output) + ntpath.basename( + obj_name + ), # temporary, so its easier to visualize output + file_output, + ) def _generator(self, procs: List, offsets: List): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] + file_re = None + if self.config["filter"]: + flags = re.I if self.config["ignore-case"] else 0 + file_re = re.compile(self.config["filter"], flags) if procs: # The handles plugin doesn't expose any staticmethod/classmethod, and it also requires stashing # private variables, so we need an instance (for now, anyway). We _could_ call Handles._generator() # to do some of the other work that is duplicated here, but then we'd need to parse the TreeGrid # results instead of just dealing with them as direct objects here. - handles_plugin = handles.Handles(context = self.context, config_path = self._config_path) - type_map = handles_plugin.get_type_map(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name) - cookie = handles_plugin.find_cookie(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name) + handles_plugin = handles.Handles( + context=self.context, config_path=self._config_path + ) + type_map = handles_plugin.get_type_map( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) + cookie = handles_plugin.find_cookie( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) for proc in procs: - try: object_table = proc.ObjectTable except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _EPROCESS.ObjectTable at {proc.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _EPROCESS.ObjectTable at {proc.vol.offset:#x}", + ) continue for entry in handles_plugin.handles(object_table): @@ -194,12 +259,23 @@ class DumpFiles(interfaces.plugins.PluginInterface): obj_type = entry.get_object_type(type_map, cookie) if obj_type == "File": file_obj = entry.Body.cast("_FILE_OBJECT") - for result in self.process_file_object(self.context, kernel.layer_name, self.open, - file_obj): + + if file_re: + name = file_obj.file_name_with_device() + if isinstance(name, UnreadableValue): + continue + if not file_re.search(name): + continue + + for result in self.process_file_object( + self.context, kernel.layer_name, self.open, file_obj + ): yield (0, result) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot extract file from _OBJECT_HEADER at {entry.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot extract file from _OBJECT_HEADER at {entry.vol.offset:#x}", + ) # Pull file objects from the VADs. This will produce DLLs and EXEs that are # mapped into the process as images, but that the process doesn't have an @@ -211,19 +287,31 @@ class DumpFiles(interfaces.plugins.PluginInterface): file_obj = vad.ControlArea.FilePointer.dereference() elif vad.has_member("Subsection"): # Vista and beyond - file_obj = vad.Subsection.ControlArea.FilePointer.dereference().cast("_FILE_OBJECT") + file_obj = vad.Subsection.ControlArea.FilePointer.dereference().cast( + "_FILE_OBJECT" + ) else: continue if not file_obj.is_valid(): continue - for result in self.process_file_object(self.context, kernel.layer_name, self.open, - file_obj): + if file_re: + name = file_obj.file_name_with_device() + if isinstance(name, UnreadableValue): + continue + if not file_re.search(name): + continue + + for result in self.process_file_object( + self.context, kernel.layer_name, self.open, file_obj + ): yield (0, result) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot extract file from VAD at {vad.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot extract file from VAD at {vad.vol.offset:#x}", + ) elif offsets: # Now process any offsets explicitly requested by the user. @@ -232,35 +320,58 @@ class DumpFiles(interfaces.plugins.PluginInterface): layer_name = kernel.layer_name # switch to a memory layer if the user provided --physaddr instead of --virtaddr if not is_virtual: - layer_name = self.context.layers[layer_name].config["memory_layer"] + layer_name = self.context.layers[layer_name].config[ + "memory_layer" + ] file_obj = self.context.object( kernel.symbol_table_name + constants.BANG + "_FILE_OBJECT", - layer_name = layer_name, - native_layer_name = kernel.layer_name, - offset = offset) - for result in self.process_file_object(self.context, kernel.layer_name, self.open, file_obj): + layer_name=layer_name, + native_layer_name=kernel.layer_name, + offset=offset, + ) + for result in self.process_file_object( + self.context, kernel.layer_name, self.open, file_obj + ): yield (0, result) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, f"Cannot extract file at {offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, f"Cannot extract file at {offset:#x}" + ) def run(self): # a list of tuples (, ) where is the address and is True for virtual. - offsets = [] + offsets = list() # a list of processes matching the pid filter. all files for these process(es) will be dumped. - procs = [] - kernel = self.context.modules[self.config['kernel']] + procs = list() + kernel = self.context.modules[self.config["kernel"]] + + if self.config["filter"] and ( + self.config["virtaddr"] or self.config["physaddr"] + ): + raise ValueError("Cannot use filter flag with an address flag") if self.config.get("virtaddr", None) is not None: offsets.append((self.config["virtaddr"], True)) elif self.config.get("physaddr", None) is not None: offsets.append((self.config["physaddr"], False)) else: - filter_func = pslist.PsList.create_pid_filter([self.config.get("pid", None)]) - procs = pslist.PsList.list_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = filter_func) + filter_func = pslist.PsList.create_pid_filter( + [self.config.get("pid", None)] + ) + procs = pslist.PsList.list_processes( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + filter_func=filter_func, + ) - return renderers.TreeGrid([("Cache", str), ("FileObject", format_hints.Hex), ("FileName", str), - ("Result", str)], self._generator(procs, offsets)) + return renderers.TreeGrid( + [ + ("Cache", str), + ("FileObject", format_hints.Hex), + ("FileName", str), + ("Result", str), + ], + self._generator(procs, offsets), + ) diff --git a/volatility3/framework/plugins/windows/envars.py b/volatility3/framework/plugins/windows/envars.py index 9791fa580..66db03c9c 100644 --- a/volatility3/framework/plugins/windows/envars.py +++ b/volatility3/framework/plugins/windows/envars.py @@ -1,9 +1,10 @@ # 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 contextlib import logging from typing import List -from volatility3.framework import renderers, interfaces, objects, exceptions, constants +from volatility3.framework import constants, exceptions, interfaces, objects, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers import registry from volatility3.plugins.windows import pslist @@ -22,17 +23,28 @@ class Envars(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), - requirements.BooleanRequirement(name = 'silent', - description = 'Suppress common and non-persistent variables', - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.BooleanRequirement( + name="silent", + description="Suppress common and non-persistent variables", + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] def _get_silent_vars(self) -> List[str]: @@ -46,65 +58,71 @@ class Envars(interfaces.plugins.PluginInterface): """ values = [] - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - for hive in hivelist.HiveList.list_hives(context = self.context, - base_config_path = self.config_path, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - hive_offsets = None): + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + hive_offsets=None, + ): sys = False ntuser = False ## The global variables try: - key = hive.get_key('CurrentControlSet\\Control\\Session Manager\\Environment') + key = hive.get_key( + "CurrentControlSet\\Control\\Session Manager\\Environment" + ) sys = True except KeyError: - try: - key = hive.get_key('ControlSet001\\Control\\Session Manager\\Environment') + with contextlib.suppress(KeyError): + key = hive.get_key( + "ControlSet001\\Control\\Session Manager\\Environment" + ) sys = True - except KeyError: - pass if sys: - try: + with contextlib.suppress(KeyError): for node in key.get_values(): try: value_node_name = node.get_name() if value_node_name: values.append(value_node_name) - except (exceptions.InvalidAddressException, registry.RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ) as excp: vollog.log( constants.LOGLEVEL_VVV, - "Error while parsing global environment variables keys (some keys might be excluded)") + "Error while parsing global environment variables keys (some keys might be excluded)", + ) continue - except KeyError: - pass ## The user-specific variables - try: - key = hive.get_key('Environment') + with contextlib.suppress(KeyError): + key = hive.get_key("Environment") ntuser = True - except KeyError: - pass if ntuser: - try: + with contextlib.suppress(KeyError): for node in key.get_values(): try: value_node_name = node.get_name() if value_node_name: values.append(value_node_name) - except (exceptions.InvalidAddressException, registry.RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ) as excp: vollog.log( constants.LOGLEVEL_VVV, - "Error while parsing user environment variables keys (some keys might be excluded)") + "Error while parsing user environment variables keys (some keys might be excluded)", + ) continue - except KeyError: - pass ## The volatile user variables try: - key = hive.get_key('Volatile Environment') + key = hive.get_key("Volatile Environment") except KeyError: continue try: @@ -113,89 +131,113 @@ class Envars(interfaces.plugins.PluginInterface): value_node_name = node.get_name() if value_node_name: values.append(value_node_name) - except (exceptions.InvalidAddressException, registry.RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + registry.RegistryFormatException, + ) as excp: vollog.log( constants.LOGLEVEL_VVV, - "Error while parsing volatile environment variables keys (some keys might be excluded)") + "Error while parsing volatile environment variables keys (some keys might be excluded)", + ) continue except KeyError: continue ## These are variables set explicitly but are ## common enough to ignore safely. - values.extend([ - "ProgramFiles", - "CommonProgramFiles", - "SystemDrive", - "SystemRoot", - "ProgramData", - "PUBLIC", - "ALLUSERSPROFILE", - "COMPUTERNAME", - "SESSIONNAME", - "USERNAME", - "USERPROFILE", - "PROMPT", - "USERDOMAIN", - "AppData", - "CommonFiles", - "CommonDesktop", - "CommonProgramGroups", - "CommonStartMenu", - "CommonStartUp", - "Cookies", - "DesktopDirectory", - "Favorites", - "History", - "NetHood", - "PersonalDocuments", - "RecycleBin", - "StartMenu", - "Templates", - "AltStartup", - "CommonFavorites", - "ConnectionWizard", - "DocAndSettingRoot", - "InternetCache", - "windir", - "Path", - "HOMEDRIVE", - "PROCESSOR_ARCHITECTURE", - "NUMBER_OF_PROCESSORS", - "ProgramFiles(x86)", - "CommonProgramFiles(x86)", - "CommonProgramW6432", - "PSModulePath", - "PROCESSOR_IDENTIFIER", - "FP_NO_HOST_CHECK", - "LOCALAPPDATA", - "TMP", - "ProgramW6432", - ]) + values.extend( + [ + "ProgramFiles", + "CommonProgramFiles", + "SystemDrive", + "SystemRoot", + "ProgramData", + "PUBLIC", + "ALLUSERSPROFILE", + "COMPUTERNAME", + "SESSIONNAME", + "USERNAME", + "USERPROFILE", + "PROMPT", + "USERDOMAIN", + "AppData", + "CommonFiles", + "CommonDesktop", + "CommonProgramGroups", + "CommonStartMenu", + "CommonStartUp", + "Cookies", + "DesktopDirectory", + "Favorites", + "History", + "NetHood", + "PersonalDocuments", + "RecycleBin", + "StartMenu", + "Templates", + "AltStartup", + "CommonFavorites", + "ConnectionWizard", + "DocAndSettingRoot", + "InternetCache", + "windir", + "Path", + "HOMEDRIVE", + "PROCESSOR_ARCHITECTURE", + "NUMBER_OF_PROCESSORS", + "ProgramFiles(x86)", + "CommonProgramFiles(x86)", + "CommonProgramW6432", + "PSModulePath", + "PROCESSOR_IDENTIFIER", + "FP_NO_HOST_CHECK", + "LOCALAPPDATA", + "TMP", + "ProgramW6432", + ] + ) return values def _generator(self, data): silent_vars = [] - if self.config.get('SILENT', None): + if self.config.get("SILENT", None): silent_vars = self._get_silent_vars() for task in data: for var, val in task.environment_variables(): - if self.config.get('silent', None): + if self.config.get("silent", None): if var in silent_vars: continue - yield (0, (int(task.UniqueProcessId), str(objects.utility.array_to_string(task.ImageFileName)), - hex(task.get_peb().ProcessParameters.Environment.vol.offset), str(var), str(val))) + yield ( + 0, + ( + int(task.UniqueProcessId), + str(objects.utility.array_to_string(task.ImageFileName)), + hex(task.get_peb().ProcessParameters.Environment.vol.offset), + str(var), + str(val), + ), + ) def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - 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), ("Block", str), ("Variable", str), ("Value", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Block", str), + ("Variable", str), + ("Value", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/filescan.py b/volatility3/framework/plugins/windows/filescan.py index 79d85eb8d..0f68f39d4 100644 --- a/volatility3/framework/plugins/windows/filescan.py +++ b/volatility3/framework/plugins/windows/filescan.py @@ -18,17 +18,23 @@ class FileScan(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + ), ] @classmethod - def scan_files(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_files( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for file objects using the poolscanner module and constraints. Args: @@ -40,18 +46,22 @@ class FileScan(interfaces.plugins.PluginInterface): A list of File objects as found from the `layer_name` layer based on File pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'Fil\xe5', b'File']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"Fil\xe5", b"File"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config['kernel']] - - for fileobj in self.scan_files(self.context, kernel.layer_name, kernel.symbol_table_name): + kernel = self.context.modules[self.config["kernel"]] + for fileobj in self.scan_files( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: file_name = fileobj.FileName.String except exceptions.InvalidAddressException: @@ -60,4 +70,7 @@ class FileScan(interfaces.plugins.PluginInterface): yield (0, (format_hints.Hex(fileobj.vol.offset), file_name, fileobj.Size)) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Name", str), ("Size", int)], self._generator()) + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("Name", str), ("Size", int)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/getservicesids.py b/volatility3/framework/plugins/windows/getservicesids.py index a47b1178a..9b20ed2d0 100644 --- a/volatility3/framework/plugins/windows/getservicesids.py +++ b/volatility3/framework/plugins/windows/getservicesids.py @@ -16,15 +16,17 @@ vollog = logging.getLogger(__name__) def createservicesid(svc) -> str: - """ Calculate the Service SID """ - uni = ''.join([c + '\x00' for c in svc]) - sha = hashlib.sha1(uni.upper().encode("utf-8")).digest() # pylint: disable-msg=E1101 + """Calculate the Service SID""" + uni = "".join([c + "\x00" for c in svc]) + sha = hashlib.sha1( + uni.upper().encode("utf-8") + ).digest() # pylint: disable-msg=E1101 dec = list() for i in range(5): ## The use of struct here is OK. It doesn't make much sense ## to leverage obj.Object inside this loop. - dec.append(struct.unpack(' List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] def _generator(self): - - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] # Get the system hive - for hive in hivelist.HiveList.list_hives(context = self.context, - base_config_path = self.config_path, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_string = 'machine\\system', - hive_offsets = None): + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="machine\\system", + hive_offsets=None, + ): # Get ControlSet\Services. try: services = hive.get_key(r"CurrentControlSet\Services") diff --git a/volatility3/framework/plugins/windows/getsids.py b/volatility3/framework/plugins/windows/getsids.py index da6fa71c9..3e332f85d 100644 --- a/volatility3/framework/plugins/windows/getsids.py +++ b/volatility3/framework/plugins/windows/getsids.py @@ -8,7 +8,14 @@ import os import re from typing import List, Dict, Union -from volatility3.framework import renderers, interfaces, objects, exceptions, constants, layers +from volatility3.framework import ( + renderers, + interfaces, + objects, + exceptions, + constants, + layers, +) from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols.windows.extensions import registry @@ -18,7 +25,9 @@ from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) -def find_sid_re(sid_string, sid_re_list) -> Union[str, interfaces.renderers.BaseAbsentValue]: +def find_sid_re( + sid_string, sid_re_list +) -> Union[str, interfaces.renderers.BaseAbsentValue]: for reg, name in sid_re_list: if reg.search(sid_string): return name @@ -34,33 +43,52 @@ class GetSIDs(interfaces.plugins.PluginInterface): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for plugin_dir in constants.PLUGINS_PATH: - sids_json_file_name = os.path.join(plugin_dir, os.path.join("windows", "sids_and_privileges.json")) + sids_json_file_name = os.path.join( + plugin_dir, os.path.join("windows", "sids_and_privileges.json") + ) if os.path.exists(sids_json_file_name): break else: - vollog.log(constants.LOGLEVEL_VVV, 'sids_and_privileges.json file is missing plugin error') - raise RuntimeError("The sids_and_privileges.json file missed from you plugin directory") + vollog.log( + constants.LOGLEVEL_VVV, + "sids_and_privileges.json file is missing plugin error", + ) + raise RuntimeError( + "The sids_and_privileges.json file missed from you plugin directory" + ) # Get all the sids from the json file. - with open(sids_json_file_name, 'r') as file_handle: + with open(sids_json_file_name, "r") as file_handle: sids_json_data = json.load(file_handle) - self.servicesids = sids_json_data['service sids'] - self.well_known_sids = sids_json_data['well known'] + self.servicesids = sids_json_data["service sids"] + self.well_known_sids = sids_json_data["well known"] # Compile all the sids regex. - self.well_known_sid_re = [(re.compile(c_list[0]), c_list[1]) for c_list in sids_json_data['sids re']] + self.well_known_sid_re = [ + (re.compile(c_list[0]), c_list[1]) + for c_list in sids_json_data["sids re"] + ] @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] def lookup_user_sids(self) -> Dict[str, str]: @@ -73,16 +101,17 @@ class GetSIDs(interfaces.plugins.PluginInterface): key = "Microsoft\\Windows NT\\CurrentVersion\\ProfileList" val = "ProfileImagePath" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] sids = {} - for hive in hivelist.HiveList.list_hives(context = self.context, - base_config_path = self.config_path, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_string = 'config\\software', - hive_offsets = None): - + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="config\\software", + hive_offsets=None, + ): try: for subkey in hive.get_key(key).get_subkeys(): sid = str(subkey.get_name()) @@ -90,26 +119,44 @@ class GetSIDs(interfaces.plugins.PluginInterface): for node in subkey.get_values(): try: value_node_name = node.get_name() or "(Default)" - except (exceptions.InvalidAddressException, layers.registry.RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + layers.registry.RegistryFormatException, + ) as excp: continue try: value_data = node.decode_data() if isinstance(value_data, int): - value_data = format_hints.MultiTypeData(value_data, encoding = 'utf-8') - elif registry.RegValueTypes(node.Type) == registry.RegValueTypes.REG_BINARY: - value_data = format_hints.MultiTypeData(value_data, show_hex = True) - elif registry.RegValueTypes(node.Type) == registry.RegValueTypes.REG_MULTI_SZ: - value_data = format_hints.MultiTypeData(value_data, - encoding = 'utf-16-le', - split_nulls = True) + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-8" + ) + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_BINARY + ): + value_data = format_hints.MultiTypeData( + value_data, show_hex=True + ) + elif ( + registry.RegValueTypes(node.Type) + == registry.RegValueTypes.REG_MULTI_SZ + ): + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-16-le", split_nulls=True + ) else: - value_data = format_hints.MultiTypeData(value_data, encoding = 'utf-16-le') + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-16-le" + ) if value_node_name == val: - path = str(value_data).replace('\\x00', '')[:-1] + path = str(value_data).replace("\\x00", "")[:-1] user = ntpath.basename(path) sids[sid] = user - except (ValueError, exceptions.InvalidAddressException, - layers.registry.RegistryFormatException) as excp: + except ( + ValueError, + exceptions.InvalidAddressException, + layers.registry.RegistryFormatException, + ) as excp: continue except (KeyError, exceptions.InvalidAddressException): continue @@ -117,7 +164,6 @@ class GetSIDs(interfaces.plugins.PluginInterface): return sids def _generator(self, procs): - user_sids = self.lookup_user_sids() # Go all over the process list, get the token @@ -129,7 +175,15 @@ class GetSIDs(interfaces.plugins.PluginInterface): token = False if not token or not isinstance(token, interfaces.objects.ObjectInterface): - yield (0, [int(task.UniqueProcessId), str(task.ImageFileName), "Token unreadable", ""]) + yield ( + 0, + [ + int(task.UniqueProcessId), + str(task.ImageFileName), + "Token unreadable", + "", + ], + ) continue # Go all over the sids and try to translate them with one of the tables we have @@ -147,17 +201,28 @@ class GetSIDs(interfaces.plugins.PluginInterface): else: sid_name = "" - yield (0, (task.UniqueProcessId, objects.utility.array_to_string(task.ImageFileName), sid_string, - sid_name)) + yield ( + 0, + ( + task.UniqueProcessId, + objects.utility.array_to_string(task.ImageFileName), + sid_string, + sid_name, + ), + ) def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - 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), ("SID", str), ("Name", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [("PID", int), ("Process", str), ("SID", str), ("Name", str)], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/handles.py b/volatility3/framework/plugins/windows/handles.py index ab11d30d6..ddd9cb78e 100644 --- a/volatility3/framework/plugins/windows/handles.py +++ b/volatility3/framework/plugins/windows/handles.py @@ -38,13 +38,20 @@ class Handles(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process IDs to include (all other processes are excluded)", - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), ] def _decode_pointer(self, value, magic): @@ -67,7 +74,7 @@ class Handles(interfaces.plugins.PluginInterface): process' handle table, determine where the corresponding object's _OBJECT_HEADER can be found.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] virtual = kernel.layer_name @@ -80,7 +87,9 @@ class Handles(interfaces.plugins.PluginInterface): object_header.GrantedAccess = handle_table_entry.GrantedAccess except AttributeError: # starting with windows 8 - is_64bit = symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + is_64bit = symbols.symbol_table_is_64bit( + self.context, kernel.symbol_table_name + ) if is_64bit: if handle_table_entry.LowValue == 0: @@ -91,10 +100,14 @@ class Handles(interfaces.plugins.PluginInterface): # is this the right thing to raise here? if magic is None: if has_capstone: - raise AttributeError("Unable to find the SAR value for decoding handle table pointers") + raise AttributeError( + "Unable to find the SAR value for decoding handle table pointers" + ) else: raise exceptions.MissingModuleException( - "capstone", "Requires capstone to find the SAR value for decoding handle table pointers") + "capstone", + "Requires capstone to find the SAR value for decoding handle table pointers", + ) offset = self._decode_pointer(handle_table_entry.LowValue, magic) else: @@ -104,8 +117,11 @@ class Handles(interfaces.plugins.PluginInterface): offset = handle_table_entry.InfoTable & ~7 # print("LowValue: {0:#x} Magic: {1:#x} Offset: {2:#x}".format(handle_table_entry.InfoTable, magic, offset)) - object_header = self.context.object(kernel.symbol_table_name + constants.BANG + "_OBJECT_HEADER", virtual, - offset = offset) + object_header = self.context.object( + kernel.symbol_table_name + constants.BANG + "_OBJECT_HEADER", + virtual, + offset=offset, + ) object_header.GrantedAccess = handle_table_entry.GrantedAccessBits object_header.HandleValue = handle_value @@ -120,14 +136,17 @@ class Handles(interfaces.plugins.PluginInterface): """ if self._sar_value is None: - if not has_capstone: return None - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] virtual_layer_name = kernel.layer_name - kvo = self.context.layers[virtual_layer_name].config['kernel_virtual_offset'] - ntkrnlmp = self.context.module(kernel.symbol_table_name, layer_name = virtual_layer_name, offset = kvo) + kvo = self.context.layers[virtual_layer_name].config[ + "kernel_virtual_offset" + ] + ntkrnlmp = self.context.module( + kernel.symbol_table_name, layer_name=virtual_layer_name, offset=kvo + ) try: func_addr = ntkrnlmp.get_symbol("ObpCaptureHandleInformationEx").address @@ -140,7 +159,9 @@ class Handles(interfaces.plugins.PluginInterface): md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) - for (address, size, mnemonic, op_str) in md.disasm_lite(data, kvo + func_addr): + for address, size, mnemonic, op_str in md.disasm_lite( + data, kvo + func_addr + ): # print("{} {} {} {}".format(address, size, mnemonic, op_str)) if mnemonic.startswith("sar"): @@ -152,8 +173,12 @@ class Handles(interfaces.plugins.PluginInterface): return self._sar_value @classmethod - def get_type_map(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> Dict[int, str]: + def get_type_map( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Dict[int, str]: """List the executive object types (_OBJECT_TYPE) using the ObTypeIndexTable or ObpObjectTypes symbol (differs per OS). This method will be necessary for determining what type of object we have given an @@ -174,8 +199,8 @@ class Handles(interfaces.plugins.PluginInterface): type_map: Dict[int, str] = {} - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) try: table_addr = ntkrnlmp.get_symbol("ObTypeIndexTable").address @@ -187,10 +212,12 @@ class Handles(interfaces.plugins.PluginInterface): if not trans_layer.is_valid(kvo + table_addr): return type_map - ptrs = ntkrnlmp.object(object_type = "array", - offset = table_addr, - subtype = ntkrnlmp.get_type("pointer"), - count = 100) + ptrs = ntkrnlmp.object( + object_type="array", + offset=table_addr, + subtype=ntkrnlmp.get_type("pointer"), + count=100, + ) for i, ptr in enumerate(ptrs): # type: ignore # the first entry in the table is always null. break the @@ -199,11 +226,15 @@ class Handles(interfaces.plugins.PluginInterface): break try: - objt = ptr.dereference().cast(symbol_table + constants.BANG + "_OBJECT_TYPE") + objt = ptr.dereference().cast( + symbol_table + constants.BANG + "_OBJECT_TYPE" + ) type_name = objt.Name.String except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _OBJECT_HEADER Name at {objt.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _OBJECT_HEADER Name at {objt.vol.offset:#x}", + ) continue type_map[i] = type_name @@ -211,28 +242,40 @@ class Handles(interfaces.plugins.PluginInterface): return type_map @classmethod - def find_cookie(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> Optional[interfaces.objects.ObjectInterface]: + def find_cookie( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Optional[interfaces.objects.ObjectInterface]: """Find the ObHeaderCookie value (if it exists)""" try: - offset = context.symbol_space.get_symbol(symbol_table + constants.BANG + "ObHeaderCookie").address + offset = context.symbol_space.get_symbol( + symbol_table + constants.BANG + "ObHeaderCookie" + ).address except exceptions.SymbolError: return None - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - return context.object(symbol_table + constants.BANG + "unsigned int", layer_name, offset = kvo + offset) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + return context.object( + symbol_table + constants.BANG + "unsigned int", + layer_name, + offset=kvo + offset, + ) - def _make_handle_array(self, offset, level, depth = 0): + def _make_handle_array(self, offset, level, depth=0): """Parse a process' handle table and yield valid handle table entries, going as deep into the table "levels" as necessary.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] virtual = kernel.layer_name - kvo = self.context.layers[virtual].config['kernel_virtual_offset'] + kvo = self.context.layers[virtual].config["kernel_virtual_offset"] - ntkrnlmp = self.context.module(kernel.symbol_table_name, layer_name = virtual, offset = kvo) + ntkrnlmp = self.context.module( + kernel.symbol_table_name, layer_name=virtual, offset=kvo + ) if level > 0: subtype = ntkrnlmp.get_type("pointer") @@ -242,19 +285,20 @@ class Handles(interfaces.plugins.PluginInterface): count = 0x1000 / subtype.size if not self.context.layers[virtual].is_valid(offset): - return + return None - table = ntkrnlmp.object(object_type = "array", - offset = offset, - subtype = subtype, - count = int(count), - absolute = True) + table = ntkrnlmp.object( + object_type="array", + offset=offset, + subtype=subtype, + count=int(count), + absolute=True, + ) layer_object = self.context.layers[virtual] - masked_offset = (offset & layer_object.maximum_address) + masked_offset = offset & layer_object.maximum_address for entry in table: - if level > 0: for x in self._make_handle_array(entry, level - 1, depth): yield x @@ -263,8 +307,10 @@ class Handles(interfaces.plugins.PluginInterface): handle_multiplier = 4 handle_level_base = depth * count * handle_multiplier - handle_value = ((entry.vol.offset - masked_offset) / - (subtype.size / handle_multiplier)) + handle_level_base + handle_value = ( + (entry.vol.offset - masked_offset) + / (subtype.size / handle_multiplier) + ) + handle_level_base item = self._get_item(entry, handle_value) @@ -281,34 +327,42 @@ class Handles(interfaces.plugins.PluginInterface): continue def handles(self, handle_table): - try: TableCode = handle_table.TableCode & ~self._level_mask table_levels = handle_table.TableCode & self._level_mask except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, "Handle table parsing was aborted due to an invalid address exception") - return + vollog.log( + constants.LOGLEVEL_VVV, + "Handle table parsing was aborted due to an invalid address exception", + ) + return None for handle_table_entry in self._make_handle_array(TableCode, table_levels): yield handle_table_entry def _generator(self, procs): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - type_map = self.get_type_map(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name) + type_map = self.get_type_map( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) - cookie = self.find_cookie(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name) + cookie = self.find_cookie( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ) for proc in procs: try: object_table = proc.ObjectTable except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _EPROCESS.ObjectType at {proc.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _EPROCESS.ObjectType at {proc.vol.offset:#x}", + ) continue process_name = utility.array_to_string(proc.ImageFileName) @@ -323,10 +377,12 @@ class Handles(interfaces.plugins.PluginInterface): obj_name = item.file_name_with_device() elif obj_type == "Process": item = entry.Body.cast("_EPROCESS") - obj_name = f"{utility.array_to_string(proc.ImageFileName)} Pid {item.UniqueProcessId}" + obj_name = f"{utility.array_to_string(item.ImageFileName)} Pid {item.UniqueProcessId}" elif obj_type == "Thread": item = entry.Body.cast("_ETHREAD") - obj_name = f"Tid {item.Cid.UniqueThread} Pid {item.Cid.UniqueProcess}" + obj_name = ( + f"Tid {item.Cid.UniqueThread} Pid {item.Cid.UniqueProcess}" + ) elif obj_type == "Key": item = entry.Body.cast("_CM_KEY_BODY") obj_name = item.get_full_key_name() @@ -336,25 +392,46 @@ class Handles(interfaces.plugins.PluginInterface): except (ValueError, exceptions.InvalidAddressException): obj_name = "" - except (exceptions.InvalidAddressException): - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _OBJECT_HEADER at {entry.vol.offset:#x}") + except exceptions.InvalidAddressException: + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _OBJECT_HEADER at {entry.vol.offset:#x}", + ) continue - yield (0, (proc.UniqueProcessId, process_name, format_hints.Hex(entry.Body.vol.offset), - format_hints.Hex(entry.HandleValue), obj_type, format_hints.Hex(entry.GrantedAccess), - obj_name)) + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(entry.Body.vol.offset), + format_hints.Hex(entry.HandleValue), + obj_type, + format_hints.Hex(entry.GrantedAccess), + obj_name, + ), + ) def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - 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), ("Offset", format_hints.Hex), - ("HandleValue", format_hints.Hex), ("Type", str), - ("GrantedAccess", format_hints.Hex), ("Name", str)], - self._generator( - pslist.PsList.list_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("HandleValue", format_hints.Hex), + ("Type", str), + ("GrantedAccess", format_hints.Hex), + ("Name", str), + ], + self._generator( + pslist.PsList.list_processes( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/hashdump.py b/volatility3/framework/plugins/windows/hashdump.py index e9f8047e0..0c98ab8ca 100644 --- a/volatility3/framework/plugins/windows/hashdump.py +++ b/volatility3/framework/plugins/windows/hashdump.py @@ -27,27 +27,294 @@ class Hashdump(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] odd_parity = [ - 1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14, 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, - 31, 31, 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47, 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, - 59, 59, 61, 61, 62, 62, 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79, 81, 81, 82, 82, 84, 84, - 87, 87, 88, 88, 91, 91, 93, 93, 94, 94, 97, 97, 98, 98, 100, 100, 103, 103, 104, 104, 107, 107, 109, 109, 110, - 110, 112, 112, 115, 115, 117, 117, 118, 118, 121, 121, 122, 122, 124, 124, 127, 127, 128, 128, 131, 131, 133, - 133, 134, 134, 137, 137, 138, 138, 140, 140, 143, 143, 145, 145, 146, 146, 148, 148, 151, 151, 152, 152, 155, - 155, 157, 157, 158, 158, 161, 161, 162, 162, 164, 164, 167, 167, 168, 168, 171, 171, 173, 173, 174, 174, 176, - 176, 179, 179, 181, 181, 182, 182, 185, 185, 186, 186, 188, 188, 191, 191, 193, 193, 194, 194, 196, 196, 199, - 199, 200, 200, 203, 203, 205, 205, 206, 206, 208, 208, 211, 211, 213, 213, 214, 214, 217, 217, 218, 218, 220, - 220, 223, 223, 224, 224, 227, 227, 229, 229, 230, 230, 233, 233, 234, 234, 236, 236, 239, 239, 241, 241, 242, - 242, 244, 244, 247, 247, 248, 248, 251, 251, 253, 253, 254, 254 + 1, + 1, + 2, + 2, + 4, + 4, + 7, + 7, + 8, + 8, + 11, + 11, + 13, + 13, + 14, + 14, + 16, + 16, + 19, + 19, + 21, + 21, + 22, + 22, + 25, + 25, + 26, + 26, + 28, + 28, + 31, + 31, + 32, + 32, + 35, + 35, + 37, + 37, + 38, + 38, + 41, + 41, + 42, + 42, + 44, + 44, + 47, + 47, + 49, + 49, + 50, + 50, + 52, + 52, + 55, + 55, + 56, + 56, + 59, + 59, + 61, + 61, + 62, + 62, + 64, + 64, + 67, + 67, + 69, + 69, + 70, + 70, + 73, + 73, + 74, + 74, + 76, + 76, + 79, + 79, + 81, + 81, + 82, + 82, + 84, + 84, + 87, + 87, + 88, + 88, + 91, + 91, + 93, + 93, + 94, + 94, + 97, + 97, + 98, + 98, + 100, + 100, + 103, + 103, + 104, + 104, + 107, + 107, + 109, + 109, + 110, + 110, + 112, + 112, + 115, + 115, + 117, + 117, + 118, + 118, + 121, + 121, + 122, + 122, + 124, + 124, + 127, + 127, + 128, + 128, + 131, + 131, + 133, + 133, + 134, + 134, + 137, + 137, + 138, + 138, + 140, + 140, + 143, + 143, + 145, + 145, + 146, + 146, + 148, + 148, + 151, + 151, + 152, + 152, + 155, + 155, + 157, + 157, + 158, + 158, + 161, + 161, + 162, + 162, + 164, + 164, + 167, + 167, + 168, + 168, + 171, + 171, + 173, + 173, + 174, + 174, + 176, + 176, + 179, + 179, + 181, + 181, + 182, + 182, + 185, + 185, + 186, + 186, + 188, + 188, + 191, + 191, + 193, + 193, + 194, + 194, + 196, + 196, + 199, + 199, + 200, + 200, + 203, + 203, + 205, + 205, + 206, + 206, + 208, + 208, + 211, + 211, + 213, + 213, + 214, + 214, + 217, + 217, + 218, + 218, + 220, + 220, + 223, + 223, + 224, + 224, + 227, + 227, + 229, + 229, + 230, + 230, + 233, + 233, + 234, + 234, + 236, + 236, + 239, + 239, + 241, + 241, + 242, + 242, + 244, + 244, + 247, + 247, + 248, + 248, + 251, + 251, + 253, + 253, + 254, + 254, ] # Permutation matrix for boot key - bootkey_perm_table = [0x8, 0x5, 0x4, 0x2, 0xb, 0x9, 0xd, 0x3, 0x0, 0x6, 0x1, 0xc, 0xe, 0xa, 0xf, 0x7] + bootkey_perm_table = [ + 0x8, + 0x5, + 0x4, + 0x2, + 0xB, + 0x9, + 0xD, + 0x3, + 0x0, + 0x6, + 0x1, + 0xC, + 0xE, + 0xA, + 0xF, + 0x7, + ] # Constants for SAM decrypt algorithm aqwerty = b"!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0" @@ -67,11 +334,14 @@ class Hashdump(interfaces.plugins.PluginInterface): result = hive.get_key(key) except KeyError: vollog.info( - f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image") + f"Unable to load the required registry key {hive.get_name()}\\{key} from this memory image" + ) return result @classmethod - def get_user_keys(cls, samhive: registry.RegistryHive) -> List[interfaces.objects.ObjectInterface]: + def get_user_keys( + cls, samhive: registry.RegistryHive + ) -> List[interfaces.objects.ObjectInterface]: user_key_path = "SAM\\Domains\\Account\\Users" user_key = cls.get_hive_key(samhive, user_key_path) @@ -91,24 +361,28 @@ class Hashdump(interfaces.plugins.PluginInterface): if not lsa: return None - bootkey = '' + bootkey = "" for lk in lsa_keys: - key = cls.get_hive_key(syshive, lsa_base + '\\' + lk) + key = cls.get_hive_key(syshive, lsa_base + "\\" + lk) class_data = None if key: class_data = syshive.read(key.Class + 4, key.ClassLength) if class_data is None: return None - bootkey += class_data.decode('utf-16-le') + bootkey += class_data.decode("utf-16-le") bootkey_str = binascii.unhexlify(bootkey) - bootkey_scrambled = bytes([bootkey_str[cls.bootkey_perm_table[i]] for i in range(len(bootkey_str))]) + bootkey_scrambled = bytes( + [bootkey_str[cls.bootkey_perm_table[i]] for i in range(len(bootkey_str))] + ) return bootkey_scrambled @classmethod - def get_hbootkey(cls, samhive: registry.RegistryHive, bootkey: bytes) -> Optional[bytes]: + def get_hbootkey( + cls, samhive: registry.RegistryHive, bootkey: bytes + ) -> Optional[bytes]: sam_account_path = "SAM\\Domains\\Account" if not bootkey: @@ -120,7 +394,7 @@ class Hashdump(interfaces.plugins.PluginInterface): sam_data = None for v in sam_account_key.get_values(): - if v.get_name() == 'F': + if v.get_name() == "F": sam_data = samhive.read(v.Data + 4, v.DataLength) if not sam_data: return None @@ -133,7 +407,9 @@ class Hashdump(interfaces.plugins.PluginInterface): rc4_key = md5.digest() rc4 = ARC4.new(rc4_key) - hbootkey = rc4.encrypt(sam_data[0x80:0xA0]) # lgtm [py/weak-cryptographic-algorithm] + hbootkey = rc4.encrypt( + sam_data[0x80:0xA0] + ) # lgtm [py/weak-cryptographic-algorithm] return hbootkey elif revision == 3: # AES encrypted @@ -145,18 +421,22 @@ class Hashdump(interfaces.plugins.PluginInterface): return None @classmethod - def decrypt_single_salted_hash(cls, rid, hbootkey: bytes, enc_hash: bytes, _lmntstr, - salt: bytes) -> Optional[bytes]: + def decrypt_single_salted_hash( + cls, rid, hbootkey: bytes, enc_hash: bytes, _lmntstr, salt: bytes + ) -> Optional[bytes]: (des_k1, des_k2) = cls.sid_to_key(rid) des1 = DES.new(des_k1, DES.MODE_ECB) des2 = DES.new(des_k2, DES.MODE_ECB) cipher = AES.new(hbootkey[:16], AES.MODE_CBC, salt) obfkey = cipher.decrypt(enc_hash) - return des1.decrypt(obfkey[:8]) + des2.decrypt(obfkey[8:16]) # lgtm [py/weak-cryptographic-algorithm] + return des1.decrypt(obfkey[:8]) + des2.decrypt( + obfkey[8:16] + ) # lgtm [py/weak-cryptographic-algorithm] @classmethod - def get_user_hashes(cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive, - hbootkey: bytes) -> Optional[Tuple[bytes, bytes]]: + def get_user_hashes( + cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive, hbootkey: bytes + ) -> Optional[Tuple[bytes, bytes]]: ## Will sometimes find extra user with rid = NAMES, returns empty strings right now try: rid = int(str(user.get_name()), 16) @@ -164,64 +444,88 @@ class Hashdump(interfaces.plugins.PluginInterface): return None sam_data = None for v in user.get_values(): - if v.get_name() == 'V': + if v.get_name() == "V": sam_data = samhive.read(v.Data + 4, v.DataLength) if not sam_data: return None - lm_offset = unpack(" Tuple[bytes, bytes]: """Takes rid of a user and converts it to a key to be used by the DES cipher""" - bytestr1 = [sid & 0xFF, (sid >> 8) & 0xFF, (sid >> 16) & 0xFF, (sid >> 24) & 0xFF] + bytestr1 = [ + sid & 0xFF, + (sid >> 8) & 0xFF, + (sid >> 16) & 0xFF, + (sid >> 24) & 0xFF, + ] bytestr1 += bytestr1[0:3] bytestr2 = [bytestr1[3]] + bytestr1[0:3] bytestr2 += bytestr2[0:3] - return cls.sidbytes_to_key(bytes(bytestr1)), cls.sidbytes_to_key(bytes(bytestr2)) + return cls.sidbytes_to_key(bytes(bytestr1)), cls.sidbytes_to_key( + bytes(bytestr2) + ) @classmethod def sidbytes_to_key(cls, s: bytes) -> bytes: """Builds final DES key from the strings generated in sid_to_key""" - key = [s[0] >> 1, ((s[0] & 0x01) << 6) | (s[1] >> 2), ((s[1] & 0x03) << 5) | (s[2] >> 3), - ((s[2] & 0x07) << 4) | (s[3] >> 4), ((s[3] & 0x0F) << 3) | (s[4] >> 5), - ((s[4] & 0x1F) << 2) | (s[5] >> 6), ((s[5] & 0x3F) << 1) | (s[6] >> 7), s[6] & 0x7F] + key = [ + s[0] >> 1, + ((s[0] & 0x01) << 6) | (s[1] >> 2), + ((s[1] & 0x03) << 5) | (s[2] >> 3), + ((s[2] & 0x07) << 4) | (s[3] >> 4), + ((s[3] & 0x0F) << 3) | (s[4] >> 5), + ((s[4] & 0x1F) << 2) | (s[5] >> 6), + ((s[5] & 0x3F) << 1) | (s[6] >> 7), + s[6] & 0x7F, + ] for i in range(8): - key[i] = (key[i] << 1) + key[i] = key[i] << 1 key[i] = cls.odd_parity[key[i]] return bytes(key) @classmethod - def decrypt_single_hash(cls, rid: int, hbootkey: bytes, enc_hash: bytes, lmntstr: bytes): + def decrypt_single_hash( + cls, rid: int, hbootkey: bytes, enc_hash: bytes, lmntstr: bytes + ): (des_k1, des_k2) = cls.sid_to_key(rid) des1 = DES.new(des_k1, DES.MODE_ECB) des2 = DES.new(des_k2, DES.MODE_ECB) @@ -232,27 +536,33 @@ class Hashdump(interfaces.plugins.PluginInterface): rc4 = ARC4.new(rc4_key) obfkey = rc4.encrypt(enc_hash) # lgtm [py/weak-cryptographic-algorithm] - return des1.decrypt(obfkey[:8]) + des2.decrypt(obfkey[8:]) # lgtm [py/weak-cryptographic-algorithm] + return des1.decrypt(obfkey[:8]) + des2.decrypt( + obfkey[8:] + ) # lgtm [py/weak-cryptographic-algorithm] @classmethod - def get_user_name(cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive) -> Optional[bytes]: + def get_user_name( + cls, user: registry.CM_KEY_NODE, samhive: registry.RegistryHive + ) -> Optional[bytes]: value = None for v in user.get_values(): - if v.get_name() == 'V': + if v.get_name() == "V": value = samhive.read(v.Data + 4, v.DataLength) if not value: return None - name_offset = unpack(" len(value): return None - username = value[name_offset:name_offset + name_length] + username = value[name_offset : name_offset + name_length] return username # replaces the dump_hashes method in vol2 - def _generator(self, syshive: registry.RegistryHive, samhive: registry.RegistryHive): + def _generator( + self, syshive: registry.RegistryHive, samhive: registry.RegistryHive + ): if syshive is None: vollog.debug("SYSTEM address is None: No system hive found") if samhive is None: @@ -271,30 +581,33 @@ class Hashdump(interfaces.plugins.PluginInterface): if name is None: name = renderers.NotAvailableValue() else: - name = str(name, 'utf-16-le', errors = 'ignore') + name = str(name, "utf-16-le", errors="ignore") - lmout = str(binascii.hexlify(lmhash or self.empty_lm), 'latin-1') - ntout = str(binascii.hexlify(nthash or self.empty_nt), 'latin-1') + lmout = str(binascii.hexlify(lmhash or self.empty_lm), "latin-1") + ntout = str(binascii.hexlify(nthash or self.empty_nt), "latin-1") rid = int(str(user.get_name()), 16) yield (0, (name, rid, lmout, ntout)) else: vollog.warning("Hbootkey is not valid") def run(self): - offset = self.config.get('offset', None) + offset = self.config.get("offset", None) syshive = None samhive = None - kernel = self.context.modules[self.config['kernel']] - for hive in hivelist.HiveList.list_hives(self.context, - self.config_path, - kernel.layer_name, - kernel.symbol_table_name, - hive_offsets = None if offset is None else [offset]): - - if hive.get_name().split('\\')[-1].upper() == 'SYSTEM': + kernel = self.context.modules[self.config["kernel"]] + for hive in hivelist.HiveList.list_hives( + self.context, + self.config_path, + kernel.layer_name, + kernel.symbol_table_name, + hive_offsets=None if offset is None else [offset], + ): + if hive.get_name().split("\\")[-1].upper() == "SYSTEM": syshive = hive - if hive.get_name().split('\\')[-1].upper() == 'SAM': + if hive.get_name().split("\\")[-1].upper() == "SAM": samhive = hive - return renderers.TreeGrid([("User", str), ("rid", int), ("lmhash", str), ("nthash", str)], - self._generator(syshive, samhive)) + return renderers.TreeGrid( + [("User", str), ("rid", int), ("lmhash", str), ("nthash", str)], + self._generator(syshive, samhive), + ) diff --git a/volatility3/framework/plugins/windows/iat.py b/volatility3/framework/plugins/windows/iat.py new file mode 100644 index 000000000..d2fdc0ad8 --- /dev/null +++ b/volatility3/framework/plugins/windows/iat.py @@ -0,0 +1,150 @@ +# This file is Copyright 2024 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, io, pefile +from volatility3.framework.symbols import intermed +from volatility3.framework import renderers, interfaces, exceptions, constants +from volatility3.framework.configuration import requirements +from volatility3.plugins.windows import pslist +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols.windows.extensions import pe + +vollog = logging.getLogger(__name__) + + +class IAT(interfaces.plugins.PluginInterface): + """Extract Import Address Table to list API (functions) used by a program contained in external libraries""" + + _required_framework_version = (2, 4, 0) + + @classmethod + def get_requirements(cls): + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + ] + + def _generator(self, procs): + kernel = self.context.modules[self.config["kernel"]] + + for proc in procs: + try: + proc_id = proc.UniqueProcessId + proc_layer_name = proc.add_process_layer() + peb = self.context.object( + kernel.symbol_table_name + constants.BANG + "_PEB", + layer_name=proc_layer_name, + offset=proc.Peb, + ) + + if proc_layer_name is None: + raise TypeError("add_process_layer failed") + + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, + self.config_path, + "windows", + "pe", + class_types=pe.class_types, + ) + pe_data = io.BytesIO() + + dos_header = self.context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=peb.ImageBaseAddress, + layer_name=proc_layer_name, + ) + + for offset, data in dos_header.reconstruct(): + pe_data.seek(offset) + pe_data.write(data) + + pe_obj = pefile.PE(data=pe_data.getvalue(), fast_load=True) + pe_obj.parse_data_directories( + [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_IMPORT"]] + ) + if hasattr(pe_obj, "DIRECTORY_ENTRY_IMPORT"): + for entry in pe_obj.DIRECTORY_ENTRY_IMPORT: + dll_entry = entry.dll + if dll_entry: + dll_entry = dll_entry.decode() + else: + dll_entry = renderers.NotAvailableValue + + bound = True + # Initially set to 0 if not bound + time_date_stamp = entry.struct.TimeDateStamp + if not time_date_stamp: + bound = False + + # Iterate over imported functions + for imp in entry.imports: + import_name = imp.name + if import_name: + import_name = imp.name.decode() + else: + import_name = renderers.NotAvailableValue() + function_address = ( + pe_obj.OPTIONAL_HEADER.ImageBase + imp.address + ) + if not function_address: + function_address = renderers.NotAvailableValue + + yield ( + 0, + ( + proc_id, + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ), + dll_entry, + bound, + import_name, + format_hints.Hex(function_address), + ), + ) + except exceptions.InvalidAddressException as excp: + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) + continue + + def run(self): + kernel = self.context.modules[self.config["kernel"]] + + return renderers.TreeGrid( + [ + ("PID", int), + ("Name", str), + ("Library", str), + ("Bound", bool), + ("Function", str), + ("Address", format_hints.Hex), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=pslist.PsList.create_pid_filter( + self.config.get("pid", None) + ), + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/info.py b/volatility3/framework/plugins/windows/info.py index 172664aef..100a677c2 100644 --- a/volatility3/framework/plugins/windows/info.py +++ b/volatility3/framework/plugins/windows/info.py @@ -22,15 +22,20 @@ class Info(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), ] @classmethod - def get_depends(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - index: int = 0) -> Iterable[Tuple[int, interfaces.layers.DataLayerInterface]]: + def get_depends( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + index: int = 0, + ) -> Iterable[Tuple[int, interfaces.layers.DataLayerInterface]]: """List the dependencies of a given layer. Args: @@ -52,7 +57,12 @@ class Info(plugins.PluginInterface): pass @classmethod - def get_kernel_module(cls, context: interfaces.context.ContextInterface, layer_name: str, symbol_table: str): + def get_kernel_module( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ): """Returns the kernel module based on the layer and symbol_table""" virtual_layer = context.layers[layer_name] if not isinstance(virtual_layer, layers.intel.Intel): @@ -60,12 +70,17 @@ class Info(plugins.PluginInterface): kvo = virtual_layer.config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) return ntkrnlmp @classmethod - def get_kdbg_structure(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str, - symbol_table: str) -> interfaces.objects.ObjectInterface: + def get_kdbg_structure( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + symbol_table: str, + ) -> interfaces.objects.ObjectInterface: """Returns the KDDEBUGGER_DATA64 structure for a kernel""" ntkrnlmp = cls.get_kernel_module(context, layer_name, symbol_table) @@ -73,23 +88,30 @@ class Info(plugins.PluginInterface): kdbg_offset = ntkrnlmp.get_symbol("KdDebuggerDataBlock").address - kdbg_table_name = intermed.IntermediateSymbolTable.create(context, - interfaces.configuration.path_join( - config_path, 'kdbg'), - "windows", - "kdbg", - native_types = native_types, - class_types = extensions.kdbg.class_types) + kdbg_table_name = intermed.IntermediateSymbolTable.create( + context, + interfaces.configuration.path_join(config_path, "kdbg"), + "windows", + "kdbg", + native_types=native_types, + class_types=extensions.kdbg.class_types, + ) - kdbg = context.object(kdbg_table_name + constants.BANG + "_KDDEBUGGER_DATA64", - offset = ntkrnlmp.offset + kdbg_offset, - layer_name = layer_name) + kdbg = context.object( + kdbg_table_name + constants.BANG + "_KDDEBUGGER_DATA64", + offset=ntkrnlmp.offset + kdbg_offset, + layer_name=layer_name, + ) return kdbg @classmethod - def get_kuser_structure(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> interfaces.objects.ObjectInterface: + def get_kuser_structure( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> interfaces.objects.ObjectInterface: """Returns the _KUSER_SHARED_DATA structure for a kernel""" virtual_layer = context.layers[layer_name] if not isinstance(virtual_layer, layers.intel.Intel): @@ -103,28 +125,42 @@ class Info(plugins.PluginInterface): else: kuser_addr = 0xFFFFF78000000000 - kuser = ntkrnlmp.object(object_type = "_KUSER_SHARED_DATA", - layer_name = layer_name, - offset = kuser_addr, - absolute = True) + kuser = ntkrnlmp.object( + object_type="_KUSER_SHARED_DATA", + layer_name=layer_name, + offset=kuser_addr, + absolute=True, + ) return kuser @classmethod - def get_version_structure(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> interfaces.objects.ObjectInterface: + def get_version_structure( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> interfaces.objects.ObjectInterface: """Returns the KdVersionBlock information from a kernel""" ntkrnlmp = cls.get_kernel_module(context, layer_name, symbol_table) vers_offset = ntkrnlmp.get_symbol("KdVersionBlock").address - vers = ntkrnlmp.object(object_type = "_DBGKD_GET_VERSION64", layer_name = layer_name, offset = vers_offset) + vers = ntkrnlmp.object( + object_type="_DBGKD_GET_VERSION64", + layer_name=layer_name, + offset=vers_offset, + ) return vers @classmethod - def get_ntheader_structure(cls, context: interfaces.context.ContextInterface, config_path: str, - layer_name: str) -> interfaces.objects.ObjectInterface: + def get_ntheader_structure( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + ) -> interfaces.objects.ObjectInterface: """Gets the ntheader structure for the kernel of the specified layer""" virtual_layer = context.layers[layer_name] if not isinstance(virtual_layer, layers.intel.Intel): @@ -132,42 +168,52 @@ class Info(plugins.PluginInterface): kvo = virtual_layer.config["kernel_virtual_offset"] - pe_table_name = intermed.IntermediateSymbolTable.create(context, - interfaces.configuration.path_join(config_path, 'pe'), - "windows", - "pe", - class_types = extensions.pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + context, + interfaces.configuration.path_join(config_path, "pe"), + "windows", + "pe", + class_types=extensions.pe.class_types, + ) - dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = kvo, - layer_name = layer_name) + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=kvo, + layer_name=layer_name, + ) nt_header = dos_header.get_nt_header() return nt_header def _generator(self): - - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] layer_name = kernel.layer_name symbol_table = kernel.symbol_table_name layer = self.context.layers[layer_name] table = self.context.symbol_space[symbol_table] - kdbg = self.get_kdbg_structure(self.context, self.config_path, layer_name, symbol_table) + kdbg = self.get_kdbg_structure( + self.context, self.config_path, layer_name, symbol_table + ) yield (0, ("Kernel Base", hex(layer.config["kernel_virtual_offset"]))) yield (0, ("DTB", hex(layer.config["page_map_offset"]))) yield (0, ("Symbols", table.config["isf_url"])) - yield (0, ("Is64Bit", str(symbols.symbol_table_is_64bit(self.context, symbol_table)))) - yield (0, ("IsPAE", str(self.context.layers[layer_name].metadata.get("pae", False)))) + yield ( + 0, + ("Is64Bit", str(symbols.symbol_table_is_64bit(self.context, symbol_table))), + ) + yield ( + 0, + ("IsPAE", str(self.context.layers[layer_name].metadata.get("pae", False))), + ) for i, layer in self.get_depends(self.context, layer_name): yield (0, (layer.name, f"{i} {layer.__class__.__name__}")) if kdbg.Header.OwnerTag == 0x4742444B: - yield (0, ("KdDebuggerDataBlock", hex(kdbg.vol.offset))) yield (0, ("NTBuildLab", kdbg.get_build_lab())) yield (0, ("CSDVersion", str(kdbg.get_csdversion()))) @@ -182,29 +228,59 @@ class Info(plugins.PluginInterface): cpu_count_offset = ntkrnlmp.get_symbol("KeNumberProcessors").address - cpu_count = ntkrnlmp.object(object_type = "unsigned int", layer_name = layer_name, offset = cpu_count_offset) + cpu_count = ntkrnlmp.object( + object_type="unsigned int", layer_name=layer_name, offset=cpu_count_offset + ) yield (0, ("KeNumberProcessors", str(cpu_count))) kuser = self.get_kuser_structure(self.context, layer_name, symbol_table) yield (0, ("SystemTime", str(kuser.SystemTime.get_time()))) - yield (0, ("NtSystemRoot", - str(kuser.NtSystemRoot.cast("string", encoding = "utf-16", errors = "replace", max_length = 260)))) + yield ( + 0, + ( + "NtSystemRoot", + str( + kuser.NtSystemRoot.cast( + "string", encoding="utf-16", errors="replace", max_length=260 + ) + ), + ), + ) yield (0, ("NtProductType", str(kuser.NtProductType.description))) yield (0, ("NtMajorVersion", str(kuser.NtMajorVersion))) yield (0, ("NtMinorVersion", str(kuser.NtMinorVersion))) # yield (0, ("KdDebuggerEnabled", "True" if kuser.KdDebuggerEnabled else "False")) # yield (0, ("SafeBootMode", "True" if kuser.SafeBootMode else "False")) - nt_header = self.get_ntheader_structure(self.context, self.config_path, layer_name) + nt_header = self.get_ntheader_structure( + self.context, self.config_path, layer_name + ) - yield (0, ("PE MajorOperatingSystemVersion", str(nt_header.OptionalHeader.MajorOperatingSystemVersion))) - yield (0, ("PE MinorOperatingSystemVersion", str(nt_header.OptionalHeader.MinorOperatingSystemVersion))) + yield ( + 0, + ( + "PE MajorOperatingSystemVersion", + str(nt_header.OptionalHeader.MajorOperatingSystemVersion), + ), + ) + yield ( + 0, + ( + "PE MinorOperatingSystemVersion", + str(nt_header.OptionalHeader.MinorOperatingSystemVersion), + ), + ) yield (0, ("PE Machine", str(nt_header.FileHeader.Machine))) - yield (0, ("PE TimeDateStamp", time.asctime(time.gmtime(nt_header.FileHeader.TimeDateStamp)))) + yield ( + 0, + ( + "PE TimeDateStamp", + time.asctime(time.gmtime(nt_header.FileHeader.TimeDateStamp)), + ), + ) def run(self): - return TreeGrid([("Variable", str), ("Value", str)], self._generator()) diff --git a/volatility3/framework/plugins/windows/joblinks.py b/volatility3/framework/plugins/windows/joblinks.py new file mode 100644 index 000000000..d84c133c0 --- /dev/null +++ b/volatility3/framework/plugins/windows/joblinks.py @@ -0,0 +1,132 @@ +# 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 + +from typing import Iterator, List, Tuple + +from volatility3.framework import exceptions, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist + +vollog = logging.getLogger(__name__) + + +class JobLinks(interfaces.plugins.PluginInterface): + """Print process job link information""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.BooleanRequirement( + name="physical", + description="Display physical offset instead of virtual", + default=False, + optional=True, + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + ] + + def _generator(self) -> Iterator[Tuple]: + kernel = self.context.modules[self.config["kernel"]] + memory = self.context.layers[kernel.layer_name] + + for proc in pslist.PsList.list_processes( + self.context, kernel.layer_name, kernel.symbol_table_name + ): + try: + if not self.config["physical"]: + offset = proc.vol.offset + else: + (_, _, offset, _, _) = list( + memory.mapping(offset=proc.vol.offset, length=0) + )[0] + + job = proc.Job.dereference() + + yield ( + 0, + ( + format_hints.Hex(offset), + utility.array_to_string(proc.ImageFileName), + proc.UniqueProcessId, + proc.InheritedFromUniqueProcessId, + proc.get_session_id(), + job.SessionId, + proc.get_is_wow64(), + job.TotalProcesses, + job.ActiveProcesses, + job.TotalTerminatedProcesses, + renderers.NotApplicableValue(), + "(Original Process)", + ), + ) + + for entry in job.ProcessListHead.to_list( + proc.vol.type_name, "JobLinks" + ): + if not self.config["physical"]: + offset = entry.vol.offset + else: + (_, _, offset, _, _) = list( + memory.mapping(offset=entry.vol.offset, length=0) + )[0] + + yield ( + 1, + ( + format_hints.Hex(offset), + utility.array_to_string(entry.ImageFileName), + entry.UniqueProcessId, + entry.InheritedFromUniqueProcessId, + entry.get_session_id(), + 0, + entry.get_is_wow64(), + 0, + 0, + 0, + "Yes", + entry.get_peb().ProcessParameters.ImagePathName.get_string(), + ), + ) + + except exceptions.InvalidAddressException: + continue + + def run(self) -> renderers.TreeGrid: + offsettype = ( + "(V)" + if not self.config.get("physical", pslist.PsList.PHYSICAL_DEFAULT) + else "(P)" + ) + + return renderers.TreeGrid( + [ + (f"Offset{offsettype}", format_hints.Hex), + ("Name", str), + ("PID", int), + ("PPID", int), + ("Sess", int), + ("JobSess", int), + ("Wow64", bool), + ("Total", int), + ("Active", int), + ("Term", int), + ("JobLink", str), + ("Process", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/ldrmodules.py b/volatility3/framework/plugins/windows/ldrmodules.py index e7c96e946..4c8456fa9 100644 --- a/volatility3/framework/plugins/windows/ldrmodules.py +++ b/volatility3/framework/plugins/windows/ldrmodules.py @@ -1,34 +1,43 @@ -from volatility3.framework import interfaces, constants -from volatility3.framework import renderers, interfaces, exceptions +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows.extensions import pe from volatility3.plugins.windows import pslist, vadinfo + class LdrModules(interfaces.plugins.PluginInterface): + """Lists the loaded modules in a particular windows memory image.""" + _required_framework_version = (2, 0, 0) _version = (1, 0, 0) - + @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'vadinfo', component = vadinfo.VadInfo, 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="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, 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, procs): - - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) def filter_function(x: interfaces.objects.ObjectInterface) -> bool: try: @@ -36,25 +45,28 @@ class LdrModules(interfaces.plugins.PluginInterface): except AttributeError: return False - filter_func = filter_function + filter_func = filter_function for proc in procs: proc_layer_name = proc.add_process_layer() # Build dictionaries from different module lists, where the DllBase address is the key and value is the module object - load_order_mod = dict((mod.DllBase, mod) - for mod in proc.load_order_modules()) - init_order_mod = dict((mod.DllBase, mod) - for mod in proc.init_order_modules()) - mem_order_mod = dict((mod.DllBase, mod) - for mod in proc.mem_order_modules()) + load_order_mod = dict( + (mod.DllBase, mod) for mod in proc.load_order_modules() + ) + init_order_mod = dict( + (mod.DllBase, mod) for mod in proc.init_order_modules() + ) + mem_order_mod = dict((mod.DllBase, mod) for mod in proc.mem_order_modules()) # Build dictionary of mapped files, where the VAD start address is the key and value is the file name of the mapped file mapped_files = {} - for vad in vadinfo.VadInfo.list_vads(proc, filter_func = filter_func): - dos_header = self.context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = vad.get_start(), - layer_name = proc_layer_name) + for vad in vadinfo.VadInfo.list_vads(proc, filter_func=filter_func): + dos_header = self.context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=vad.get_start(), + layer_name=proc_layer_name, + ) try: # Filter out VADs that do not start with a MZ header if dos_header.e_magic != 0x5A4D: @@ -70,29 +82,45 @@ class LdrModules(interfaces.plugins.PluginInterface): init_mod = init_order_mod.get(base, None) mem_mod = mem_order_mod.get(base, None) - yield (0, [int(proc.UniqueProcessId), - str(proc.ImageFileName.cast("string", - max_length = proc.ImageFileName.vol.count, - errors = 'replace')), - format_hints.Hex(base), - load_mod != None, - init_mod != None, - mem_mod != None, - mapped_files[base]]) + yield ( + 0, + [ + int(proc.UniqueProcessId), + str( + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ) + ), + format_hints.Hex(base), + load_mod is not None, + init_mod is not None, + mem_mod is not None, + mapped_files[base], + ], + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - kernel = self.context.modules[self.config['kernel']] + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid([("Pid", int), - ("Process", str), - ("Base", format_hints.Hex), - ("InLoad", bool), - ("InInit", bool), - ("InMem", bool), - ("MappedPath", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("Pid", int), + ("Process", str), + ("Base", format_hints.Hex), + ("InLoad", bool), + ("InInit", bool), + ("InMem", bool), + ("MappedPath", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/lsadump.py b/volatility3/framework/plugins/windows/lsadump.py index edf16416c..da8dee325 100644 --- a/volatility3/framework/plugins/windows/lsadump.py +++ b/volatility3/framework/plugins/windows/lsadump.py @@ -27,10 +27,17 @@ class Lsadump(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'hashdump', component = hashdump.Hashdump, version = (1, 1, 0)), - requirements.VersionRequirement(name = 'hivelist', component = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="hashdump", component=hashdump.Hashdump, version=(1, 1, 0) + ), + requirements.VersionRequirement( + name="hivelist", component=hivelist.HiveList, version=(1, 0, 0) + ), ] @classmethod @@ -46,8 +53,8 @@ class Lsadump(interfaces.plugins.PluginInterface): data = b"" for i in range(60, len(secret), 16): - aes = AES.new(aeskey, AES.MODE_CBC, b'\x00' * 16) - buf = secret[i:i + 16] + aes = AES.new(aeskey, AES.MODE_CBC, b"\x00" * 16) + buf = secret[i : i + 16] if len(buf) < 16: buf += (16 - len(buf)) * "\00" data += aes.decrypt(buf) @@ -55,14 +62,16 @@ class Lsadump(interfaces.plugins.PluginInterface): return data @classmethod - def get_lsa_key(cls, sechive: registry.RegistryHive, bootkey: bytes, vista_or_later: bool) -> Optional[bytes]: + def get_lsa_key( + cls, sechive: registry.RegistryHive, bootkey: bytes, vista_or_later: bool + ) -> Optional[bytes]: if not bootkey: return None if vista_or_later: - policy_key = 'PolEKList' + policy_key = "PolEKList" else: - policy_key = 'PolSecretEncryptionKey' + policy_key = "PolSecretEncryptionKey" enc_reg_key = hashdump.Hashdump.get_hive_key(sechive, "Policy\\" + policy_key) if not enc_reg_key: @@ -84,7 +93,9 @@ class Lsadump(interfaces.plugins.PluginInterface): rc4key = md5.digest() rc4 = ARC4.new(rc4key) - lsa_key = rc4.decrypt(obf_lsa_key[12:60]) # lgtm [py/weak-cryptographic-algorithm] + lsa_key = rc4.decrypt( + obf_lsa_key[12:60] + ) # lgtm [py/weak-cryptographic-algorithm] lsa_key = lsa_key[0x10:0x20] else: lsa_key = cls.decrypt_aes(obf_lsa_key, bootkey) @@ -92,17 +103,25 @@ class Lsadump(interfaces.plugins.PluginInterface): return lsa_key @classmethod - def get_secret_by_name(cls, sechive: registry.RegistryHive, name: str, lsakey: bytes, is_vista_or_later: bool): - enc_secret_key = hashdump.Hashdump.get_hive_key(sechive, "Policy\\Secrets\\" + name + "\\CurrVal") + def get_secret_by_name( + cls, + sechive: registry.RegistryHive, + name: str, + lsakey: bytes, + is_vista_or_later: bool, + ): + enc_secret_key = hashdump.Hashdump.get_hive_key( + sechive, "Policy\\Secrets\\" + name + "\\CurrVal" + ) secret = None if enc_secret_key: enc_secret_value = next(enc_secret_key.get_values()) if enc_secret_value: - - enc_secret = sechive.read(enc_secret_value.Data + 4, enc_secret_value.DataLength) + enc_secret = sechive.read( + enc_secret_value.Data + 4, enc_secret_value.DataLength + ) if enc_secret: - if not is_vista_or_later: secret = cls.decrypt_secret(enc_secret[0xC:], lsakey) else: @@ -116,51 +135,55 @@ class Lsadump(interfaces.plugins.PluginInterface): Decrypts a block of data with DES using given key. Note that key can be longer than 7 bytes.""" - decrypted_data = b'' + decrypted_data = b"" j = 0 # key index for i in range(0, len(secret), 8): - enc_block = secret[i:i + 8] - block_key = key[j:j + 7] + enc_block = secret[i : i + 8] + block_key = key[j : j + 7] des_key = hashdump.Hashdump.sidbytes_to_key(block_key) des = DES.new(des_key, DES.MODE_ECB) enc_block = enc_block + b"\x00" * int(abs(8 - len(enc_block)) % 8) - decrypted_data += des.decrypt(enc_block) # lgtm [py/weak-cryptographic-algorithm] + decrypted_data += des.decrypt( + enc_block + ) # lgtm [py/weak-cryptographic-algorithm] j += 7 - if len(key[j:j + 7]) < 7: - j = len(key[j:j + 7]) + if len(key[j : j + 7]) < 7: + j = len(key[j : j + 7]) (dec_data_len,) = unpack(" Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: + cls, + context: interfaces.context.ContextInterface, + kernel_layer_name: str, + symbol_table: str, + proc: interfaces.objects.ObjectInterface, + ) -> Iterable[Tuple[interfaces.objects.ObjectInterface, bytes]]: """Generate memory regions for a process that may contain injected code. @@ -87,40 +105,65 @@ class Malfind(interfaces.plugins.PluginInterface): proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(proc_id, excp.invalid_address, - excp.layer_name)) - return + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) + return None proc_layer = context.layers[proc_layer_name] for vad in proc.get_vad_root().traverse(): protection_string = vad.get_protection( - vadinfo.VadInfo.protect_values(context, kernel_layer_name, symbol_table), vadinfo.winnt_protections) + vadinfo.VadInfo.protect_values( + context, kernel_layer_name, symbol_table + ), + vadinfo.winnt_protections, + ) write_exec = "EXECUTE" in protection_string and "WRITE" in protection_string # the write/exec check applies to everything if not write_exec: continue - if (vad.get_private_memory() == 1 - and vad.get_tag() == "VadS") or (vad.get_private_memory() == 0 - and protection_string != "PAGE_EXECUTE_WRITECOPY"): + if (vad.get_private_memory() == 1 and vad.get_tag() == "VadS") or ( + vad.get_private_memory() == 0 + and protection_string != "PAGE_EXECUTE_WRITECOPY" + ): if cls.is_vad_empty(proc_layer, vad): continue - data = proc_layer.read(vad.get_start(), 64, pad = True) + data = proc_layer.read(vad.get_start(), 64, pad=True) yield vad, data def _generator(self, procs): # determine if we're on a 32 or 64 bit kernel - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - is_32bit_arch = not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + # set refined criteria to know when to add to "Notes" column + refined_criteria = { + b"MZ": "MZ header", + b"\x55\x8B": "PE header", + b"\x55\x48": "Function prologue", + b"\x55\x89": "Function prologue", + } + + is_32bit_arch = not symbols.symbol_table_is_64bit( + self.context, kernel.symbol_table_name + ) for proc in procs: + # by default, "Notes" column will be set to N/A + notes = renderers.NotApplicableValue() process_name = utility.array_to_string(proc.ImageFileName) - for vad, data in self.list_injections(self.context, kernel.layer_name, kernel.symbol_table_name, proc): + for vad, data in self.list_injections( + self.context, kernel.layer_name, kernel.symbol_table_name, proc + ): + # Check for unique headers and update "Notes" column if criteria is met + if data[0:2] in refined_criteria: + notes = refined_criteria[data[0:2]] # if we're on a 64 bit kernel, we may still need 32 bit disasm due to wow64 if is_32bit_arch or proc.get_is_wow64(): @@ -128,37 +171,76 @@ class Malfind(interfaces.plugins.PluginInterface): else: architecture = "intel64" - disasm = interfaces.renderers.Disassembly(data, vad.get_start(), architecture) + disasm = interfaces.renderers.Disassembly( + data, vad.get_start(), architecture + ) file_output = "Disabled" - if self.config['dump']: + if self.config["dump"]: file_output = "Error outputting to file" try: - file_handle = vadinfo.VadInfo.vad_dump(self.context, proc, vad, self.open) + file_handle = vadinfo.VadInfo.vad_dump( + self.context, proc, vad, self.open + ) file_handle.close() file_output = file_handle.preferred_filename except (exceptions.InvalidAddressException, OverflowError) as excp: - vollog.debug("Unable to dump PE with pid {0}.{1:#x}: {2}".format( - proc.UniqueProcessId, vad.get_start(), excp)) + vollog.debug( + "Unable to dump PE with pid {0}.{1:#x}: {2}".format( + proc.UniqueProcessId, vad.get_start(), excp + ) + ) - yield (0, (proc.UniqueProcessId, process_name, format_hints.Hex(vad.get_start()), - format_hints.Hex(vad.get_end()), vad.get_tag(), - vad.get_protection( - vadinfo.VadInfo.protect_values(self.context, kernel.layer_name, - kernel.symbol_table_name), - vadinfo.winnt_protections), vad.get_commit_charge(), vad.get_private_memory(), - file_output, format_hints.HexBytes(data), disasm)) + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(vad.get_start()), + format_hints.Hex(vad.get_end()), + vad.get_tag(), + vad.get_protection( + vadinfo.VadInfo.protect_values( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + ), + vadinfo.winnt_protections, + ), + vad.get_commit_charge(), + vad.get_private_memory(), + file_output, + notes, + format_hints.HexBytes(data), + disasm, + ), + ) def run(self): - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) - kernel = self.context.modules[self.config['kernel']] + 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), ("Start VPN", format_hints.Hex), - ("End VPN", format_hints.Hex), ("Tag", str), ("Protection", str), - ("CommitCharge", int), ("PrivateMemory", int), ("File output", str), - ("Hexdump", format_hints.HexBytes), ("Disasm", interfaces.renderers.Disassembly)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Start VPN", format_hints.Hex), + ("End VPN", format_hints.Hex), + ("Tag", str), + ("Protection", str), + ("CommitCharge", int), + ("PrivateMemory", int), + ("File output", str), + ("Notes", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", interfaces.renderers.Disassembly), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/mbrscan.py b/volatility3/framework/plugins/windows/mbrscan.py index d064e7d29..e58ca8c24 100644 --- a/volatility3/framework/plugins/windows/mbrscan.py +++ b/volatility3/framework/plugins/windows/mbrscan.py @@ -16,6 +16,7 @@ from volatility3.framework.symbols.windows.extensions import mbr vollog = logging.getLogger(__name__) + class MBRScan(interfaces.plugins.PluginInterface): """Scans for and parses potential Master Boot Records (MBRs)""" @@ -23,178 +24,238 @@ class MBRScan(interfaces.plugins.PluginInterface): _version = (1, 0, 0) @classmethod - def get_requirements(cls)-> List[interfaces.configuration.RequirementInterface]: + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.BooleanRequirement(name = 'full', - description ="It analyzes and provides all the information in the partition entry and bootcode hexdump. (It returns a lot of information, so we recommend you render it in CSV.)", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.BooleanRequirement( + name="full", + description="It analyzes and provides all the information in the partition entry and bootcode hexdump. (It returns a lot of information, so we recommend you render it in CSV.)", + default=False, + optional=True, + ), ] @classmethod - def get_hash(cls, data:bytes) -> str: + def get_hash(cls, data: bytes) -> str: return hashlib.md5(data).hexdigest() def _generator(self) -> Iterator[Tuple]: - kernel = self.context.modules[self.config['kernel']] - physical_layer_name = self.context.layers[kernel.layer_name].config.get('memory_layer', None) - + kernel = self.context.modules[self.config["kernel"]] + physical_layer_name = self.context.layers[kernel.layer_name].config.get( + "memory_layer", None + ) + # Decide of Memory Dump Architecture layer = self.context.layers[physical_layer_name] - architecture = "intel" if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) else "intel64" + architecture = ( + "intel" + if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + else "intel64" + ) # Read in the Symbol File - symbol_table = intermed.IntermediateSymbolTable.create(context = self.context, - config_path = self.config_path, - sub_path = "windows", - filename = "mbr", - class_types = { - 'PARTITION_TABLE': mbr.PARTITION_TABLE, - 'PARTITION_ENTRY': mbr.PARTITION_ENTRY - }) + symbol_table = intermed.IntermediateSymbolTable.create( + context=self.context, + config_path=self.config_path, + sub_path="windows", + filename="mbr", + class_types={ + "PARTITION_TABLE": mbr.PARTITION_TABLE, + "PARTITION_ENTRY": mbr.PARTITION_ENTRY, + }, + ) partition_table_object = symbol_table + constants.BANG + "PARTITION_TABLE" - + # Define Signature and Data Length mbr_signature = b"\x55\xAA" mbr_length = 0x200 bootcode_length = 0x1B8 # Scan the Layer for Raw Master Boot Record (MBR) and parse the fields - for offset, _value in layer.scan(context = self.context, scanner = scanners.MultiStringScanner(patterns = [mbr_signature])): + for offset, _value in layer.scan( + context=self.context, + scanner=scanners.MultiStringScanner(patterns=[mbr_signature]), + ): try: mbr_start_offset = offset - (mbr_length - len(mbr_signature)) - partition_table = self.context.object(partition_table_object, offset = mbr_start_offset, layer_name = layer.name) + partition_table = self.context.object( + partition_table_object, + offset=mbr_start_offset, + layer_name=layer.name, + ) # Extract only BootCode - full_mbr = layer.read(mbr_start_offset, mbr_length, pad = True) + full_mbr = layer.read(mbr_start_offset, mbr_length, pad=True) bootcode = full_mbr[:bootcode_length] - + all_zeros = None if bootcode: all_zeros = bootcode.count(b"\x00") == len(bootcode) if not all_zeros: - partition_entries = [ - partition_table.FirstEntry, partition_table.SecondEntry, - partition_table.ThirdEntry, partition_table.FourthEntry + partition_table.FirstEntry, + partition_table.SecondEntry, + partition_table.ThirdEntry, + partition_table.FourthEntry, ] if not self.config.get("full", True): - yield (0, ( - format_hints.Hex(offset), - partition_table.get_disk_signature(), - self.get_hash(bootcode), - self.get_hash(full_mbr), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - interfaces.renderers.Disassembly(bootcode, 0, architecture) - )) - else: - yield (0, ( - format_hints.Hex(offset), - partition_table.get_disk_signature(), - self.get_hash(bootcode), - self.get_hash(full_mbr), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - interfaces.renderers.Disassembly(bootcode, 0, architecture), - format_hints.HexBytes(bootcode) - )) - - for partition_index, partition_entry_object in enumerate(partition_entries, start=1): - - if not self.config.get("full", True): - yield (1, ( + yield ( + 0, + ( format_hints.Hex(offset), partition_table.get_disk_signature(), self.get_hash(bootcode), self.get_hash(full_mbr), - partition_index, - partition_entry_object.is_bootable(), - partition_entry_object.get_partition_type(), - format_hints.Hex(partition_entry_object.get_size_in_sectors()), - renderers.NotApplicableValue() - )) - else: - yield (1, ( - format_hints.Hex(offset), - partition_table.get_disk_signature(), - self.get_hash(bootcode), - self.get_hash(full_mbr), - partition_index, - partition_entry_object.is_bootable(), - format_hints.Hex(partition_entry_object.get_bootable_flag()), - partition_entry_object.get_partition_type(), - format_hints.Hex(partition_entry_object.PartitionType), - format_hints.Hex(partition_entry_object.get_starting_lba()), - partition_entry_object.get_starting_cylinder(), - partition_entry_object.get_starting_chs(), - partition_entry_object.get_starting_sector(), - partition_entry_object.get_ending_cylinder(), - partition_entry_object.get_ending_chs(), - partition_entry_object.get_ending_sector(), - format_hints.Hex(partition_entry_object.get_size_in_sectors()), renderers.NotApplicableValue(), - renderers.NotApplicableValue() - )) + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + interfaces.renderers.Disassembly( + bootcode, 0, architecture + ), + ), + ) + else: + yield ( + 0, + ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + interfaces.renderers.Disassembly( + bootcode, 0, architecture + ), + format_hints.HexBytes(bootcode), + ), + ) + + for partition_index, partition_entry_object in enumerate( + partition_entries, start=1 + ): + if not self.config.get("full", True): + yield ( + 1, + ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + partition_index, + partition_entry_object.is_bootable(), + partition_entry_object.get_partition_type(), + format_hints.Hex( + partition_entry_object.get_size_in_sectors() + ), + renderers.NotApplicableValue(), + ), + ) + else: + yield ( + 1, + ( + format_hints.Hex(offset), + partition_table.get_disk_signature(), + self.get_hash(bootcode), + self.get_hash(full_mbr), + partition_index, + partition_entry_object.is_bootable(), + format_hints.Hex( + partition_entry_object.get_bootable_flag() + ), + partition_entry_object.get_partition_type(), + format_hints.Hex( + partition_entry_object.PartitionType + ), + format_hints.Hex( + partition_entry_object.get_starting_lba() + ), + partition_entry_object.get_starting_cylinder(), + partition_entry_object.get_starting_chs(), + partition_entry_object.get_starting_sector(), + partition_entry_object.get_ending_cylinder(), + partition_entry_object.get_ending_chs(), + partition_entry_object.get_ending_sector(), + format_hints.Hex( + partition_entry_object.get_size_in_sectors() + ), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + ), + ) else: - vollog.log(constants.LOGLEVEL_VVVV, f"Not a valid MBR: Data all zeroed out : {format_hints.Hex(offset)}") + vollog.log( + constants.LOGLEVEL_VVVV, + f"Not a valid MBR: Data all zeroed out : {format_hints.Hex(offset)}", + ) continue - + except exceptions.PagedInvalidAddressException as excp: - vollog.log(constants.LOGLEVEL_VVVV, f"Invalid address identified in guessed MBR: {hex(excp.invalid_address)}") + vollog.log( + constants.LOGLEVEL_VVVV, + f"Invalid address identified in guessed MBR: {hex(excp.invalid_address)}", + ) continue - - def run(self)-> renderers.TreeGrid: + + def run(self) -> renderers.TreeGrid: if not self.config.get("full", True): - return renderers.TreeGrid([ - ("Potential MBR at Physical Offset", format_hints.Hex), - ("Disk Signature", str), - ("Bootcode MD5", str), - ("Full MBR MD5", str), - ("PartitionIndex", int), - ("Bootable", bool), - ("PartitionType", str), - ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly) - ], self._generator()) + return renderers.TreeGrid( + [ + ("Potential MBR at Physical Offset", format_hints.Hex), + ("Disk Signature", str), + ("Bootcode MD5", str), + ("Full MBR MD5", str), + ("PartitionIndex", int), + ("Bootable", bool), + ("PartitionType", str), + ("SectorInSize", format_hints.Hex), + ("Disasm", interfaces.renderers.Disassembly), + ], + self._generator(), + ) else: - return renderers.TreeGrid([ - ("Potential MBR at Physical Offset", format_hints.Hex), - ("Disk Signature", str), - ("Bootcode MD5", str), - ("Full MBR MD5", str), - ("PartitionIndex", int), - ("Bootable", bool), - ("BootFlag", format_hints.Hex), - ("PartitionType", str), - ("PartitionTypeRaw", format_hints.Hex), - ("StartingLBA", format_hints.Hex), - ("StartingCylinder", int), - ("StartingCHS", int), - ("StartingSector", int), - ("EndingCylinder", int), - ("EndingCHS", int), - ("EndingSector", int), - ("SectorInSize", format_hints.Hex), - ("Disasm", interfaces.renderers.Disassembly), - ("Bootcode", format_hints.HexBytes) - ], self._generator()) + return renderers.TreeGrid( + [ + ("Potential MBR at Physical Offset", format_hints.Hex), + ("Disk Signature", str), + ("Bootcode MD5", str), + ("Full MBR MD5", str), + ("PartitionIndex", int), + ("Bootable", bool), + ("BootFlag", format_hints.Hex), + ("PartitionType", str), + ("PartitionTypeRaw", format_hints.Hex), + ("StartingLBA", format_hints.Hex), + ("StartingCylinder", int), + ("StartingCHS", int), + ("StartingSector", int), + ("EndingCylinder", int), + ("EndingCHS", int), + ("EndingSector", int), + ("SectorInSize", format_hints.Hex), + ("Disasm", interfaces.renderers.Disassembly), + ("Bootcode", format_hints.HexBytes), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/memmap.py b/volatility3/framework/plugins/windows/memmap.py index 74e5885da..b5c9a211e 100644 --- a/volatility3/framework/plugins/windows/memmap.py +++ b/volatility3/framework/plugins/windows/memmap.py @@ -22,19 +22,27 @@ class Memmap(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.IntRequirement(name = 'pid', - description = "Process ID to include (all other processes are excluded)", - optional = True), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed memory segments", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.IntRequirement( + name="pid", + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed memory segments", + default=False, + optional=True, + ), ] - def _generator(self, procs): for proc in procs: pid = "Unknown" @@ -44,47 +52,73 @@ class Memmap(interfaces.plugins.PluginInterface): proc_layer_name = proc.add_process_layer() proc_layer = self.context.layers[proc_layer_name] except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(pid, excp.invalid_address, - excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + pid, excp.invalid_address, excp.layer_name + ) + ) continue - if self.config['dump']: + if self.config["dump"]: file_handle = self.open(f"pid.{pid}.dmp") else: # Ensure the file isn't actually created if not needed file_handle = contextlib.ExitStack() with file_handle as file_data: file_offset = 0 - for mapval in proc_layer.mapping(0x0, proc_layer.maximum_address, ignore_errors = True): + for mapval in proc_layer.mapping( + 0x0, proc_layer.maximum_address, ignore_errors=True + ): offset, size, mapped_offset, mapped_size, maplayer = mapval file_output = "Disabled" - if self.config['dump']: + if self.config["dump"]: try: - data = proc_layer.read(offset, size, pad = True) + data = proc_layer.read(offset, size, pad=True) file_data.write(data) file_output = file_handle.preferred_filename except exceptions.InvalidAddressException: file_output = "Error outputting to file" - vollog.debug("Unable to write {}'s address {} to {}".format( - proc_layer_name, offset, file_handle.preferred_filename)) + vollog.debug( + "Unable to write {}'s address {} to {}".format( + proc_layer_name, + offset, + file_handle.preferred_filename, + ) + ) - yield (0, (format_hints.Hex(offset), format_hints.Hex(mapped_offset), - format_hints.Hex(mapped_size), - format_hints.Hex(file_offset), file_output)) + yield ( + 0, + ( + format_hints.Hex(offset), + format_hints.Hex(mapped_offset), + format_hints.Hex(mapped_size), + format_hints.Hex(file_offset), + file_output, + ), + ) file_offset += mapped_size offset += mapped_size def run(self): - filter_func = pslist.PsList.create_pid_filter([self.config.get('pid', None)]) - kernel = self.context.modules[self.config['kernel']] + filter_func = pslist.PsList.create_pid_filter([self.config.get("pid", None)]) + kernel = self.context.modules[self.config["kernel"]] - return renderers.TreeGrid([("Virtual", format_hints.Hex), ("Physical", format_hints.Hex), - ("Size", format_hints.Hex), ("Offset in File", format_hints.Hex), - ("File output", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("Virtual", format_hints.Hex), + ("Physical", format_hints.Hex), + ("Size", format_hints.Hex), + ("Offset in File", format_hints.Hex), + ("File output", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/mftscan.py b/volatility3/framework/plugins/windows/mftscan.py index 654e26db7..91a2e9152 100644 --- a/volatility3/framework/plugins/windows/mftscan.py +++ b/volatility3/framework/plugins/windows/mftscan.py @@ -1,7 +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 contextlib import datetime import logging @@ -23,57 +23,60 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_requirements(cls): return [ - requirements.TranslationLayerRequirement(name = 'primary', - description = 'Memory layer for the kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'yarascanner', component = yarascan.YaraScanner, - version = (2, 0, 0)), + requirements.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), ] def _generator(self): - layer = self.context.layers[self.config['primary']] + layer = self.context.layers[self.config["primary"]] # Yara Rule to scan for MFT Header Signatures - rules = yarascan.YaraScan.process_yara_options({'yara_rules': '/FILE0|FILE\*|BAAD/'}) + rules = yarascan.YaraScan.process_yara_options( + {"yara_rules": "/FILE0|FILE\*|BAAD/"} + ) # Read in the Symbol File - symbol_table = intermed.IntermediateSymbolTable.create(context = self.context, - config_path = self.config_path, - sub_path = "windows", - filename = "mft", - class_types = { - 'FILE_NAME_ENTRY': mft.MFTFileName, - 'MFT_ENTRY': mft.MFTEntry - }) + symbol_table = intermed.IntermediateSymbolTable.create( + context=self.context, + config_path=self.config_path, + sub_path="windows", + filename="mft", + class_types={"FILE_NAME_ENTRY": mft.MFTFileName, "MFT_ENTRY": mft.MFTEntry}, + ) # get each of the individual Field Sets mft_object = symbol_table + constants.BANG + "MFT_ENTRY" attribute_object = symbol_table + constants.BANG + "ATTRIBUTE" - header_object = symbol_table + constants.BANG + "ATTR_HEADER" si_object = symbol_table + constants.BANG + "STANDARD_INFORMATION_ENTRY" fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY" # Scan the layer for Raw MFT records and parse the fields - for offset, _rule_name, _name, _value in layer.scan(context = self.context, - scanner = yarascan.YaraScanner(rules = rules)): - try: - mft_record = self.context.object(mft_object, offset = offset, layer_name = layer.name) + for offset, _rule_name, _name, _value in layer.scan( + context=self.context, scanner=yarascan.YaraScanner(rules=rules) + ): + with contextlib.suppress(exceptions.PagedInvalidAddressException): + mft_record = self.context.object( + mft_object, offset=offset, layer_name=layer.name + ) # We will update this on each pass in the next loop and use it as the new offset. attr_base_offset = mft_record.FirstAttrOffset - - attr_header = self.context.object(header_object, - offset = offset + attr_base_offset, - layer_name = layer.name) + attr = self.context.object( + attribute_object, + offset=offset + attr_base_offset, + layer_name=layer.name, + ) # There is no field that has a count of Attributes # Keep Attempting to read attributes until we get an invalid attr_header.AttrType - while attr_header.AttrType.is_valid_choice: - vollog.debug(f"Attr Type: {attr_header.AttrType.lookup()}") - - # Offset past the headers to the attribute data - attr_data_offset = offset + attr_base_offset + self.context.symbol_space.get_type( - attribute_object).relative_child_offset("Attr_Data") + while attr.Attr_Header.AttrType.is_valid_choice: + vollog.debug(f"Attr Type: {attr.Attr_Header.AttrType.lookup()}") # MFT Flags determine the file type or dir # If we don't have a valid enum, coerce to hex so we can keep the record @@ -83,17 +86,16 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): mft_flag = hex(mft_record.Flags) # Standard Information Attribute - if attr_header.AttrType.lookup() == 'STANDARD_INFORMATION': - attr_data = self.context.object(si_object, offset = attr_data_offset, layer_name = layer.name) - + if attr.Attr_Header.AttrType.lookup() == "STANDARD_INFORMATION": + attr_data = attr.Attr_Data.cast(si_object) yield 0, ( - format_hints.Hex(attr_data_offset), + format_hints.Hex(attr_data.vol.offset), mft_record.get_signature(), mft_record.RecordNumber, mft_record.LinkCount, mft_flag, renderers.NotApplicableValue(), - attr_header.AttrType.lookup(), + attr.Attr_Header.AttrType.lookup(), conversion.wintime_to_datetime(attr_data.CreationTime), conversion.wintime_to_datetime(attr_data.ModifiedTime), conversion.wintime_to_datetime(attr_data.UpdatedTime), @@ -102,8 +104,8 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): ) # File Name Attribute - if attr_header.AttrType.lookup() == 'FILE_NAME': - attr_data = self.context.object(fn_object, offset = attr_data_offset, layer_name = layer.name) + if attr.Attr_Header.AttrType.lookup() == "FILE_NAME": + attr_data = attr.Attr_Data.cast(fn_object) file_name = attr_data.get_full_name() # If we don't have a valid enum, coerce to hex so we can keep the record @@ -112,34 +114,39 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except ValueError: permissions = hex(attr_data.Flags) - yield 1, (format_hints.Hex(attr_data_offset), mft_record.get_signature(), - mft_record.RecordNumber, mft_record.LinkCount, mft_flag, permissions, - attr_header.AttrType.lookup(), - conversion.wintime_to_datetime(attr_data.CreationTime), - conversion.wintime_to_datetime(attr_data.ModifiedTime), - conversion.wintime_to_datetime(attr_data.UpdatedTime), - conversion.wintime_to_datetime(attr_data.AccessedTime), file_name) + yield 1, ( + format_hints.Hex(attr_data.vol.offset), + mft_record.get_signature(), + mft_record.RecordNumber, + mft_record.LinkCount, + mft_flag, + permissions, + attr.Attr_Header.AttrType.lookup(), + conversion.wintime_to_datetime(attr_data.CreationTime), + conversion.wintime_to_datetime(attr_data.ModifiedTime), + conversion.wintime_to_datetime(attr_data.UpdatedTime), + conversion.wintime_to_datetime(attr_data.AccessedTime), + file_name, + ) # If there's no advancement the loop will never end, so break it now - if attr_header.Length == 0: + if attr.Attr_Header.Length == 0: break # Update the base offset to point to the next attribute - attr_base_offset += attr_header.Length - # Get the next attribute - attr_header = self.context.object(header_object, - offset = offset + attr_base_offset, - layer_name = layer.name) - - except exceptions.PagedInvalidAddressException: - pass + attr_base_offset += attr.Attr_Header.Length + attr = self.context.object( + attribute_object, + offset=offset + attr_base_offset, + layer_name=layer.name, + ) def generate_timeline(self): for row in self._generator(): _depth, row_data = row # Only Output FN Records - if row_data[6] == 'FILE_NAME': + if row_data[6] == "FILE_NAME": filename = row_data[-1] description = f"MFT FILE_NAME entry for {filename}" yield (description, timeliner.TimeLinerType.CREATED, row_data[7]) @@ -148,17 +155,156 @@ class MFTScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield (description, timeliner.TimeLinerType.ACCESSED, row_data[10]) def run(self): - return renderers.TreeGrid([ - ('Offset', format_hints.Hex), - ('Record Type', str), - ('Record Number', int), - ('Link Count', int), - ('MFT Type', str), - ('Permissions', str), - ('Attribute Type', str), - ('Created', datetime.datetime), - ('Modified', datetime.datetime), - ('Updated', datetime.datetime), - ('Accessed', datetime.datetime), - ('Filename', str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Record Type", str), + ("Record Number", int), + ("Link Count", int), + ("MFT Type", str), + ("Permissions", str), + ("Attribute Type", str), + ("Created", datetime.datetime), + ("Modified", datetime.datetime), + ("Updated", datetime.datetime), + ("Accessed", datetime.datetime), + ("Filename", str), + ], + self._generator(), + ) + + +class ADS(interfaces.plugins.PluginInterface): + """Scans for Alternate Data Stream""" + + _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.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), + ] + + def _generator(self): + layer = self.context.layers[self.config["primary"]] + + # Yara Rule to scan for MFT Header Signatures + rules = yarascan.YaraScan.process_yara_options( + {"yara_rules": "/FILE0|FILE\*|BAAD/"} + ) + + # Read in the Symbol File + symbol_table = intermed.IntermediateSymbolTable.create( + context=self.context, + config_path=self.config_path, + sub_path="windows", + filename="mft", + class_types={ + "MFT_ENTRY": mft.MFTEntry, + "FILE_NAME_ENTRY": mft.MFTFileName, + "ATTRIBUTE": mft.MFTAttribute, + }, + ) + + # get each of the individual Field Sets + mft_object = symbol_table + constants.BANG + "MFT_ENTRY" + attribute_object = symbol_table + constants.BANG + "ATTRIBUTE" + fn_object = symbol_table + constants.BANG + "FILE_NAME_ENTRY" + + # Scan the layer for Raw MFT records and parse the fields + for offset, _rule_name, _name, _value in layer.scan( + context=self.context, scanner=yarascan.YaraScanner(rules=rules) + ): + with contextlib.suppress(exceptions.PagedInvalidAddressException): + mft_record = self.context.object( + mft_object, offset=offset, layer_name=layer.name + ) + # We will update this on each pass in the next loop and use it as the new offset. + attr_base_offset = mft_record.FirstAttrOffset + + attr = self.context.object( + attribute_object, + offset=offset + attr_base_offset, + layer_name=layer.name, + ) + + # There is no field that has a count of Attributes + # Keep Attempting to read attributes until we get an invalid attr.AttrType + is_ads = False + file_name = renderers.NotAvailableValue + # The First $DATA Attr is the 'principal' file itself not the ADS + while attr.Attr_Header.AttrType.is_valid_choice: + if attr.Attr_Header.AttrType.lookup() == "FILE_NAME": + attr_data = attr.Attr_Data.cast(fn_object) + file_name = attr_data.get_full_name() + if attr.Attr_Header.AttrType.lookup() == "DATA": + if is_ads: + if not attr.Attr_Header.NonResidentFlag: + # Resident files are the most interesting. + if attr.Attr_Header.NameLength > 0: + ads_name = attr.get_resident_filename() + if not ads_name: + ads_name = renderers.NotAvailableValue + + content = attr.get_resident_filecontent() + if content: + # Preparing for Disassembly + disasm = interfaces.renderers.BaseAbsentValue + architecture = layer.metadata.get( + "architecture", None + ) + if architecture: + disasm = interfaces.renderers.Disassembly( + content, 0, architecture.lower() + ) + else: + content = renderers.NotAvailableValue + disasm = interfaces.renderers.BaseAbsentValue + + yield 0, ( + format_hints.Hex(attr_data.vol.offset), + mft_record.get_signature(), + mft_record.RecordNumber, + attr.Attr_Header.AttrType.lookup(), + file_name, + ads_name, + format_hints.HexBytes(content), + disasm, + ) + else: + is_ads = True + + # If there's no advancement the loop will never end, so break it now + if attr.Attr_Header.Length == 0: + break + + # Update the base offset to point to the next attribute + attr_base_offset += attr.Attr_Header.Length + # Get the next attribute + attr = self.context.object( + attribute_object, + offset=offset + attr_base_offset, + layer_name=layer.name, + ) + + def run(self): + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Record Type", str), + ("Record Number", int), + ("MFT Type", str), + ("Filename", str), + ("ADS Filename", str), + ("Hexdump", format_hints.HexBytes), + ("Disasm", interfaces.renderers.Disassembly), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/modscan.py b/volatility3/framework/plugins/windows/modscan.py index b661d71d7..99fadac07 100644 --- a/volatility3/framework/plugins/windows/modscan.py +++ b/volatility3/framework/plugins/windows/modscan.py @@ -23,25 +23,35 @@ class ModScan(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'poolerscanner', - component = poolscanner.PoolScanner, - version = (1, 0, 0)), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed modules", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="dlllist", component=dlllist.DllList, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), ] @classmethod - def scan_modules(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_modules( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for modules using the poolscanner module and constraints. Args: @@ -53,19 +63,24 @@ class ModScan(interfaces.plugins.PluginInterface): A list of Driver objects as found from the `layer_name` layer based on Driver pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'MmLd']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"MmLd"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object @classmethod - def get_session_layers(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - pids: List[int] = None) -> Generator[str, None, None]: + def get_session_layers( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + pids: List[int] = None, + ) -> Generator[str, None, None]: """Build a cache of possible virtual layers, in priority starting with the primary/kernel layer. Then keep one layer per session by cycling through the process list. @@ -82,10 +97,12 @@ class ModScan(interfaces.plugins.PluginInterface): seen_ids: List[interfaces.objects.ObjectInterface] = [] filter_func = pslist.PsList.create_pid_filter(pids or []) - for proc in pslist.PsList.list_processes(context = context, - layer_name = layer_name, - symbol_table = symbol_table, - filter_func = filter_func): + for proc in pslist.PsList.list_processes( + context=context, + layer_name=layer_name, + symbol_table=symbol_table, + filter_func=filter_func, + ): proc_id = "Unknown" try: proc_id = proc.UniqueProcessId @@ -93,9 +110,11 @@ class ModScan(interfaces.plugins.PluginInterface): # create the session space object in the process' own layer. # not all processes have a valid session pointer. - session_space = context.object(symbol_table + constants.BANG + "_MM_SESSION_SPACE", - layer_name = layer_name, - offset = proc.Session) + session_space = context.object( + symbol_table + constants.BANG + "_MM_SESSION_SPACE", + layer_name=layer_name, + offset=proc.Session, + ) if session_space.SessionId in seen_ids: continue @@ -104,7 +123,9 @@ class ModScan(interfaces.plugins.PluginInterface): vollog.log( constants.LOGLEVEL_VVV, "Process {} does not have a valid Session or a layer could not be constructed for it".format( - proc_id)) + proc_id + ), + ) continue # save the layer if we haven't seen the session yet @@ -112,8 +133,12 @@ class ModScan(interfaces.plugins.PluginInterface): yield proc_layer_name @classmethod - def find_session_layer(cls, context: interfaces.context.ContextInterface, session_layers: Iterable[str], - base_address: int): + def find_session_layer( + cls, + context: interfaces.context.ContextInterface, + session_layers: Iterable[str], + base_address: int, + ): """Given a base address and a list of layer names, find a layer that can access the specified address. @@ -135,17 +160,20 @@ class ModScan(interfaces.plugins.PluginInterface): return None def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - session_layers = list(self.get_session_layers(self.context, kernel.layer_name, kernel.symbol_table_name)) - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) - - for mod in self.scan_modules(self.context, kernel.layer_name, kernel.symbol_table_name): + session_layers = list( + self.get_session_layers( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + ) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + for mod in self.scan_modules( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: BaseDllName = mod.BaseDllName.get_string() except exceptions.InvalidAddressException: @@ -157,23 +185,44 @@ class ModScan(interfaces.plugins.PluginInterface): FullDllName = "" file_output = "Disabled" - if self.config['dump']: - - session_layer_name = self.find_session_layer(self.context, session_layers, mod.DllBase) + if self.config["dump"]: + session_layer_name = self.find_session_layer( + self.context, session_layers, mod.DllBase + ) file_output = f"Cannot find a viable session layer for {mod.DllBase:#x}" if session_layer_name: - file_handle = dlllist.DllList.dump_pe(self.context, - pe_table_name, - mod, - self.open, - layer_name = session_layer_name) + file_handle = dlllist.DllList.dump_pe( + self.context, + pe_table_name, + mod, + self.open, + layer_name=session_layer_name, + ) file_output = "Error outputting file" if file_handle: file_output = file_handle.preferred_filename - yield (0, (format_hints.Hex(mod.vol.offset), format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), BaseDllName, FullDllName, file_output)) + yield ( + 0, + ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output, + ), + ) def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Base", format_hints.Hex), ("Size", format_hints.Hex), - ("Name", str), ("Path", str), ("File output", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Base", format_hints.Hex), + ("Size", format_hints.Hex), + ("Name", str), + ("Path", str), + ("File output", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index 7b488a8eb..7dabf6954 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -25,26 +25,40 @@ class Modules(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed modules", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="dlllist", component=dlllist.DllList, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed modules", + default=False, + optional=True, + ), + requirements.StringRequirement( + name="name", + description="module name/sub string", + optional=True, + default=None, + ), ] def _generator(self): - kernel = self.context.modules[self.config['kernel']] - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) - - for mod in self.list_modules(self.context, kernel.layer_name, kernel.symbol_table_name): + kernel = self.context.modules[self.config["kernel"]] + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + for mod in self.list_modules( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: BaseDllName = mod.BaseDllName.get_string() except exceptions.InvalidAddressException: @@ -55,23 +69,39 @@ class Modules(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: FullDllName = "" + if self.config["name"] and self.config["name"] not in BaseDllName: + continue + file_output = "Disabled" - if self.config['dump']: - file_handle = dlllist.DllList.dump_pe(self.context, pe_table_name, mod, self.open) + if self.config["dump"]: + file_handle = dlllist.DllList.dump_pe( + self.context, pe_table_name, mod, self.open + ) file_output = "Error outputting file" if file_handle: file_handle.close() file_output = file_handle.preferred_filename - yield (0, (format_hints.Hex(mod.vol.offset), format_hints.Hex(mod.DllBase), - format_hints.Hex(mod.SizeOfImage), BaseDllName, FullDllName, file_output)) + yield ( + 0, + ( + format_hints.Hex(mod.vol.offset), + format_hints.Hex(mod.DllBase), + format_hints.Hex(mod.SizeOfImage), + BaseDllName, + FullDllName, + file_output, + ), + ) @classmethod - def get_session_layers(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - pids: List[int] = None) -> Generator[str, None, None]: + def get_session_layers( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + pids: List[int] = None, + ) -> Generator[str, None, None]: """Build a cache of possible virtual layers, in priority starting with the primary/kernel layer. Then keep one layer per session by cycling through the process list. @@ -88,10 +118,12 @@ class Modules(interfaces.plugins.PluginInterface): seen_ids: List[interfaces.objects.ObjectInterface] = [] filter_func = pslist.PsList.create_pid_filter(pids or []) - for proc in pslist.PsList.list_processes(context = context, - layer_name = layer_name, - symbol_table = symbol_table, - filter_func = filter_func): + for proc in pslist.PsList.list_processes( + context=context, + layer_name=layer_name, + symbol_table=symbol_table, + filter_func=filter_func, + ): proc_id = "Unknown" try: proc_id = proc.UniqueProcessId @@ -99,9 +131,11 @@ class Modules(interfaces.plugins.PluginInterface): # create the session space object in the process' own layer. # not all processes have a valid session pointer. - session_space = context.object(symbol_table + constants.BANG + "_MM_SESSION_SPACE", - layer_name = layer_name, - offset = proc.Session) + session_space = context.object( + symbol_table + constants.BANG + "_MM_SESSION_SPACE", + layer_name=layer_name, + offset=proc.Session, + ) if session_space.SessionId in seen_ids: continue @@ -110,7 +144,9 @@ class Modules(interfaces.plugins.PluginInterface): vollog.log( constants.LOGLEVEL_VVV, "Process {} does not have a valid Session or a layer could not be constructed for it".format( - proc_id)) + proc_id + ), + ) continue # save the layer if we haven't seen the session yet @@ -118,8 +154,12 @@ class Modules(interfaces.plugins.PluginInterface): yield proc_layer_name @classmethod - def find_session_layer(cls, context: interfaces.context.ContextInterface, session_layers: Iterable[str], - base_address: int): + def find_session_layer( + cls, + context: interfaces.context.ContextInterface, + session_layers: Iterable[str], + base_address: int, + ): """Given a base address and a list of layer names, find a layer that can access the specified address. @@ -141,8 +181,12 @@ class Modules(interfaces.plugins.PluginInterface): return None @classmethod - def list_modules(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> Iterable[interfaces.objects.ObjectInterface]: + def list_modules( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the modules in the primary layer. Args: @@ -154,8 +198,8 @@ class Modules(interfaces.plugins.PluginInterface): A list of Modules as retrieved from PsLoadedModuleList """ - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) try: # use this type if its available (starting with windows 10) @@ -166,13 +210,24 @@ class Modules(interfaces.plugins.PluginInterface): type_name = ldr_entry_type.type_name.split(constants.BANG)[1] list_head = ntkrnlmp.get_symbol("PsLoadedModuleList").address - list_entry = ntkrnlmp.object(object_type = "_LIST_ENTRY", offset = list_head) + list_entry = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=list_head) reloff = ldr_entry_type.relative_child_offset("InLoadOrderLinks") - module = ntkrnlmp.object(object_type = type_name, offset = list_entry.vol.offset - reloff, absolute = True) + module = ntkrnlmp.object( + object_type=type_name, offset=list_entry.vol.offset - reloff, absolute=True + ) for mod in module.InLoadOrderLinks: yield mod def run(self): - return renderers.TreeGrid([("Offset", format_hints.Hex), ("Base", format_hints.Hex), ("Size", format_hints.Hex), - ("Name", str), ("Path", str), ("File output", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Base", format_hints.Hex), + ("Size", format_hints.Hex), + ("Name", str), + ("Path", str), + ("File output", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/mutantscan.py b/volatility3/framework/plugins/windows/mutantscan.py index 29e27c9b1..64d3b5470 100644 --- a/volatility3/framework/plugins/windows/mutantscan.py +++ b/volatility3/framework/plugins/windows/mutantscan.py @@ -18,17 +18,23 @@ class MutantScan(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + ), ] @classmethod - def scan_mutants(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_mutants( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for mutants using the poolscanner module and constraints. Args: @@ -40,18 +46,22 @@ class MutantScan(interfaces.plugins.PluginInterface): A list of Mutant objects found by scanning memory for the Mutant pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'Mut\xe1', b'Muta']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"Mut\xe1", b"Muta"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config['kernel']] - - for mutant in self.scan_mutants(self.context, kernel.layer_name, kernel.symbol_table_name): + kernel = self.context.modules[self.config["kernel"]] + for mutant in self.scan_mutants( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: name = mutant.get_name() except (ValueError, exceptions.InvalidAddressException): @@ -60,7 +70,10 @@ class MutantScan(interfaces.plugins.PluginInterface): yield (0, (format_hints.Hex(mutant.vol.offset), name)) def run(self): - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("Name", str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/netscan.py b/volatility3/framework/plugins/windows/netscan.py index 16301bdd8..62ead3ab7 100644 --- a/volatility3/framework/plugins/windows/netscan.py +++ b/volatility3/framework/plugins/windows/netscan.py @@ -28,24 +28,32 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'poolscanner', - component = poolscanner.PoolScanner, - version = (1, 0, 0)), - requirements.VersionRequirement(name = 'info', component = info.Info, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'verinfo', component = verinfo.VerInfo, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="poolscanner", component=poolscanner.PoolScanner, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) + ), requirements.BooleanRequirement( - name = 'include-corrupt', - description = - "Radically eases result validation. This will show partially overwritten data. WARNING: the results are likely to include garbage and/or corrupt data. Be cautious!", - default = False, - optional = True), + name="include-corrupt", + description="Radically eases result validation. This will show partially overwritten data. WARNING: the results are likely to include garbage and/or corrupt data. Be cautious!", + default=False, + optional=True, + ), ] @staticmethod - def create_netscan_constraints(context: interfaces.context.ContextInterface, - symbol_table: str) -> List[poolscanner.PoolConstraint]: + def create_netscan_constraints( + context: interfaces.context.ContextInterface, symbol_table: str + ) -> List[poolscanner.PoolConstraint]: """Creates a list of Pool Tag Constraints for network objects. Args: @@ -56,33 +64,49 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): The list containing the built constraints. """ - tcpl_size = context.symbol_space.get_type(symbol_table + constants.BANG + "_TCP_LISTENER").size - tcpe_size = context.symbol_space.get_type(symbol_table + constants.BANG + "_TCP_ENDPOINT").size - udpa_size = context.symbol_space.get_type(symbol_table + constants.BANG + "_UDP_ENDPOINT").size + tcpl_size = context.symbol_space.get_type( + symbol_table + constants.BANG + "_TCP_LISTENER" + ).size + tcpe_size = context.symbol_space.get_type( + symbol_table + constants.BANG + "_TCP_ENDPOINT" + ).size + udpa_size = context.symbol_space.get_type( + symbol_table + constants.BANG + "_UDP_ENDPOINT" + ).size # ~ vollog.debug("Using pool size constraints: TcpL {}, TcpE {}, UdpA {}".format(tcpl_size, tcpe_size, udpa_size)) return [ # TCP listener - poolscanner.PoolConstraint(b'TcpL', - type_name = symbol_table + constants.BANG + "_TCP_LISTENER", - size = (tcpl_size, None), - page_type = poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE), + poolscanner.PoolConstraint( + b"TcpL", + type_name=symbol_table + constants.BANG + "_TCP_LISTENER", + size=(tcpl_size, None), + page_type=poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE, + ), # TCP Endpoint - poolscanner.PoolConstraint(b'TcpE', - type_name = symbol_table + constants.BANG + "_TCP_ENDPOINT", - size = (tcpe_size, None), - page_type = poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE), + poolscanner.PoolConstraint( + b"TcpE", + type_name=symbol_table + constants.BANG + "_TCP_ENDPOINT", + size=(tcpe_size, None), + page_type=poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE, + ), # UDP Endpoint - poolscanner.PoolConstraint(b'UdpA', - type_name = symbol_table + constants.BANG + "_UDP_ENDPOINT", - size = (udpa_size, None), - page_type = poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE) + poolscanner.PoolConstraint( + b"UdpA", + type_name=symbol_table + constants.BANG + "_UDP_ENDPOINT", + size=(udpa_size, None), + page_type=poolscanner.PoolType.NONPAGED | poolscanner.PoolType.FREE, + ), ] @classmethod - def determine_tcpip_version(cls, context: interfaces.context.ContextInterface, layer_name: str, - nt_symbol_table: str) -> Tuple[str, Type]: + def determine_tcpip_version( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbol_table: str, + ) -> Tuple[str, Type]: """Tries to determine which symbol filename to use for the image's tcpip driver. The logic is partially taken from the info plugin. Args: @@ -102,7 +126,9 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): is_64bit = symbols.symbol_table_is_64bit(context, nt_symbol_table) - is_18363_or_later = versions.is_win10_18363_or_later(context = context, symbol_table = nt_symbol_table) + is_18363_or_later = versions.is_win10_18363_or_later( + context=context, symbol_table=nt_symbol_table + ) if is_64bit: arch = "x64" @@ -119,15 +145,24 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): nt_minor_version = int(kuser.NtMinorVersion) except ValueError: # vers struct exists, but is not an int anymore? - raise NotImplementedError("Kernel Debug Structure version format not supported!") + raise NotImplementedError( + "Kernel Debug Structure version format not supported!" + ) except: # unsure what to raise here. Also, it might be useful to add some kind of fallback, # either to a user-provided version or to another method to determine tcpip.sys's version raise exceptions.VolatilityException( - "Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!") + "Kernel Debug Structure missing VERSION/KUSER structure, unable to determine Windows version!" + ) - vollog.debug("Determined OS Version: {}.{} {}.{}".format(kuser.NtMajorVersion, kuser.NtMinorVersion, - vers.MajorVersion, vers.MinorVersion)) + vollog.debug( + "Determined OS Version: {}.{} {}.{}".format( + kuser.NtMajorVersion, + kuser.NtMinorVersion, + vers.MajorVersion, + vers.MinorVersion, + ) + ) if nt_major_version == 10 and arch == "x64": # win10 x64 has an additional class type we have to include. @@ -159,7 +194,7 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): (10, 0, 17134, 0): "netscan-win10-17134-x86", (10, 0, 17763, 0): "netscan-win10-17134-x86", (10, 0, 18362, 0): "netscan-win10-17134-x86", - (10, 0, 18363, 0): "netscan-win10-17134-x86" + (10, 0, 18363, 0): "netscan-win10-17134-x86", } else: version_dict = { @@ -182,11 +217,11 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): (10, 0, 17763, 0): "netscan-win10-17763-x64", (10, 0, 18362, 0): "netscan-win10-18362-x64", (10, 0, 18363, 0): "netscan-win10-18363-x64", - (10, 0, 19041, 0): "netscan-win10-19041-x64" + (10, 0, 19041, 0): "netscan-win10-19041-x64", } # we do not need to check for tcpip's specific FileVersion in every case - tcpip_mod_version = 0 # keep it 0 as a default + tcpip_mod_version = 0 # keep it 0 as a default # special use cases @@ -195,25 +230,44 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # "10.0.18362.1198" with the last part being incremented. However, we can use # os_distinguisher to differentiate between 18362 and 18363 if vers_minor_version == 18362 and is_18363_or_later: - vollog.debug("Detected 18363 data structures: working with 18363 symbol table.") + vollog.debug( + "Detected 18363 data structures: working with 18363 symbol table." + ) vers_minor_version = 18363 # we need to define additional version numbers (which are then found via tcpip.sys's FileVersion header) in case there is # ambiguity _within_ an OS version. If such a version number (last number of the tuple) is defined for the current OS # we need to inspect tcpip.sys's headers to see if we can grab the precise version - if [ (a,b,c,d) for a, b, c, d in version_dict if (a,b,c) == (nt_major_version, nt_minor_version, vers_minor_version) and d != 0]: - vollog.debug("Requiring further version inspection due to OS version by checking tcpip.sys's FileVersion header") + if [ + (a, b, c, d) + for a, b, c, d in version_dict + if (a, b, c) == (nt_major_version, nt_minor_version, vers_minor_version) + and d != 0 + ]: + vollog.debug( + "Requiring further version inspection due to OS version by checking tcpip.sys's FileVersion header" + ) # the following is IntelLayer specific and might need to be adapted to other architectures. - physical_layer_name = context.layers[layer_name].config.get('memory_layer', None) + physical_layer_name = context.layers[layer_name].config.get( + "memory_layer", None + ) if physical_layer_name: - ver = verinfo.VerInfo.find_version_info(context, physical_layer_name, "tcpip.sys") + ver = verinfo.VerInfo.find_version_info( + context, physical_layer_name, "tcpip.sys" + ) if ver: tcpip_mod_version = ver[3] - vollog.debug("Determined tcpip.sys's FileVersion: {}".format(tcpip_mod_version)) + vollog.debug( + "Determined tcpip.sys's FileVersion: {}".format( + tcpip_mod_version + ) + ) else: vollog.debug("Could not determine tcpip.sys's FileVersion.") else: - vollog.debug("Unable to retrieve physical memory layer, skipping FileVersion check.") + vollog.debug( + "Unable to retrieve physical memory layer, skipping FileVersion check." + ) # when determining the symbol file we have to consider the following cases: # the determined version's symbol file is found by intermed.create -> proceed @@ -221,13 +275,19 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # the determined version has no mapped symbol file -> if win10 use latest, otherwise throw exc # windows version cannot be determined -> throw exc - filename = version_dict.get((nt_major_version, nt_minor_version, vers_minor_version, tcpip_mod_version)) + filename = version_dict.get( + (nt_major_version, nt_minor_version, vers_minor_version, tcpip_mod_version) + ) if not filename: # no match on filename means that we possibly have a version newer than those listed here. # try to grab the latest supported version of the current image NT version. If that symbol # version does not work, support has to be added manually. current_versions = [ - (nt_maj, nt_min, vers_min, tcpip_ver) for nt_maj, nt_min, vers_min, tcpip_ver in version_dict if nt_maj == nt_major_version and nt_min == nt_minor_version and tcpip_ver <= tcpip_mod_version + (nt_maj, nt_min, vers_min, tcpip_ver) + for nt_maj, nt_min, vers_min, tcpip_ver in version_dict + if nt_maj == nt_major_version + and nt_min == nt_minor_version + and tcpip_ver <= tcpip_mod_version ] current_versions.sort() @@ -236,19 +296,32 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): filename = version_dict.get(latest_version) - vollog.debug(f"Unable to find exact matching symbol file, going with latest: {filename}") + vollog.debug( + f"Unable to find exact matching symbol file, going with latest: {filename}" + ) else: - raise NotImplementedError("This version of Windows is not supported: {}.{} {}.{}!".format( - nt_major_version, nt_minor_version, vers.MajorVersion, vers_minor_version)) + raise NotImplementedError( + "This version of Windows is not supported: {}.{} {}.{}!".format( + nt_major_version, + nt_minor_version, + vers.MajorVersion, + vers_minor_version, + ) + ) vollog.debug(f"Determined symbol filename: {filename}") return filename, class_types @classmethod - def create_netscan_symbol_table(cls, context: interfaces.context.ContextInterface, layer_name: str, - nt_symbol_table: str, config_path: str) -> str: + def create_netscan_symbol_table( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbol_table: str, + config_path: str, + ) -> str: """Creates a symbol table for TCP Listeners and TCP/UDP Endpoints. Args: @@ -268,20 +341,23 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): nt_symbol_table, ) - return intermed.IntermediateSymbolTable.create(context, - config_path, - os.path.join("windows", "netscan"), - symbol_filename, - class_types = class_types, - table_mapping = table_mapping) + return intermed.IntermediateSymbolTable.create( + context, + config_path, + os.path.join("windows", "netscan"), + symbol_filename, + class_types=class_types, + table_mapping=table_mapping, + ) @classmethod - def scan(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbol_table: str, - netscan_symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbol_table: str, + netscan_symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for network objects using the poolscanner module and constraints. Args: @@ -296,23 +372,30 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): constraints = cls.create_netscan_constraints(context, netscan_symbol_table) - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, nt_symbol_table, constraints): - + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, nt_symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object def _generator(self, show_corrupt_results: Optional[bool] = None): - """ Generates the network objects for use in rendering. """ + """Generates the network objects for use in rendering.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - netscan_symbol_table = self.create_netscan_symbol_table(self.context, kernel.layer_name, - kernel.symbol_table_name, - self.config_path) + netscan_symbol_table = self.create_netscan_symbol_table( + self.context, kernel.layer_name, kernel.symbol_table_name, self.config_path + ) - for netw_obj in self.scan(self.context, kernel.layer_name, kernel.symbol_table_name, netscan_symbol_table): - - vollog.debug(f"Found netw obj @ 0x{netw_obj.vol.offset:2x} of assumed type {type(netw_obj)}") + for netw_obj in self.scan( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + netscan_symbol_table, + ): + vollog.debug( + f"Found netw obj @ 0x{netw_obj.vol.offset:2x} of assumed type {type(netw_obj)}" + ) # objects passed pool header constraints. check for additional constraints if strict flag is set. if not show_corrupt_results and not netw_obj.is_valid(): continue @@ -322,10 +405,22 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # For UdpA, the state is always blank and the remote end is asterisks for ver, laddr, _ in netw_obj.dual_stack_sockets(): - yield (0, (format_hints.Hex(netw_obj.vol.offset), "UDP" + ver, laddr, netw_obj.Port, "*", 0, "", - netw_obj.get_owner_pid() or renderers.UnreadableValue(), netw_obj.get_owner_procname() - or renderers.UnreadableValue(), netw_obj.get_create_time() - or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + "UDP" + ver, + laddr, + netw_obj.Port, + "*", + 0, + "", + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() + or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) elif isinstance(netw_obj, network._TCP_ENDPOINT): vollog.debug(f"Found _TCP_ENDPOINT @ 0x{netw_obj.vol.offset:2x}") @@ -341,11 +436,21 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except ValueError: state = renderers.UnreadableValue() - yield (0, (format_hints.Hex(netw_obj.vol.offset), proto, netw_obj.get_local_address() - or renderers.UnreadableValue(), netw_obj.LocalPort, netw_obj.get_remote_address() - or renderers.UnreadableValue(), netw_obj.RemotePort, state, netw_obj.get_owner_pid() - or renderers.UnreadableValue(), netw_obj.get_owner_procname() or renderers.UnreadableValue(), - netw_obj.get_create_time() or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + proto, + netw_obj.get_local_address() or renderers.UnreadableValue(), + netw_obj.LocalPort, + netw_obj.get_remote_address() or renderers.UnreadableValue(), + netw_obj.RemotePort, + state, + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) # check for isinstance of tcp listener last, because all other objects are inherited from here elif isinstance(netw_obj, network._TCP_LISTENER): @@ -353,13 +458,27 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # For TcpL, the state is always listening and the remote port is zero for ver, laddr, raddr in netw_obj.dual_stack_sockets(): - yield (0, (format_hints.Hex(netw_obj.vol.offset), "TCP" + ver, laddr, netw_obj.Port, raddr, 0, - "LISTENING", netw_obj.get_owner_pid() or renderers.UnreadableValue(), - netw_obj.get_owner_procname() or renderers.UnreadableValue(), netw_obj.get_create_time() - or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + "TCP" + ver, + laddr, + netw_obj.Port, + raddr, + 0, + "LISTENING", + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() + or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) else: # this should not happen therefore we log it. - vollog.debug(f"Found network object unsure of its type: {netw_obj} of type {type(netw_obj)}") + vollog.debug( + f"Found network object unsure of its type: {netw_obj} of type {type(netw_obj)}" + ) def generate_timeline(self): for row in self._generator(): @@ -368,28 +487,44 @@ class NetScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if not isinstance(row_data[9], datetime.datetime): continue row_data = [ - "N/A" if isinstance(i, renderers.UnreadableValue) or isinstance(i, renderers.UnparsableValue) else i + ( + "N/A" + if isinstance(i, renderers.UnreadableValue) + or isinstance(i, renderers.UnparsableValue) + else i + ) for i in row_data ] - description = "Network connection: Process {} {} Local Address {}:{} " \ - "Remote Address {}:{} State {} Protocol {} ".format(row_data[7], row_data[8], - row_data[2], row_data[3], - row_data[4], row_data[5], - row_data[6], row_data[1]) + description = ( + "Network connection: Process {} {} Local Address {}:{} " + "Remote Address {}:{} State {} Protocol {} ".format( + row_data[7], + row_data[8], + row_data[2], + row_data[3], + row_data[4], + row_data[5], + row_data[6], + row_data[1], + ) + ) yield (description, timeliner.TimeLinerType.CREATED, row_data[9]) def run(self): - show_corrupt_results = self.config.get('include-corrupt', None) + show_corrupt_results = self.config.get("include-corrupt", None) - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("Proto", str), - ("LocalAddr", str), - ("LocalPort", int), - ("ForeignAddr", str), - ("ForeignPort", int), - ("State", str), - ("PID", int), - ("Owner", str), - ("Created", datetime.datetime), - ], self._generator(show_corrupt_results = show_corrupt_results)) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Proto", str), + ("LocalAddr", str), + ("LocalPort", int), + ("ForeignAddr", str), + ("ForeignPort", int), + ("State", str), + ("PID", int), + ("Owner", str), + ("Created", datetime.datetime), + ], + self._generator(show_corrupt_results=show_corrupt_results), + ) diff --git a/volatility3/framework/plugins/windows/netstat.py b/volatility3/framework/plugins/windows/netstat.py index 486957565..24eb02018 100644 --- a/volatility3/framework/plugins/windows/netstat.py +++ b/volatility3/framework/plugins/windows/netstat.py @@ -26,23 +26,36 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'netscan', component = netscan.NetScan, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'modules', component = modules.Modules, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'pdbutil', component = pdbutil.PDBUtility, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'info', component = info.Info, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'verinfo', component = verinfo.VerInfo, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="netscan", component=netscan.NetScan, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="verinfo", component=verinfo.VerInfo, version=(1, 0, 0) + ), requirements.BooleanRequirement( - name = 'include-corrupt', - description = - "Radically eases result validation. This will show partially overwritten data. WARNING: the results are likely to include garbage and/or corrupt data. Be cautious!", - default = False, - optional = True), + name="include-corrupt", + description="Radically eases result validation. This will show partially overwritten data. WARNING: the results are likely to include garbage and/or corrupt data. Be cautious!", + default=False, + optional=True, + ), ] @classmethod - def _decode_pointer(self, value): + def _decode_pointer(cls, value): """Copied from `windows.handles`. Windows encodes pointers to objects and decodes them on the fly @@ -57,8 +70,13 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return value @classmethod - def read_pointer(cls, context: interfaces.context.ContextInterface, layer_name: str, offset: int, - length: int) -> int: + def read_pointer( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + offset: int, + length: int, + ) -> int: """Reads a pointer at a given offset and returns the address it points to. Args: @@ -74,8 +92,13 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return int.from_bytes(context.layers[layer_name].read(offset, length), "little") @classmethod - def parse_bitmap(cls, context: interfaces.context.ContextInterface, layer_name: str, bitmap_offset: int, - bitmap_size_in_byte: int) -> list: + def parse_bitmap( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + bitmap_offset: int, + bitmap_size_in_byte: int, + ) -> list: """Parses a given bitmap and looks for each occurrence of a 1. Args: @@ -97,14 +120,15 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return ret @classmethod - def enumerate_structures_by_port(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - net_symbol_table: str, - port: int, - port_pool_addr: int, - proto = "tcp") -> \ - Iterable[interfaces.objects.ObjectInterface]: + def enumerate_structures_by_port( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + net_symbol_table: str, + port: int, + port_pool_addr: int, + proto="tcp", + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all UDP Endpoints and TCP Listeners by parsing UdpPortPool and TcpPortPool. Args: @@ -120,23 +144,29 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ if proto == "tcp": obj_name = net_symbol_table + constants.BANG + "_TCP_LISTENER" - ptr_offset = context.symbol_space.get_type(obj_name).relative_child_offset("Next") + ptr_offset = context.symbol_space.get_type(obj_name).relative_child_offset( + "Next" + ) elif proto == "udp": obj_name = net_symbol_table + constants.BANG + "_UDP_ENDPOINT" - ptr_offset = context.symbol_space.get_type(obj_name).relative_child_offset("Next") + ptr_offset = context.symbol_space.get_type(obj_name).relative_child_offset( + "Next" + ) else: # invalid argument. - return + return None vollog.debug(f"Current Port: {port}") # the given port serves as a shifted index into the port pool lists list_index = port >> 8 - truncated_port = port & 0xff + truncated_port = port & 0xFF # constructing port_pool object here so callers don't have to - port_pool = context.object(net_symbol_table + constants.BANG + "_INET_PORT_POOL", - layer_name = layer_name, - offset = port_pool_addr) + port_pool = context.object( + net_symbol_table + constants.BANG + "_INET_PORT_POOL", + layer_name=layer_name, + offset=port_pool_addr, + ) # first, grab the given port's PortAssignment (`_PORT_ASSIGNMENT`) inpa = port_pool.PortAssignments[list_index] @@ -145,7 +175,7 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): assignment = inpa.InPaBigPoolBase.Assignments[truncated_port] if not assignment: - return + return None # the value within assignment.Entry is a) masked and b) points inside of the network object # first decode the pointer @@ -153,20 +183,28 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if netw_inside: # if the value is valid, calculate the actual object address by subtracting the offset - curr_obj = context.object(obj_name, layer_name = layer_name, offset = netw_inside - ptr_offset) + curr_obj = context.object( + obj_name, layer_name=layer_name, offset=netw_inside - ptr_offset + ) yield curr_obj # if the same port is used on different interfaces multiple objects are created # those can be found by following the pointer within the object's `Next` field until it is empty while curr_obj.Next: - curr_obj = context.object(obj_name, - layer_name = layer_name, - offset = cls._decode_pointer(curr_obj.Next) - ptr_offset) + curr_obj = context.object( + obj_name, + layer_name=layer_name, + offset=cls._decode_pointer(curr_obj.Next) - ptr_offset, + ) yield curr_obj @classmethod - def get_tcpip_module(cls, context: interfaces.context.ContextInterface, layer_name: str, - nt_symbols: str) -> Optional[interfaces.objects.ObjectInterface]: + def get_tcpip_module( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbols: str, + ) -> Optional[interfaces.objects.ObjectInterface]: """Uses `windows.modules` to find tcpip.sys in memory. Args: @@ -184,9 +222,15 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return None @classmethod - def parse_hashtable(cls, context: interfaces.context.ContextInterface, layer_name: str, ht_offset: int, - ht_length: int, alignment: int, - net_symbol_table: str) -> Generator[interfaces.objects.ObjectInterface, None, None]: + def parse_hashtable( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + ht_offset: int, + ht_length: int, + alignment: int, + net_symbol_table: str, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Parses a hashtable quick and dirty. Args: @@ -201,18 +245,25 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # we are looking for entries whose values are not their own address for index in range(ht_length): current_addr = ht_offset + index * alignment - current_pointer = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = current_addr) + current_pointer = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=current_addr, + ) # check if addr of pointer is equal to the value pointed to if current_pointer.vol.offset == current_pointer: continue yield current_pointer @classmethod - def parse_partitions(cls, context: interfaces.context.ContextInterface, layer_name: str, net_symbol_table: str, - tcpip_symbol_table: str, - tcpip_module_offset: int) -> Iterable[interfaces.objects.ObjectInterface]: + def parse_partitions( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + net_symbol_table: str, + tcpip_symbol_table: str, + tcpip_module_offset: int, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Parses tcpip.sys's PartitionTable containing established TCP connections. The amount of Partition depends on the value of the symbol `PartitionCount` and correlates with the maximum processor count (refer to Art of Memory Forensics, chapter 11). @@ -234,38 +285,66 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): obj_name = net_symbol_table + constants.BANG + "_TCP_ENDPOINT" # part_table_symbol is the offset within tcpip.sys which contains the address of the partition table itself - part_table_symbol = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + - "PartitionTable").address - part_count_symbol = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + - "PartitionCount").address + part_table_symbol = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "PartitionTable" + ).address + part_count_symbol = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "PartitionCount" + ).address - part_table_addr = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = tcpip_module_offset + part_table_symbol) + part_table_addr = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + part_table_symbol, + ) # part_table is the actual partition table offset and consists out of a dynamic amount of _PARTITION objects - part_table = context.object(net_symbol_table + constants.BANG + "_PARTITION_TABLE", - layer_name = layer_name, - offset = part_table_addr) - part_count = int.from_bytes(context.layers[layer_name].read(tcpip_module_offset + part_count_symbol, 1), - "little") + part_table = context.object( + net_symbol_table + constants.BANG + "_PARTITION_TABLE", + layer_name=layer_name, + offset=part_table_addr, + ) + part_count = int.from_bytes( + context.layers[layer_name].read(tcpip_module_offset + part_count_symbol, 1), + "little", + ) part_table.Partitions.count = part_count - vollog.debug("Found TCP connection PartitionTable @ 0x{:x} (partition count: {})".format( - part_table_addr, part_count)) - entry_offset = context.symbol_space.get_type(obj_name).relative_child_offset("ListEntry") + vollog.debug( + "Found TCP connection PartitionTable @ 0x{:x} (partition count: {})".format( + part_table_addr, part_count + ) + ) + entry_offset = context.symbol_space.get_type(obj_name).relative_child_offset( + "ListEntry" + ) for ctr, partition in enumerate(part_table.Partitions): vollog.debug(f"Parsing partition {ctr}") if partition.Endpoints.NumEntries > 0: - for endpoint_entry in cls.parse_hashtable(context, layer_name, partition.Endpoints.Directory, - partition.Endpoints.TableSize, alignment, net_symbol_table): - - endpoint = context.object(obj_name, layer_name = layer_name, offset = endpoint_entry - entry_offset) + for endpoint_entry in cls.parse_hashtable( + context, + layer_name, + partition.Endpoints.Directory, + partition.Endpoints.TableSize, + alignment, + net_symbol_table, + ): + endpoint = context.object( + obj_name, + layer_name=layer_name, + offset=endpoint_entry - entry_offset, + ) yield endpoint @classmethod - def create_tcpip_symbol_table(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str, - tcpip_module_offset: int, tcpip_module_size: int) -> str: + def create_tcpip_symbol_table( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + tcpip_module_offset: int, + tcpip_module_size: int, + ) -> str: """DEPRECATED: Use PDBUtility.symbol_table_from_pdb instead Creates symbol table for the current image's tcpip.sys driver. @@ -286,13 +365,24 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): vollog.debug( "Deprecation: This plugin uses netstat.create_tcpip_symbol_table instead of PDBUtility.symbol_table_from_pdb" ) - return pdbutil.PDBUtility.symbol_table_from_pdb(context, - interfaces.configuration.path_join(config_path, 'tcpip'), - layer_name, "tcpip.pdb", tcpip_module_offset, tcpip_module_size) + return pdbutil.PDBUtility.symbol_table_from_pdb( + context, + interfaces.configuration.path_join(config_path, "tcpip"), + layer_name, + "tcpip.pdb", + tcpip_module_offset, + tcpip_module_size, + ) @classmethod - def find_port_pools(cls, context: interfaces.context.ContextInterface, layer_name: str, net_symbol_table: str, - tcpip_symbol_table: str, tcpip_module_offset: int) -> Tuple[int, int]: + def find_port_pools( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + net_symbol_table: str, + tcpip_symbol_table: str, + tcpip_module_offset: int, + ) -> Tuple[int, int]: """Finds the given image's port pools. Older Windows versions (presumably < Win10 build 14251) use driver symbols called `UdpPortPool` and `TcpPortPool` which point towards the pools. Newer Windows versions use `UdpCompartmentSet` and `TcpCompartmentSet`, which we first have to translate into @@ -311,56 +401,79 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): if "UdpPortPool" in context.symbol_space[tcpip_symbol_table].symbols: # older Windows versions - upp_symbol = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + "UdpPortPool").address - upp_addr = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = tcpip_module_offset + upp_symbol) + upp_symbol = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "UdpPortPool" + ).address + upp_addr = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + upp_symbol, + ) - tpp_symbol = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + "TcpPortPool").address - tpp_addr = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = tcpip_module_offset + tpp_symbol) + tpp_symbol = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "TcpPortPool" + ).address + tpp_addr = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + tpp_symbol, + ) elif "UdpCompartmentSet" in context.symbol_space[tcpip_symbol_table].symbols: # newer Windows versions since 10.14xxx - ucs = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + "UdpCompartmentSet").address - tcs = context.symbol_space.get_symbol(tcpip_symbol_table + constants.BANG + "TcpCompartmentSet").address + ucs = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "UdpCompartmentSet" + ).address + tcs = context.symbol_space.get_symbol( + tcpip_symbol_table + constants.BANG + "TcpCompartmentSet" + ).address - ucs_offset = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = tcpip_module_offset + ucs) - tcs_offset = context.object(net_symbol_table + constants.BANG + "pointer", - layer_name = layer_name, - offset = tcpip_module_offset + tcs) + ucs_offset = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + ucs, + ) + tcs_offset = context.object( + net_symbol_table + constants.BANG + "pointer", + layer_name=layer_name, + offset=tcpip_module_offset + tcs, + ) - ucs_obj = context.object(net_symbol_table + constants.BANG + "_INET_COMPARTMENT_SET", - layer_name = layer_name, - offset = ucs_offset) + ucs_obj = context.object( + net_symbol_table + constants.BANG + "_INET_COMPARTMENT_SET", + layer_name=layer_name, + offset=ucs_offset, + ) upp_addr = ucs_obj.InetCompartment.ProtocolCompartment.PortPool - tcs_obj = context.object(net_symbol_table + constants.BANG + "_INET_COMPARTMENT_SET", - layer_name = layer_name, - offset = tcs_offset) + tcs_obj = context.object( + net_symbol_table + constants.BANG + "_INET_COMPARTMENT_SET", + layer_name=layer_name, + offset=tcs_offset, + ) tpp_addr = tcs_obj.InetCompartment.ProtocolCompartment.PortPool else: # this branch should not be reached. raise exceptions.SymbolError( - "UdpPortPool", tcpip_symbol_table, - f"Neither UdpPortPool nor UdpCompartmentSet found in {tcpip_symbol_table} table") + "UdpPortPool", + tcpip_symbol_table, + f"Neither UdpPortPool nor UdpCompartmentSet found in {tcpip_symbol_table} table", + ) vollog.debug(f"Found PortPools @ 0x{upp_addr:x} (UDP) && 0x{tpp_addr:x} (TCP)") return upp_addr, tpp_addr @classmethod - def list_sockets(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - nt_symbols: str, - net_symbol_table: str, - tcpip_module_offset: int, - tcpip_symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_sockets( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + nt_symbols: str, + net_symbol_table: str, + tcpip_module_offset: int, + tcpip_symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all UDP Endpoints, TCP Listeners and TCP Endpoints in the primary layer that are in tcpip.sys's UdpPortPool, TcpPortPool and TCP Endpoint partition table, respectively. @@ -377,27 +490,49 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ # first, TCP endpoints by parsing the partition table - for endpoint in cls.parse_partitions(context, layer_name, net_symbol_table, tcpip_symbol_table, - tcpip_module_offset): + for endpoint in cls.parse_partitions( + context, + layer_name, + net_symbol_table, + tcpip_symbol_table, + tcpip_module_offset, + ): yield endpoint # then, towards the UDP and TCP port pools # first, find their addresses - upp_addr, tpp_addr = cls.find_port_pools(context, layer_name, net_symbol_table, tcpip_symbol_table, - tcpip_module_offset) + upp_addr, tpp_addr = cls.find_port_pools( + context, + layer_name, + net_symbol_table, + tcpip_symbol_table, + tcpip_module_offset, + ) # create port pool objects at the detected address and parse the port bitmap - upp_obj = context.object(net_symbol_table + constants.BANG + "_INET_PORT_POOL", - layer_name = layer_name, - offset = upp_addr) - udpa_ports = cls.parse_bitmap(context, layer_name, upp_obj.PortBitMap.Buffer, - upp_obj.PortBitMap.SizeOfBitMap // 8) + upp_obj = context.object( + net_symbol_table + constants.BANG + "_INET_PORT_POOL", + layer_name=layer_name, + offset=upp_addr, + ) + udpa_ports = cls.parse_bitmap( + context, + layer_name, + upp_obj.PortBitMap.Buffer, + upp_obj.PortBitMap.SizeOfBitMap // 8, + ) - tpp_obj = context.object(net_symbol_table + constants.BANG + "_INET_PORT_POOL", - layer_name = layer_name, - offset = tpp_addr) - tcpl_ports = cls.parse_bitmap(context, layer_name, tpp_obj.PortBitMap.Buffer, - tpp_obj.PortBitMap.SizeOfBitMap // 8) + tpp_obj = context.object( + net_symbol_table + constants.BANG + "_INET_PORT_POOL", + layer_name=layer_name, + offset=tpp_addr, + ) + tcpl_ports = cls.parse_bitmap( + context, + layer_name, + tpp_obj.PortBitMap.Buffer, + tpp_obj.PortBitMap.SizeOfBitMap // 8, + ) vollog.debug(f"Found TCP Ports: {tcpl_ports}") vollog.debug(f"Found UDP Ports: {udpa_ports}") @@ -406,38 +541,55 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # port value can be 0, which we can skip if not port: continue - for obj in cls.enumerate_structures_by_port(context, layer_name, net_symbol_table, port, tpp_addr, "tcp"): + for obj in cls.enumerate_structures_by_port( + context, layer_name, net_symbol_table, port, tpp_addr, "tcp" + ): yield obj for port in udpa_ports: # same as above, skip port 0 if not port: continue - for obj in cls.enumerate_structures_by_port(context, layer_name, net_symbol_table, port, upp_addr, "udp"): + for obj in cls.enumerate_structures_by_port( + context, layer_name, net_symbol_table, port, upp_addr, "udp" + ): yield obj def _generator(self, show_corrupt_results: Optional[bool] = None): - """ Generates the network objects for use in rendering. """ + """Generates the network objects for use in rendering.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - netscan_symbol_table = netscan.NetScan.create_netscan_symbol_table(self.context, - kernel.layer_name, - kernel.symbol_table_name, - self.config_path) + netscan_symbol_table = netscan.NetScan.create_netscan_symbol_table( + self.context, kernel.layer_name, kernel.symbol_table_name, self.config_path + ) - tcpip_module = self.get_tcpip_module(self.context, kernel.layer_name, kernel.symbol_table_name) + tcpip_module = self.get_tcpip_module( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + if not tcpip_module: + vollog.error("Unable to locate symbols for the memory image's tcpip module") try: tcpip_symbol_table = pdbutil.PDBUtility.symbol_table_from_pdb( - self.context, interfaces.configuration.path_join(self.config_path, 'tcpip'), - kernel.layer_name, "tcpip.pdb", tcpip_module.DllBase, tcpip_module.SizeOfImage) + self.context, + interfaces.configuration.path_join(self.config_path, "tcpip"), + kernel.layer_name, + "tcpip.pdb", + tcpip_module.DllBase, + tcpip_module.SizeOfImage, + ) except exceptions.VolatilityException: - vollog.warning("Unable to locate symbols for the memory image's tcpip module") - - for netw_obj in self.list_sockets(self.context, kernel.layer_name, kernel.symbol_table_name, - netscan_symbol_table, tcpip_module.DllBase, tcpip_symbol_table): + vollog.error("Unable to locate symbols for the memory image's tcpip module") + for netw_obj in self.list_sockets( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + netscan_symbol_table, + tcpip_module.DllBase, + tcpip_symbol_table, + ): # objects passed pool header constraints. check for additional constraints if strict flag is set. if not show_corrupt_results and not netw_obj.is_valid(): continue @@ -447,10 +599,22 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # For UdpA, the state is always blank and the remote end is asterisks for ver, laddr, _ in netw_obj.dual_stack_sockets(): - yield (0, (format_hints.Hex(netw_obj.vol.offset), "UDP" + ver, laddr, netw_obj.Port, "*", 0, "", - netw_obj.get_owner_pid() or renderers.UnreadableValue(), netw_obj.get_owner_procname() - or renderers.UnreadableValue(), netw_obj.get_create_time() - or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + "UDP" + ver, + laddr, + netw_obj.Port, + "*", + 0, + "", + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() + or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) elif isinstance(netw_obj, network._TCP_ENDPOINT): vollog.debug(f"Found _TCP_ENDPOINT @ 0x{netw_obj.vol.offset:2x}") @@ -459,8 +623,11 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): elif netw_obj.get_address_family() == network.AF_INET6: proto = "TCPv6" else: - vollog.debug("TCP Endpoint @ 0x{:2x} has unknown address family 0x{:x}".format( - netw_obj.vol.offset, netw_obj.get_address_family())) + vollog.debug( + "TCP Endpoint @ 0x{:2x} has unknown address family 0x{:x}".format( + netw_obj.vol.offset, netw_obj.get_address_family() + ) + ) proto = "TCPv?" try: @@ -468,11 +635,21 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): except ValueError: state = renderers.UnreadableValue() - yield (0, (format_hints.Hex(netw_obj.vol.offset), proto, netw_obj.get_local_address() - or renderers.UnreadableValue(), netw_obj.LocalPort, netw_obj.get_remote_address() - or renderers.UnreadableValue(), netw_obj.RemotePort, state, netw_obj.get_owner_pid() - or renderers.UnreadableValue(), netw_obj.get_owner_procname() or renderers.UnreadableValue(), - netw_obj.get_create_time() or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + proto, + netw_obj.get_local_address() or renderers.UnreadableValue(), + netw_obj.LocalPort, + netw_obj.get_remote_address() or renderers.UnreadableValue(), + netw_obj.RemotePort, + state, + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) # check for isinstance of tcp listener last, because all other objects are inherited from here elif isinstance(netw_obj, network._TCP_LISTENER): @@ -480,47 +657,79 @@ class NetStat(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # For TcpL, the state is always listening and the remote port is zero for ver, laddr, raddr in netw_obj.dual_stack_sockets(): - yield (0, (format_hints.Hex(netw_obj.vol.offset), "TCP" + ver, laddr, netw_obj.Port, raddr, 0, - "LISTENING", netw_obj.get_owner_pid() or renderers.UnreadableValue(), - netw_obj.get_owner_procname() or renderers.UnreadableValue(), netw_obj.get_create_time() - or renderers.UnreadableValue())) + yield ( + 0, + ( + format_hints.Hex(netw_obj.vol.offset), + "TCP" + ver, + laddr, + netw_obj.Port, + raddr, + 0, + "LISTENING", + netw_obj.get_owner_pid() or renderers.UnreadableValue(), + netw_obj.get_owner_procname() + or renderers.UnreadableValue(), + netw_obj.get_create_time() or renderers.UnreadableValue(), + ), + ) else: # this should not happen therefore we log it. - vollog.debug(f"Found network object unsure of its type: {netw_obj} of type {type(netw_obj)}") + vollog.debug( + f"Found network object unsure of its type: {netw_obj} of type {type(netw_obj)}" + ) def generate_timeline(self): for row in self._generator(): _depth, row_data = row row_dict = {} - row_dict["Offset"], row_dict["Proto"], row_dict["LocalAddr"], row_dict["LocalPort"], \ - row_dict["ForeignAddr"], row_dict["ForeignPort"], row_dict["State"], \ - row_dict["PID"], row_dict["Owner"], row_dict["Created"] = row_data + ( + row_dict["Offset"], + row_dict["Proto"], + row_dict["LocalAddr"], + row_dict["LocalPort"], + row_dict["ForeignAddr"], + row_dict["ForeignPort"], + row_dict["State"], + row_dict["PID"], + row_dict["Owner"], + row_dict["Created"], + ) = row_data # Skip network connections without creation time if not isinstance(row_dict["Created"], datetime.datetime): continue - description = "Network connection: Process {} {} Local Address {}:{} " \ - "Remote Address {}:{} State {} Protocol {} ".format(row_dict["PID"], row_dict["Owner"], - row_dict["LocalAddr"], - row_dict["LocalPort"], - row_dict["ForeignAddr"], - row_dict["ForeignPort"], - row_dict["State"], row_dict["Proto"]) + description = ( + "Network connection: Process {} {} Local Address {}:{} " + "Remote Address {}:{} State {} Protocol {} ".format( + row_dict["PID"], + row_dict["Owner"], + row_dict["LocalAddr"], + row_dict["LocalPort"], + row_dict["ForeignAddr"], + row_dict["ForeignPort"], + row_dict["State"], + row_dict["Proto"], + ) + ) yield (description, timeliner.TimeLinerType.CREATED, row_dict["Created"]) def run(self): - show_corrupt_results = self.config.get('include-corrupt', None) + show_corrupt_results = self.config.get("include-corrupt", None) - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("Proto", str), - ("LocalAddr", str), - ("LocalPort", int), - ("ForeignAddr", str), - ("ForeignPort", int), - ("State", str), - ("PID", int), - ("Owner", str), - ("Created", datetime.datetime), - ], self._generator(show_corrupt_results = show_corrupt_results)) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Proto", str), + ("LocalAddr", str), + ("LocalPort", int), + ("ForeignAddr", str), + ("ForeignPort", int), + ("State", str), + ("PID", int), + ("Owner", str), + ("Created", datetime.datetime), + ], + self._generator(show_corrupt_results=show_corrupt_results), + ) diff --git a/volatility3/framework/plugins/windows/poolscanner.py b/volatility3/framework/plugins/windows/poolscanner.py index c5d60ce03..e131c5f78 100644 --- a/volatility3/framework/plugins/windows/poolscanner.py +++ b/volatility3/framework/plugins/windows/poolscanner.py @@ -31,16 +31,18 @@ class PoolConstraint: """Class to maintain tag/size/index/type information about Pool header tags.""" - def __init__(self, - tag: bytes, - type_name: str, - object_type: Optional[str] = None, - page_type: Optional[PoolType] = None, - size: Optional[Tuple[Optional[int], Optional[int]]] = None, - index: Optional[Tuple[Optional[int], Optional[int]]] = None, - alignment: Optional[int] = 1, - skip_type_test: bool = False, - additional_structures: Optional[List[str]] = None) -> None: + def __init__( + self, + tag: bytes, + type_name: str, + object_type: Optional[str] = None, + page_type: Optional[PoolType] = None, + size: Optional[Tuple[Optional[int], Optional[int]]] = None, + index: Optional[Tuple[Optional[int], Optional[int]]] = None, + alignment: Optional[int] = 1, + skip_type_test: bool = False, + additional_structures: Optional[List[str]] = None, + ) -> None: self.tag = tag self.type_name = type_name self.object_type = object_type @@ -53,23 +55,30 @@ class PoolConstraint: class PoolHeaderScanner(interfaces.layers.ScannerInterface): - - def __init__(self, module: interfaces.context.ModuleInterface, constraint_lookup: Dict[bytes, PoolConstraint], - alignment: int): + def __init__( + self, + module: interfaces.context.ModuleInterface, + constraint_lookup: Dict[bytes, PoolConstraint], + alignment: int, + ): super().__init__() self._module = module self._constraint_lookup = constraint_lookup self._alignment = alignment - header_type = self._module.get_type('_POOL_HEADER') - self._header_offset = header_type.relative_child_offset('PoolTag') - self._subscanner = scanners.MultiStringScanner([c for c in constraint_lookup.keys()]) + header_type = self._module.get_type("_POOL_HEADER") + self._header_offset = header_type.relative_child_offset("PoolTag") + self._subscanner = scanners.MultiStringScanner( + [c for c in constraint_lookup.keys()] + ) def __call__(self, data: bytes, data_offset: int): for offset, pattern in self._subscanner(data, data_offset): - header = self._module.object(object_type = "_POOL_HEADER", - offset = offset - self._header_offset, - absolute = True) + header = self._module.object( + object_type="_POOL_HEADER", + offset=offset - self._header_offset, + absolute=True, + ) constraint = self._constraint_lookup[pattern] try: # Size check @@ -87,9 +96,13 @@ class PoolHeaderScanner(interfaces.layers.ScannerInterface): if (constraint.page_type & PoolType.FREE) and header.is_free_pool(): checks_pass = True - elif (constraint.page_type & PoolType.NONPAGED) and header.is_nonpaged_pool(): + elif ( + constraint.page_type & PoolType.NONPAGED + ) and header.is_nonpaged_pool(): checks_pass = True - elif (constraint.page_type & PoolType.PAGED) and header.is_paged_pool(): + elif ( + constraint.page_type & PoolType.PAGED + ) and header.is_paged_pool(): checks_pass = True if not checks_pass: @@ -120,38 +133,58 @@ class PoolScanner(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'handles', plugin = handles.Handles, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="handles", plugin=handles.Handles, version=(1, 0, 0) + ), ] def _generator(self): - - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] symbol_table = kernel.symbol_table_name constraints = self.builtin_constraints(symbol_table) - for constraint, mem_object, header in self.generate_pool_scan(self.context, kernel.layer_name, - symbol_table, constraints): + for constraint, mem_object, header in self.generate_pool_scan( + self.context, kernel.layer_name, symbol_table, constraints + ): # generate some type-specific info for sanity checking if constraint.object_type == "Process": - name = mem_object.ImageFileName.cast("string", - max_length = mem_object.ImageFileName.vol.count, - errors = "replace") + name = mem_object.ImageFileName.cast( + "string", + max_length=mem_object.ImageFileName.vol.count, + errors="replace", + ) elif constraint.object_type == "File": try: name = mem_object.FileName.String except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, f"Skipping file at {mem_object.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Skipping file at {mem_object.vol.offset:#x}", + ) continue else: name = renderers.NotApplicableValue() - yield (0, (constraint.type_name, format_hints.Hex(header.vol.offset), header.vol.layer_name, name)) + yield ( + 0, + ( + constraint.type_name, + format_hints.Hex(header.vol.offset), + header.vol.layer_name, + name, + ), + ) @staticmethod - def builtin_constraints(symbol_table: str, tags_filter: List[bytes] = None) -> List[PoolConstraint]: + def builtin_constraints( + symbol_table: str, tags_filter: List[bytes] = None + ) -> List[PoolConstraint]: """Get built-in PoolConstraints given a list of pool tags. The tags_filter is a list of pool tags, and the associated @@ -168,83 +201,109 @@ class PoolScanner(plugins.PluginInterface): builtins = [ # atom tables - PoolConstraint(b'AtmT', - type_name = symbol_table + constants.BANG + "_RTL_ATOM_TABLE", - size = (200, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"AtmT", + type_name=symbol_table + constants.BANG + "_RTL_ATOM_TABLE", + size=(200, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # processes on windows before windows 8 - PoolConstraint(b'Pro\xe3', - type_name = symbol_table + constants.BANG + "_EPROCESS", - object_type = "Process", - size = (600, None), - skip_type_test = True, - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Pro\xe3", + type_name=symbol_table + constants.BANG + "_EPROCESS", + object_type="Process", + size=(600, None), + skip_type_test=True, + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # processes on windows starting with windows 8 - PoolConstraint(b'Proc', - type_name = symbol_table + constants.BANG + "_EPROCESS", - object_type = "Process", - size = (600, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Proc", + type_name=symbol_table + constants.BANG + "_EPROCESS", + object_type="Process", + size=(600, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # files on windows before windows 8 - PoolConstraint(b'Fil\xe5', - type_name = symbol_table + constants.BANG + "_FILE_OBJECT", - object_type = "File", - size = (150, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Fil\xe5", + type_name=symbol_table + constants.BANG + "_FILE_OBJECT", + object_type="File", + size=(150, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # files on windows starting with windows 8 - PoolConstraint(b'File', - type_name = symbol_table + constants.BANG + "_FILE_OBJECT", - object_type = "File", - size = (150, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"File", + type_name=symbol_table + constants.BANG + "_FILE_OBJECT", + object_type="File", + size=(150, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # mutants on windows before windows 8 - PoolConstraint(b'Mut\xe1', - type_name = symbol_table + constants.BANG + "_KMUTANT", - object_type = "Mutant", - size = (64, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Mut\xe1", + type_name=symbol_table + constants.BANG + "_KMUTANT", + object_type="Mutant", + size=(64, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # mutants on windows starting with windows 8 - PoolConstraint(b'Muta', - type_name = symbol_table + constants.BANG + "_KMUTANT", - object_type = "Mutant", - size = (64, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Muta", + type_name=symbol_table + constants.BANG + "_KMUTANT", + object_type="Mutant", + size=(64, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # drivers on windows before windows 8 - PoolConstraint(b'Dri\xf6', - type_name = symbol_table + constants.BANG + "_DRIVER_OBJECT", - object_type = "Driver", - size = (248, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, - additional_structures = ["_DRIVER_EXTENSION"]), + PoolConstraint( + b"Dri\xf6", + type_name=symbol_table + constants.BANG + "_DRIVER_OBJECT", + object_type="Driver", + size=(248, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + additional_structures=["_DRIVER_EXTENSION"], + ), # drivers on windows starting with windows 8 - PoolConstraint(b'Driv', - type_name = symbol_table + constants.BANG + "_DRIVER_OBJECT", - object_type = "Driver", - size = (248, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Driv", + type_name=symbol_table + constants.BANG + "_DRIVER_OBJECT", + object_type="Driver", + size=(248, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # kernel modules - PoolConstraint(b'MmLd', - type_name = symbol_table + constants.BANG + "_LDR_DATA_TABLE_ENTRY", - size = (76, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"MmLd", + type_name=symbol_table + constants.BANG + "_LDR_DATA_TABLE_ENTRY", + size=(76, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # symlinks on windows before windows 8 - PoolConstraint(b'Sym\xe2', - type_name = symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", - object_type = "SymbolicLink", - size = (72, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Sym\xe2", + type_name=symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", + object_type="SymbolicLink", + size=(72, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # symlinks on windows starting with windows 8 - PoolConstraint(b'Symb', - type_name = symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", - object_type = "SymbolicLink", - size = (72, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE), + PoolConstraint( + b"Symb", + type_name=symbol_table + constants.BANG + "_OBJECT_SYMBOLIC_LINK", + object_type="SymbolicLink", + size=(72, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + ), # registry hives - PoolConstraint(b'CM10', - type_name = symbol_table + constants.BANG + "_CMHIVE", - size = (800, None), - page_type = PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, - skip_type_test = True), + PoolConstraint( + b"CM10", + type_name=symbol_table + constants.BANG + "_CMHIVE", + size=(800, None), + page_type=PoolType.PAGED | PoolType.NONPAGED | PoolType.FREE, + skip_type_test=True, + ), ] if not tags_filter: @@ -253,13 +312,21 @@ class PoolScanner(plugins.PluginInterface): return [constraint for constraint in builtins if constraint.tag in tags_filter] @classmethod - def generate_pool_scan(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - constraints: List[PoolConstraint]) \ - -> Generator[Tuple[ - PoolConstraint, interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface], None, None]: + def generate_pool_scan( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + constraints: List[PoolConstraint], + ) -> Generator[ + Tuple[ + PoolConstraint, + interfaces.objects.ObjectInterface, + interfaces.objects.ObjectInterface, + ], + None, + None, + ]: """ Args: @@ -273,9 +340,13 @@ class PoolScanner(plugins.PluginInterface): """ # get the object type map - type_map = handles.Handles.get_type_map(context = context, layer_name = layer_name, symbol_table = symbol_table) + type_map = handles.Handles.get_type_map( + context=context, layer_name=layer_name, symbol_table=symbol_table + ) - cookie = handles.Handles.find_cookie(context = context, layer_name = layer_name, symbol_table = symbol_table) + cookie = handles.Handles.find_cookie( + context=context, layer_name=layer_name, symbol_table=symbol_table + ) is_windows_10 = versions.is_windows_10(context, symbol_table) is_windows_8_or_later = versions.is_windows_8_or_later(context, symbol_table) @@ -285,45 +356,61 @@ class PoolScanner(plugins.PluginInterface): # switch to a non-virtual layer if necessary if not is_windows_10: - scan_layer = context.layers[scan_layer].config['memory_layer'] + scan_layer = context.layers[scan_layer].config["memory_layer"] if symbols.symbol_table_is_64bit(context, symbol_table): alignment = 0x10 else: alignment = 8 - for constraint, header in cls.pool_scan(context, scan_layer, symbol_table, constraints, alignment = alignment): - - mem_objects = header.get_object(constraint = constraint, - use_top_down = is_windows_8_or_later, - native_layer_name = layer_name, - kernel_symbol_table = symbol_table) + for constraint, header in cls.pool_scan( + context, scan_layer, symbol_table, constraints, alignment=alignment + ): + mem_objects = header.get_object( + constraint=constraint, + use_top_down=is_windows_8_or_later, + native_layer_name=layer_name, + kernel_symbol_table=symbol_table, + ) for mem_object in mem_objects: if mem_object is None: - vollog.log(constants.LOGLEVEL_VVV, f"Cannot create an instance of {constraint.type_name}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot create an instance of {constraint.type_name}", + ) continue if constraint.object_type is not None and not constraint.skip_type_test: try: - if mem_object.get_object_header().get_object_type(type_map, cookie) != constraint.object_type: + if ( + mem_object.get_object_header().get_object_type( + type_map, cookie + ) + != constraint.object_type + ): continue except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot test instance type check for {constraint.type_name}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot test instance type check for {constraint.type_name}", + ) continue yield constraint, mem_object, header @classmethod - def pool_scan(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - pool_constraints: List[PoolConstraint], - alignment: int = 8, - progress_callback: Optional[constants.ProgressCallback] = None) \ - -> Generator[Tuple[PoolConstraint, interfaces.objects.ObjectInterface], None, None]: + def pool_scan( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + pool_constraints: List[PoolConstraint], + alignment: int = 8, + progress_callback: Optional[constants.ProgressCallback] = None, + ) -> Generator[ + Tuple[PoolConstraint, interfaces.objects.ObjectInterface], None, None + ]: """Returns the _POOL_HEADER object (based on the symbol_table template) after scanning through layer_name returning all headers that match any of the constraints provided. Only one constraint can be provided per @@ -344,11 +431,13 @@ class PoolScanner(plugins.PluginInterface): constraint_lookup: Dict[bytes, PoolConstraint] = {} for constraint in pool_constraints: if constraint.tag in constraint_lookup: - raise ValueError(f"Constraint tag is used for more than one constraint: {repr(constraint.tag)}") + raise ValueError( + f"Constraint tag is used for more than one constraint: {repr(constraint.tag)}" + ) constraint_lookup[constraint.tag] = constraint pool_header_table_name = cls.get_pool_header_table(context, symbol_table) - module = context.module(pool_header_table_name, layer_name, offset = 0) + module = context.module(pool_header_table_name, layer_name, offset=0) # Run the scan locating the offsets of a particular tag layer = context.layers[layer_name] @@ -356,7 +445,9 @@ class PoolScanner(plugins.PluginInterface): yield from layer.scan(context, scanner, progress_callback) @classmethod - def get_pool_header_table(cls, context: interfaces.context.ContextInterface, symbol_table: str) -> str: + def get_pool_header_table( + cls, context: interfaces.context.ContextInterface, symbol_table: str + ) -> str: """Returns the appropriate symbol_table containing a _POOL_HEADER type, even if the original symbol table doesn't contain one. @@ -366,7 +457,9 @@ class PoolScanner(plugins.PluginInterface): """ # Setup the pool header and offset differential try: - context.symbol_space.get_type(symbol_table + constants.BANG + "_POOL_HEADER") + context.symbol_space.get_type( + symbol_table + constants.BANG + "_POOL_HEADER" + ) table_name = symbol_table except exceptions.SymbolError: # We have to manually load a symbol table @@ -387,16 +480,20 @@ class PoolScanner(plugins.PluginInterface): else: class_type = extensions.pool.POOL_HEADER - table_name = intermed.IntermediateSymbolTable.create(context = context, - config_path = configuration.path_join( - context.symbol_space[symbol_table].config_path, - "poolheader"), - sub_path = "windows", - filename = pool_header_json_filename, - table_mapping = {'nt_symbols': symbol_table}, - class_types = {'_POOL_HEADER': class_type}) + table_name = intermed.IntermediateSymbolTable.create( + context=context, + config_path=configuration.path_join( + context.symbol_space[symbol_table].config_path, "poolheader" + ), + sub_path="windows", + filename=pool_header_json_filename, + table_mapping={"nt_symbols": symbol_table}, + class_types={"_POOL_HEADER": class_type}, + ) return table_name def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([("Tag", str), ("Offset", format_hints.Hex), ("Layer", str), ("Name", str)], - self._generator()) + return renderers.TreeGrid( + [("Tag", str), ("Offset", format_hints.Hex), ("Layer", str), ("Name", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/privileges.py b/volatility3/framework/plugins/windows/privileges.py index 2d48a30f7..0370dfc92 100644 --- a/volatility3/framework/plugins/windows/privileges.py +++ b/volatility3/framework/plugins/windows/privileges.py @@ -24,45 +24,62 @@ class Privs(interfaces.plugins.PluginInterface): # Find the sids json path (or raise error if its not in the plugin directory). for plugin_dir in constants.PLUGINS_PATH: - sids_json_file_name = os.path.join(plugin_dir, os.path.join("windows", "sids_and_privileges.json")) + sids_json_file_name = os.path.join( + plugin_dir, os.path.join("windows", "sids_and_privileges.json") + ) if os.path.exists(sids_json_file_name): break else: - vollog.log(constants.LOGLEVEL_VVV, 'sids_and_privileges.json file is missing plugin error') - raise RuntimeError("The sids_and_privileges.json file missed from you plugin directory") + vollog.log( + constants.LOGLEVEL_VVV, + "sids_and_privileges.json file is missing plugin error", + ) + raise RuntimeError( + "The sids_and_privileges.json file missed from you plugin directory" + ) # Get service sids dictionary (we need only the service sids). - with open(sids_json_file_name, 'r') as file_handle: - temp_json = json.load(file_handle)['privileges'] - self.privilege_info = {int(priv_num): temp_json[priv_num] for priv_num in temp_json} + with open(sids_json_file_name, "r") as file_handle: + temp_json = json.load(file_handle)["privileges"] + self.privilege_info = { + int(priv_num): temp_json[priv_num] for priv_num in temp_json + } @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), ] def _generator(self, procs): - for task in procs: try: process_token = task.Token.dereference().cast("_TOKEN") except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, 'Skip invalid token.') + vollog.log(constants.LOGLEVEL_VVV, "Skip invalid token.") continue for value, present, enabled, default in process_token.privileges(): # Skip privileges whose bit positions cannot be # translated to a privilege name if not self.privilege_info.get(int(value)): - vollog.log(constants.LOGLEVEL_VVV, f'Skip invalid privilege ({value}).') + vollog.log( + constants.LOGLEVEL_VVV, f"Skip invalid privilege ({value})." + ) continue name, desc = self.privilege_info.get(int(value)) @@ -76,23 +93,37 @@ class Privs(interfaces.plugins.PluginInterface): if default: attributes.append("Default") - yield (0, [ - int(task.UniqueProcessId), - objects.utility.array_to_string(task.ImageFileName), - int(value), - str(name), ",".join(attributes), - str(desc) - ]) + yield ( + 0, + [ + int(task.UniqueProcessId), + objects.utility.array_to_string(task.ImageFileName), + int(value), + str(name), + ",".join(attributes), + str(desc), + ], + ) def run(self): + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + kernel = self.context.modules[self.config["kernel"]] - 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), ("Value", int), ("Privilege", str), - ("Attributes", str), ("Description", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Value", int), + ("Privilege", str), + ("Attributes", str), + ("Description", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/pslist.py b/volatility3/framework/plugins/windows/pslist.py index cadccc5f1..e7a0d5dd4 100644 --- a/volatility3/framework/plugins/windows/pslist.py +++ b/volatility3/framework/plugins/windows/pslist.py @@ -27,27 +27,40 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.BooleanRequirement(name = 'physical', - description = 'Display physical offsets instead of virtual', - default = cls.PHYSICAL_DEFAULT, - optional = True), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process ID to include (all other processes are excluded)", - optional = True), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed processes", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.BooleanRequirement( + name="physical", + description="Display physical offsets instead of virtual", + default=cls.PHYSICAL_DEFAULT, + optional=True, + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed processes", + default=False, + optional=True, + ), ] @classmethod def process_dump( - cls, context: interfaces.context.ContextInterface, kernel_table_name: str, pe_table_name: str, - proc: interfaces.objects.ObjectInterface, - open_method: Type[interfaces.plugins.FileHandlerInterface]) -> interfaces.plugins.FileHandlerInterface: + cls, + context: interfaces.context.ContextInterface, + kernel_table_name: str, + pe_table_name: str, + proc: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + ) -> interfaces.plugins.FileHandlerInterface: """Extracts the complete data for a process as a FileHandlerInterface Args: @@ -62,27 +75,46 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ file_handle = None + proc_id = "Invalid process object" try: + proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() - peb = context.object(kernel_table_name + constants.BANG + "_PEB", - layer_name = proc_layer_name, - offset = proc.Peb) + peb = context.object( + kernel_table_name + constants.BANG + "_PEB", + layer_name=proc_layer_name, + offset=proc.Peb, + ) + + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=peb.ImageBaseAddress, + layer_name=proc_layer_name, + ) + + process_name = proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ) + + file_handle = open_method( + open_method.sanitize_filename( + f"{proc.UniqueProcessId}.{process_name}.{peb.ImageBaseAddress:#x}.dmp" + ) + ) - dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = peb.ImageBaseAddress, - layer_name = proc_layer_name) - file_handle = open_method(f"pid.{proc.UniqueProcessId}.{peb.ImageBaseAddress:#x}.dmp") for offset, data in dos_header.reconstruct(): file_handle.seek(offset) file_handle.write(data) except Exception as excp: - vollog.debug(f"Unable to dump PE with pid {proc.UniqueProcessId}: {excp}") + vollog.debug(f"Unable to dump PE with pid {proc_id}: {excp}") return file_handle @classmethod - def create_pid_filter(cls, pid_list: List[int] = None, exclude: bool = False) -> Callable[ - [interfaces.objects.ObjectInterface], bool]: + def create_pid_filter( + cls, pid_list: List[int] = None, exclude: bool = False + ) -> Callable[[interfaces.objects.ObjectInterface], bool]: """A factory for producing filter functions that filter based on a list of process IDs. @@ -105,8 +137,9 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return filter_func @classmethod - def create_name_filter(cls, name_list: List[str] = None, exclude: bool = False) -> Callable[ - [interfaces.objects.ObjectInterface], bool]: + def create_name_filter( + cls, name_list: List[str] = None, exclude: bool = False + ) -> Callable[[interfaces.objects.ObjectInterface], bool]: """A factory for producing filter functions that filter based on a list of process names. @@ -122,18 +155,26 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): filter_list = [x for x in name_list if x is not None] if filter_list: if exclude: - filter_func = lambda x: utility.array_to_string(x.ImageFileName) in filter_list + filter_func = ( + lambda x: utility.array_to_string(x.ImageFileName) in filter_list + ) else: - filter_func = lambda x: utility.array_to_string(x.ImageFileName) not in filter_list + filter_func = ( + lambda x: utility.array_to_string(x.ImageFileName) + not in filter_list + ) return filter_func @classmethod - def list_processes(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def list_processes( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Lists all the processes in the primary layer that are in the pid config option. @@ -148,11 +189,11 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """ # We only use the object factory to demonstrate how to use one - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) ps_aph_offset = ntkrnlmp.get_symbol("PsActiveProcessHead").address - list_entry = ntkrnlmp.object(object_type = "_LIST_ENTRY", offset = ps_aph_offset) + list_entry = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=ps_aph_offset) # This is example code to demonstrate how to use symbol_space directly, rather than through a module: # @@ -165,55 +206,84 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # Note: "nt_symbols!_EPROCESS" could have been used, but would rely on the "nt_symbols" symbol table not already # having been present. Strictly, the value of the requirement should be joined with the BANG character # defined in the constants file - reloff = ntkrnlmp.get_type("_EPROCESS").relative_child_offset("ActiveProcessLinks") - eproc = ntkrnlmp.object(object_type = "_EPROCESS", offset = list_entry.vol.offset - reloff, absolute = True) + reloff = ntkrnlmp.get_type("_EPROCESS").relative_child_offset( + "ActiveProcessLinks" + ) + eproc = ntkrnlmp.object( + object_type="_EPROCESS", + offset=list_entry.vol.offset - reloff, + absolute=True, + ) for proc in eproc.ActiveProcessLinks: if not filter_func(proc): yield proc def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) memory = self.context.layers[kernel.layer_name] if not isinstance(memory, layers.intel.Intel): raise TypeError("Primary layer is not an intel layer") - for proc in self.list_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = self.create_pid_filter(self.config.get('pid', None))): - - if not self.config.get('physical', self.PHYSICAL_DEFAULT): + for proc in self.list_processes( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + filter_func=self.create_pid_filter(self.config.get("pid", None)), + ): + if not self.config.get("physical", self.PHYSICAL_DEFAULT): offset = proc.vol.offset else: - (_, _, offset, _, _) = list(memory.mapping(offset = proc.vol.offset, length = 0))[0] + (_, _, offset, _, _) = list( + memory.mapping(offset=proc.vol.offset, length=0) + )[0] file_output = "Disabled" try: - if self.config['dump']: - file_handle = self.process_dump(self.context, kernel.symbol_table_name, - pe_table_name, proc, self.open) + if self.config["dump"]: + file_handle = self.process_dump( + self.context, + kernel.symbol_table_name, + pe_table_name, + proc, + self.open, + ) file_output = "Error outputting file" if file_handle: file_handle.close() file_output = str(file_handle.preferred_filename) - yield (0, (proc.UniqueProcessId, proc.InheritedFromUniqueProcessId, - proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count, - errors = 'replace'), - format_hints.Hex(offset), proc.ActiveThreads, proc.get_handle_count(), proc.get_session_id(), - proc.get_is_wow64(), proc.get_create_time(), proc.get_exit_time(), file_output)) + yield ( + 0, + ( + proc.UniqueProcessId, + proc.InheritedFromUniqueProcessId, + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ), + format_hints.Hex(offset), + proc.ActiveThreads, + proc.get_handle_count(), + proc.get_session_id(), + proc.get_is_wow64(), + proc.get_create_time(), + proc.get_exit_time(), + file_output, + ), + ) except exceptions.InvalidAddressException: - vollog.info(f"Invalid process found at address: {proc.vol.offset:x}. Skipping") + vollog.info( + f"Invalid process found at address: {proc.vol.offset:x}. Skipping" + ) def generate_timeline(self): for row in self._generator(): @@ -223,10 +293,23 @@ class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield (description, timeliner.TimeLinerType.MODIFIED, row_data[9]) def run(self): - offsettype = "(V)" if not self.config.get('physical', self.PHYSICAL_DEFAULT) else "(P)" + offsettype = ( + "(V)" if not self.config.get("physical", self.PHYSICAL_DEFAULT) else "(P)" + ) - return renderers.TreeGrid([("PID", int), ("PPID", int), ("ImageFileName", str), - (f"Offset{offsettype}", format_hints.Hex), ("Threads", int), - ("Handles", int), ("SessionId", int), ("Wow64", bool), - ("CreateTime", datetime.datetime), ("ExitTime", datetime.datetime), - ("File output", str)], self._generator()) + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("ImageFileName", str), + (f"Offset{offsettype}", format_hints.Hex), + ("Threads", int), + ("Handles", int), + ("SessionId", int), + ("Wow64", bool), + ("CreateTime", datetime.datetime), + ("ExitTime", datetime.datetime), + ("File output", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/psscan.py b/volatility3/framework/plugins/windows/psscan.py index a0601aef1..3d9ae5c1e 100644 --- a/volatility3/framework/plugins/windows/psscan.py +++ b/volatility3/framework/plugins/windows/psscan.py @@ -4,7 +4,7 @@ import datetime import logging -from typing import Iterable, Callable, Tuple +from typing import Iterable, Callable, Optional, Tuple from volatility3.framework import renderers, interfaces, layers, exceptions from volatility3.framework.configuration import requirements @@ -22,37 +22,53 @@ vollog = logging.getLogger(__name__) class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): """Scans for processes present in a particular windows memory image.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 3, 1) _version = (1, 1, 0) @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'info', component = info.Info, version = (1, 0, 0)), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process ID to include (all other processes are excluded)", - optional = True), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed processes", - default = False, - optional = True), - requirements.BooleanRequirement(name = 'physical', - description = "Display physical offset instead of virtual", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="info", component=info.Info, version=(1, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed processes", + default=False, + optional=True, + ), + requirements.BooleanRequirement( + name="physical", + description="Display physical offset instead of virtual", + default=False, + optional=True, + ), ] @classmethod - def scan_processes(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: False) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_processes( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for processes using the poolscanner module and constraints. Args: @@ -64,22 +80,26 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): A list of processes found by scanning the `layer_name` layer for process pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'Pro\xe3', b'Proc']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"Pro\xe3", b"Proc"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result if not filter_func(mem_object): yield mem_object @classmethod - def virtual_process_from_physical(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - proc: interfaces.objects.ObjectInterface) -> \ - Iterable[interfaces.objects.ObjectInterface]: - """ Returns a virtual process from a physical addressed one + def virtual_process_from_physical( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + proc: interfaces.objects.ObjectInterface, + ) -> Optional[interfaces.objects.ObjectInterface]: + """Returns a virtual process from a physical addressed one Args: context: The context to retrieve required elements (layers, symbol tables) from @@ -96,10 +116,12 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # If it's WinXP->8.1 we have now a physical process address. # We'll use the first thread to bounce back to the virtual process - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) - tleoffset = ntkrnlmp.get_type("_ETHREAD").relative_child_offset("ThreadListEntry") + tleoffset = ntkrnlmp.get_type("_ETHREAD").relative_child_offset( + "ThreadListEntry" + ) # Start out with the member offset offsets = [tleoffset] @@ -111,23 +133,32 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): # Now we can try to bounce back for ofs in offsets: - ethread = ntkrnlmp.object(object_type = "_ETHREAD", - offset = proc.ThreadListHead.Flink - ofs, - absolute = True) + ethread = ntkrnlmp.object( + object_type="_ETHREAD", + offset=proc.ThreadListHead.Flink - ofs, + absolute=True, + ) # Ask for the thread's process to get an _EPROCESS with a virtual address layer virtual_process = ethread.owning_process() # Sanity check the bounce. # This compares the original offset with the new one (translated from virtual layer) - (_, _, ph_offset, _, _) = list(context.layers[layer_name].mapping(offset = virtual_process.vol.offset, - length = 0))[0] - if virtual_process and \ - proc.vol.offset == ph_offset: + (_, _, ph_offset, _, _) = list( + context.layers[layer_name].mapping( + offset=virtual_process.vol.offset, length=0 + ) + )[0] + if virtual_process and proc.vol.offset == ph_offset: return virtual_process + return None @classmethod - def get_osversion(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> Tuple[int, int, int]: + def get_osversion( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Tuple[int, int, int]: """Returns the complete OS version (MAJ,MIN,BUILD) Args: @@ -146,50 +177,73 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): return (nt_major_version, nt_minor_version, build) def _generator(self): - kernel = self.context.modules[self.config['kernel']] - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) - memory = self.context.layers[kernel.layer_name] + kernel = self.context.modules[self.config["kernel"]] + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + memory = self.context.layers[kernel.layer_name] if not isinstance(memory, layers.intel.Intel): raise TypeError("Primary layer is not an intel layer") - for proc in self.scan_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None))): - + for proc in self.scan_processes( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + filter_func=pslist.PsList.create_pid_filter(self.config.get("pid", None)), + ): file_output = "Disabled" - if self.config['dump']: + if self.config["dump"]: # windows 10 objects (maybe others in the future) are already in virtual memory if proc.vol.layer_name == kernel.layer_name: vproc = proc else: - vproc = self.virtual_process_from_physical(self.context, kernel.layer_name, - kernel.symbol_table_name, proc) + vproc = self.virtual_process_from_physical( + self.context, kernel.layer_name, kernel.symbol_table_name, proc + ) - file_handle = pslist.PsList.process_dump(self.context, kernel.symbol_table_name, - pe_table_name, vproc, - self.open) + file_handle = pslist.PsList.process_dump( + self.context, + kernel.symbol_table_name, + pe_table_name, + vproc, + self.open, + ) file_output = "Error outputting file" if file_handle: file_output = file_handle.preferred_filename - if not self.config['physical']: + if not self.config["physical"]: offset = proc.vol.offset else: - (_, _, offset, _, _) = list(memory.mapping(offset = proc.vol.offset, length = 0))[0] + (_, _, offset, _, _) = list( + memory.mapping(offset=proc.vol.offset, length=0) + )[0] try: - yield (0, (proc.UniqueProcessId, proc.InheritedFromUniqueProcessId, - proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count, - errors = 'replace'), format_hints.Hex(offset), - proc.ActiveThreads, proc.get_handle_count(), proc.get_session_id(), proc.get_is_wow64(), - proc.get_create_time(), proc.get_exit_time(), file_output)) + yield ( + 0, + ( + proc.UniqueProcessId, + proc.InheritedFromUniqueProcessId, + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ), + format_hints.Hex(offset), + proc.ActiveThreads, + proc.get_handle_count(), + proc.get_session_id(), + proc.get_is_wow64(), + proc.get_create_time(), + proc.get_exit_time(), + file_output, + ), + ) except exceptions.InvalidAddressException: - vollog.info(f"Invalid process found at address: {proc.vol.offset:x}. Skipping") + vollog.info( + f"Invalid process found at address: {proc.vol.offset:x}. Skipping" + ) def generate_timeline(self): for row in self._generator(): @@ -199,9 +253,20 @@ class PsScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface): yield (description, timeliner.TimeLinerType.MODIFIED, row_data[9]) def run(self): - offsettype = "(V)" if not self.config['physical'] else "(P)" - return renderers.TreeGrid([("PID", int), ("PPID", int), ("ImageFileName", str), - (f"Offset{offsettype}", format_hints.Hex), ("Threads", int), - ("Handles", int), ("SessionId", int), ("Wow64", bool), - ("CreateTime", datetime.datetime), ("ExitTime", datetime.datetime), - ("File output", str)], self._generator()) + offsettype = "(V)" if not self.config["physical"] else "(P)" + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("ImageFileName", str), + (f"Offset{offsettype}", format_hints.Hex), + ("Threads", int), + ("Handles", int), + ("SessionId", int), + ("Wow64", bool), + ("CreateTime", datetime.datetime), + ("ExitTime", datetime.datetime), + ("File output", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/pstree.py b/volatility3/framework/plugins/windows/pstree.py index fbb883839..2be96277c 100644 --- a/volatility3/framework/plugins/windows/pstree.py +++ b/volatility3/framework/plugins/windows/pstree.py @@ -3,15 +3,16 @@ # import datetime import logging -from typing import Dict, Set, Tuple +from typing import Callable, Dict, Set, Tuple -from volatility3.framework import objects, interfaces, renderers +from volatility3.framework import objects, interfaces, renderers, exceptions from volatility3.framework.configuration import requirements from volatility3.framework.renderers import format_hints from volatility3.plugins.windows import pslist vollog = logging.getLogger(__name__) + class PsTree(interfaces.plugins.PluginInterface): """Plugin for listing processes in a tree based on their parent process ID.""" @@ -23,82 +24,169 @@ class PsTree(interfaces.plugins.PluginInterface): self._processes: Dict[int, Tuple[interfaces.objects.ObjectInterface, int]] = {} self._levels: Dict[int, int] = {} self._children: Dict[int, Set[int]] = {} + self._ancestors: Set[int] = set([]) @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.BooleanRequirement(name = 'physical', - description = 'Display physical offsets instead of virtual', - default = pslist.PsList.PHYSICAL_DEFAULT, - optional = True), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process ID to include (all other processes are excluded)", - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.BooleanRequirement( + name="physical", + description="Display physical offsets instead of virtual", + default=pslist.PsList.PHYSICAL_DEFAULT, + optional=True, + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (with ancestors and descendants, all other processes are excluded)", + optional=True, + ), ] - def find_level(self, pid: objects.Pointer) -> None: + def find_level( + self, + pid: objects.Pointer, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ) -> None: """Finds how deep the pid is in the processes list.""" - seen = set([]) - seen.add(pid) + seen = {pid} level = 0 proc, _ = self._processes.get(pid, None) + filtered = not filter_func(proc) while proc is not None and proc.InheritedFromUniqueProcessId not in seen: + if filtered: + self._ancestors.add(proc.UniqueProcessId) child_list = self._children.get(proc.InheritedFromUniqueProcessId, set([])) child_list.add(proc.UniqueProcessId) self._children[proc.InheritedFromUniqueProcessId] = child_list seen.add(proc.InheritedFromUniqueProcessId) - proc, _ = self._processes.get(proc.InheritedFromUniqueProcessId, (None, None)) + proc, _ = self._processes.get( + proc.InheritedFromUniqueProcessId, (None, None) + ) level += 1 self._levels[pid] = level - def _generator(self): + def _generator( + self, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ): """Generates the Tree of processes.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - for proc in pslist.PsList.list_processes(self.context, kernel.layer_name, - kernel.symbol_table_name): - if not self.config.get('physical', pslist.PsList.PHYSICAL_DEFAULT): + for proc in pslist.PsList.list_processes( + self.context, kernel.layer_name, kernel.symbol_table_name + ): + if not self.config.get("physical", pslist.PsList.PHYSICAL_DEFAULT): offset = proc.vol.offset else: layer_name = kernel.layer_name memory = self.context.layers[layer_name] - (_, _, offset, _, _) = list(memory.mapping(offset = proc.vol.offset, length = 0))[0] + (_, _, offset, _, _) = list( + memory.mapping(offset=proc.vol.offset, length=0) + )[0] self._processes[proc.UniqueProcessId] = proc, offset # Build the child/level maps for pid in self._processes: - self.find_level(pid) + self.find_level(pid, filter_func) process_pids = set([]) - def yield_processes(pid): + + def yield_processes(pid, descendant: bool = False): if pid in process_pids: vollog.debug(f"Pid cycle: already processed pid {pid}") - return + return None + process_pids.add(pid) + + if pid not in self._ancestors and not descendant: + vollog.debug(f"Pid cycle: pid {pid} not in filtered tree") + return None + proc, offset = self._processes[pid] - row = (proc.UniqueProcessId, proc.InheritedFromUniqueProcessId, - proc.ImageFileName.cast("string", max_length = proc.ImageFileName.vol.count, errors = 'replace'), - format_hints.Hex(offset), proc.ActiveThreads, proc.get_handle_count(), proc.get_session_id(), - proc.get_is_wow64(), proc.get_create_time(), proc.get_exit_time()) + row = ( + proc.UniqueProcessId, + proc.InheritedFromUniqueProcessId, + proc.ImageFileName.cast( + "string", max_length=proc.ImageFileName.vol.count, errors="replace" + ), + format_hints.Hex(offset), + proc.ActiveThreads, + proc.get_handle_count(), + proc.get_session_id(), + proc.get_is_wow64(), + proc.get_create_time(), + proc.get_exit_time(), + ) + + try: + audit = proc.SeAuditProcessCreationInfo.ImageFileName.Name + # If 'audit' is set to the empty string, display NotAvailableValue + row += (audit.get_string() or renderers.NotAvailableValue(),) + except exceptions.InvalidAddressException: + row += (renderers.NotAvailableValue(),) + + try: + process_params = proc.get_peb().ProcessParameters + row += ( + process_params.CommandLine.get_string(), + process_params.ImagePathName.get_string(), + ) + except exceptions.InvalidAddressException: + row += ( + renderers.NotAvailableValue(), + renderers.NotAvailableValue(), + ) yield (self._levels[pid] - 1, row) for child_pid in self._children.get(pid, []): - yield from yield_processes(child_pid) + yield from yield_processes( + child_pid, descendant or not filter_func(proc) + ) for pid in self._levels: if self._levels[pid] == 1: yield from yield_processes(pid) def run(self): - offsettype = "(V)" if not self.config.get('physical', pslist.PsList.PHYSICAL_DEFAULT) else "(P)" + offsettype = ( + "(V)" + if not self.config.get("physical", pslist.PsList.PHYSICAL_DEFAULT) + else "(P)" + ) - return renderers.TreeGrid([("PID", int), ("PPID", int), ("ImageFileName", str), - (f"Offset{offsettype}", format_hints.Hex), ("Threads", int), - ("Handles", int), ("SessionId", int), ("Wow64", bool), - ("CreateTime", datetime.datetime), ("ExitTime", datetime.datetime)], - self._generator()) + return renderers.TreeGrid( + [ + ("PID", int), + ("PPID", int), + ("ImageFileName", str), + (f"Offset{offsettype}", format_hints.Hex), + ("Threads", int), + ("Handles", int), + ("SessionId", int), + ("Wow64", bool), + ("CreateTime", datetime.datetime), + ("ExitTime", datetime.datetime), + ("Audit", str), + ("Cmd", str), + ("Path", str), + ], + self._generator( + filter_func=pslist.PsList.create_pid_filter( + self.config.get("pid", None) + ), + ), + ) diff --git a/volatility3/framework/plugins/windows/registry/hivelist.py b/volatility3/framework/plugins/windows/registry/hivelist.py index ac30560c6..1cc76dad6 100644 --- a/volatility3/framework/plugins/windows/registry/hivelist.py +++ b/volatility3/framework/plugins/windows/registry/hivelist.py @@ -19,16 +19,18 @@ class HiveGenerator: _required_framework_version = (2, 0, 0) - def __init__(self, cmhive, forward = True): + def __init__(self, cmhive, forward=True): self._cmhive = cmhive self._forward = forward self._invalid = None def __iter__(self): - for hive in self._cmhive.HiveList.to_list(self._cmhive.vol.type_name, "HiveList", forward = self._forward): + for hive in self._cmhive.HiveList.to_list( + self._cmhive.vol.type_name, "HiveList", forward=self._forward + ): if not hive.is_valid(): self._invalid = hive.vol.offset - return + return None yield hive @property @@ -45,69 +47,101 @@ class HiveList(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.StringRequirement(name = 'filter', - description = "String to filter hive names returned", - optional = True, - default = None), - requirements.PluginRequirement(name = 'hivescan', plugin = hivescan.HiveScan, version = (1, 0, 0)), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed registry hives", - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.StringRequirement( + name="filter", + description="String to filter hive names returned", + optional=True, + default=None, + ), + requirements.PluginRequirement( + name="hivescan", plugin=hivescan.HiveScan, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed registry hives", + default=False, + optional=True, + ), ] def _sanitize_hive_name(self, name: str) -> str: - return name.split('\\')[-1].replace(' ', '_').replace('.', '').replace('[', '').replace(']', '') + return ( + name.split("\\")[-1] + .replace(" ", "_") + .replace(".", "") + .replace("[", "") + .replace("]", "") + ) def _generator(self) -> Iterator[Tuple[int, Tuple[int, str]]]: chunk_size = 0x500000 - kernel = self.context.modules[self.config['kernel']] - - for hive_object in self.list_hive_objects(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_string = self.config.get('filter', None)): + kernel = self.context.modules[self.config["kernel"]] + for hive_object in self.list_hive_objects( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string=self.config.get("filter", None), + ): file_output = "Disabled" - if self.config['dump']: + if self.config["dump"]: # Construct the hive hive = next( - self.list_hives(self.context, - self.config_path, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - hive_offsets = [hive_object.vol.offset])) + self.list_hives( + self.context, + self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + hive_offsets=[hive_object.vol.offset], + ) + ) maxaddr = hive.hive.Storage[0].Length hive_name = self._sanitize_hive_name(hive.get_name()) - file_handle = self.open(f'registry.{hive_name}.{hex(hive.hive_offset)}.hive') + file_handle = self.open( + f"registry.{hive_name}.{hex(hive.hive_offset)}.hive" + ) with file_handle as file_data: if hive._base_block: - hive_data = self.context.layers[hive.dependencies[0]].read(hive.hive.BaseBlock, 1 << 12) + hive_data = self.context.layers[hive.dependencies[0]].read( + hive.hive.BaseBlock, 1 << 12 + ) else: - hive_data = '\x00' * (1 << 12) + hive_data = "\x00" * (1 << 12) file_data.write(hive_data) for i in range(0, maxaddr, chunk_size): current_chunk_size = min(chunk_size, maxaddr - i) - data = hive.read(i, current_chunk_size, pad = True) + data = hive.read(i, current_chunk_size, pad=True) file_data.write(data) # if self._progress_callback: # self._progress_callback((i / maxaddr) * 100, 'Writing layer {}'.format(hive_name)) file_output = file_handle.preferred_filename - yield (0, (format_hints.Hex(hive_object.vol.offset), hive_object.get_name() or "", file_output)) + yield ( + 0, + ( + format_hints.Hex(hive_object.vol.offset), + hive_object.get_name() or "", + file_output, + ), + ) @classmethod - def list_hives(cls, - context: interfaces.context.ContextInterface, - base_config_path: str, - layer_name: str, - symbol_table: str, - filter_string: Optional[str] = None, - hive_offsets: List[int] = None) -> Iterable[registry.RegistryHive]: + def list_hives( + cls, + context: interfaces.context.ContextInterface, + base_config_path: str, + layer_name: str, + symbol_table: str, + filter_string: Optional[str] = None, + hive_offsets: List[int] = None, + ) -> Iterable[registry.RegistryHive]: """Walks through a registry, hive by hive returning the constructed registry layer name. @@ -125,34 +159,49 @@ class HiveList(interfaces.plugins.PluginInterface): if hive_offsets is None: try: hive_offsets = [ - hive.vol.offset for hive in cls.list_hive_objects(context, layer_name, symbol_table, filter_string) + hive.vol.offset + for hive in cls.list_hive_objects( + context, layer_name, symbol_table, filter_string + ) ] except ImportError: - vollog.warning("Unable to import windows.hivelist plugin, please provide a hive offset") - raise ValueError("Unable to import windows.hivelist plugin, please provide a hive offset") + vollog.warning( + "Unable to import windows.hivelist plugin, please provide a hive offset" + ) + raise ValueError( + "Unable to import windows.hivelist plugin, please provide a hive offset" + ) for hive_offset in hive_offsets: # Construct the hive - reg_config_path = cls.make_subconfig(context = context, - base_config_path = base_config_path, - hive_offset = hive_offset, - base_layer = layer_name, - nt_symbols = symbol_table) + reg_config_path = cls.make_subconfig( + context=context, + base_config_path=base_config_path, + hive_offset=hive_offset, + base_layer=layer_name, + nt_symbols=symbol_table, + ) try: - hive = registry.RegistryHive(context, reg_config_path, name = 'hive' + hex(hive_offset)) + hive = registry.RegistryHive( + context, reg_config_path, name="hive" + hex(hive_offset) + ) except exceptions.InvalidAddressException: - vollog.warning(f"Couldn't create RegistryHive layer at offset {hex(hive_offset)}, skipping") + vollog.warning( + f"Couldn't create RegistryHive layer at offset {hex(hive_offset)}, skipping" + ) continue context.layers.add_layer(hive) yield hive @classmethod - def list_hive_objects(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - filter_string: str = None) -> Iterator[interfaces.objects.ObjectInterface]: + def list_hive_objects( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + filter_string: str = None, + ) -> Iterator[interfaces.objects.ObjectInterface]: """Lists all the hives in the primary layer. Args: @@ -166,40 +215,59 @@ class HiveList(interfaces.plugins.PluginInterface): """ # We only use the object factory to demonstrate how to use one - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) list_head = ntkrnlmp.get_symbol("CmpHiveListHead").address - list_entry = ntkrnlmp.object(object_type = "_LIST_ENTRY", offset = list_head) + list_entry = ntkrnlmp.object(object_type="_LIST_ENTRY", offset=list_head) reloff = ntkrnlmp.get_type("_CMHIVE").relative_child_offset("HiveList") - cmhive = ntkrnlmp.object(object_type = "_CMHIVE", offset = list_entry.vol.offset - reloff, absolute = True) + cmhive = ntkrnlmp.object( + object_type="_CMHIVE", offset=list_entry.vol.offset - reloff, absolute=True + ) # Run through the list forwards seen = set() - hg = HiveGenerator(cmhive, forward = True) + hg = HiveGenerator(cmhive, forward=True) for hive in hg: if hive.vol.offset in seen: - vollog.debug("Hivelist found an already seen offset {} while " \ - "traversing forwards, this should not occur".format(hex(hive.vol.offset))) + vollog.debug( + "Hivelist found an already seen offset {} while " + "traversing forwards, this should not occur".format( + hex(hive.vol.offset) + ) + ) break seen.add(hive.vol.offset) - if filter_string is None or filter_string.lower() in str(hive.get_name() or "").lower(): + if ( + filter_string is None + or filter_string.lower() in str(hive.get_name() or "").lower() + ): if context.layers[layer_name].is_valid(hive.vol.offset): yield hive forward_invalid = hg.invalid if forward_invalid: - vollog.debug("Hivelist failed traversing the list forwards at {}, traversing backwards".format( - hex(forward_invalid))) - hg = HiveGenerator(cmhive, forward = False) + vollog.debug( + "Hivelist failed traversing the list forwards at {}, traversing backwards".format( + hex(forward_invalid) + ) + ) + hg = HiveGenerator(cmhive, forward=False) for hive in hg: if hive.vol.offset in seen: - vollog.debug("Hivelist found an already seen offset {} while " \ - "traversing backwards, list walking met in the middle".format(hex(hive.vol.offset))) + vollog.debug( + "Hivelist found an already seen offset {} while " + "traversing backwards, list walking met in the middle".format( + hex(hive.vol.offset) + ) + ) break seen.add(hive.vol.offset) - if filter_string is None or filter_string.lower() in str(hive.get_name() or "").lower(): + if ( + filter_string is None + or filter_string.lower() in str(hive.get_name() or "").lower() + ): if context.layers[layer_name].is_valid(hive.vol.offset): yield hive @@ -212,30 +280,53 @@ class HiveList(interfaces.plugins.PluginInterface): # therefore, there must be more 2 or more invalid hives, so the middle of the list is not reachable # by walking the list, so revert to scanning, and walk the list forwards and backwards from each # found hive - vollog.debug("Hivelist failed traversing backwards at {}, a different " \ - "location from forwards, revert to scanning".format(hex(backward_invalid))) - for hive in hivescan.HiveScan.scan_hives(context, layer_name, symbol_table): + vollog.debug( + "Hivelist failed traversing backwards at {}, a different " + "location from forwards, revert to scanning".format( + hex(backward_invalid) + ) + ) + for hive in hivescan.HiveScan.scan_hives( + context, layer_name, symbol_table + ): try: if hive.HiveList.Flink: start_hive_offset = hive.HiveList.Flink - reloff ## Now instantiate the first hive in virtual address space as normal - start_hive = ntkrnlmp.object(object_type = "_CMHIVE", - offset = start_hive_offset, - absolute = True) + start_hive = ntkrnlmp.object( + object_type="_CMHIVE", + offset=start_hive_offset, + absolute=True, + ) for forward in (True, False): - for linked_hive in start_hive.HiveList.to_list(hive.vol.type_name, "HiveList", forward): - if not linked_hive.is_valid() or linked_hive.vol.offset in seen: + for linked_hive in start_hive.HiveList.to_list( + hive.vol.type_name, "HiveList", forward + ): + if ( + not linked_hive.is_valid() + or linked_hive.vol.offset in seen + ): continue seen.add(linked_hive.vol.offset) - if filter_string is None or filter_string.lower() in str(linked_hive.get_name() - or "").lower(): - if context.layers[layer_name].is_valid(linked_hive.vol.offset): + if ( + filter_string is None + or filter_string.lower() + in str(linked_hive.get_name() or "").lower() + ): + if context.layers[layer_name].is_valid( + linked_hive.vol.offset + ): yield linked_hive except exceptions.InvalidAddressException: - vollog.debug("InvalidAddressException when traversing hive {} found from scan, skipping".format( - hex(hive.vol.offset))) + vollog.debug( + "InvalidAddressException when traversing hive {} found from scan, skipping".format( + hex(hive.vol.offset) + ) + ) def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([("Offset", format_hints.Hex), ("FileFullPath", str), ("File output", str)], - self._generator()) + return renderers.TreeGrid( + [("Offset", format_hints.Hex), ("FileFullPath", str), ("File output", str)], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/registry/hivescan.py b/volatility3/framework/plugins/windows/registry/hivescan.py index ab15e56ac..7b3c0b622 100644 --- a/volatility3/framework/plugins/windows/registry/hivescan.py +++ b/volatility3/framework/plugins/windows/registry/hivescan.py @@ -21,18 +21,26 @@ class HiveScan(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'bigpools', plugin = bigpools.BigPools, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="bigpools", plugin=bigpools.BigPools, version=(1, 0, 0) + ), ] @classmethod - def scan_hives(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_hives( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for hives using the poolscanner module and constraints or bigpools module with tag. Args: @@ -45,32 +53,40 @@ class HiveScan(interfaces.plugins.PluginInterface): """ is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) - is_windows_8_1_or_later = versions.is_windows_8_1_or_later(context = context, symbol_table = symbol_table) + is_windows_8_1_or_later = versions.is_windows_8_1_or_later( + context=context, symbol_table=symbol_table + ) if is_windows_8_1_or_later and is_64bit: - kvo = context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + kvo = context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) - for pool in bigpools.BigPools.list_big_pools(context, - layer_name = layer_name, - symbol_table = symbol_table, - tags = ["CM10"]): - cmhive = ntkrnlmp.object(object_type = "_CMHIVE", offset = pool.Va, absolute = True) + for pool in bigpools.BigPools.list_big_pools( + context, layer_name=layer_name, symbol_table=symbol_table, tags=["CM10"] + ): + cmhive = ntkrnlmp.object( + object_type="_CMHIVE", offset=pool.Va, absolute=True + ) yield cmhive else: - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'CM10']) + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"CM10"] + ) - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - for hive in self.scan_hives(self.context, kernel.layer_name, kernel.symbol_table_name): - - yield (0, (format_hints.Hex(hive.vol.offset), )) + for hive in self.scan_hives( + self.context, kernel.layer_name, kernel.symbol_table_name + ): + yield (0, (format_hints.Hex(hive.vol.offset),)) def run(self): return renderers.TreeGrid([("Offset", format_hints.Hex)], self._generator()) diff --git a/volatility3/framework/plugins/windows/registry/printkey.py b/volatility3/framework/plugins/windows/registry/printkey.py index 2082c339e..180f8f9d9 100644 --- a/volatility3/framework/plugins/windows/registry/printkey.py +++ b/volatility3/framework/plugins/windows/registry/printkey.py @@ -25,18 +25,26 @@ class PrintKey(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)), - requirements.IntRequirement(name = 'offset', description = "Hive Offset", default = None, optional = True), - requirements.StringRequirement(name = 'key', - description = "Key to start from", - default = None, - optional = True), - requirements.BooleanRequirement(name = 'recurse', - description = 'Recurses through keys', - default = False, - optional = True) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + requirements.IntRequirement( + name="offset", description="Hive Offset", default=None, optional=True + ), + requirements.StringRequirement( + name="key", description="Key to start from", default=None, optional=True + ), + requirements.BooleanRequirement( + name="recurse", + description="Recurses through keys", + default=False, + optional=True, + ), ] @classmethod @@ -44,8 +52,12 @@ class PrintKey(interfaces.plugins.PluginInterface): cls, hive: RegistryHive, node_path: Sequence[objects.StructType] = None, - recurse: bool = False - ) -> Iterable[Tuple[int, bool, datetime.datetime, str, bool, interfaces.objects.ObjectInterface]]: + recurse: bool = False, + ) -> Iterable[ + Tuple[ + int, bool, datetime.datetime, str, bool, interfaces.objects.ObjectInterface + ] + ]: """Walks through a set of nodes from a given node (last one in node_path). Avoids loops by not traversing into nodes already present in the node_path. @@ -62,16 +74,25 @@ class PrintKey(interfaces.plugins.PluginInterface): node_path = [hive.get_node(hive.root_cell_offset)] if not isinstance(node_path, list) or len(node_path) < 1: vollog.warning("Hive walker was not passed a valid node_path (or None)") - return + return None node = node_path[-1] key_path_items = [hive] + node_path[1:] - key_path = '\\'.join([k.get_name() for k in key_path_items]) - if node.vol.type_name.endswith(constants.BANG + '_CELL_DATA'): - raise RegistryFormatException(hive.name, "Encountered _CELL_DATA instead of _CM_KEY_NODE") + key_path = "\\".join([k.get_name() for k in key_path_items]) + if node.vol.type_name.endswith(constants.BANG + "_CELL_DATA"): + raise RegistryFormatException( + hive.name, "Encountered _CELL_DATA instead of _CM_KEY_NODE" + ) last_write_time = conversion.wintime_to_datetime(node.LastWriteTime.QuadPart) for key_node in node.get_subkeys(): - result = (len(node_path), True, last_write_time, key_path, key_node.get_volatile(), key_node) + result = ( + len(node_path), + True, + last_write_time, + key_path, + key_node.get_volatile(), + key_node, + ) yield result if recurse: @@ -82,16 +103,27 @@ class PrintKey(interfaces.plugins.PluginInterface): vollog.debug(excp) continue - yield from cls.key_iterator(hive, node_path + [key_node], recurse = recurse) + yield from cls.key_iterator( + hive, node_path + [key_node], recurse=recurse + ) for value_node in node.get_values(): - result = (len(node_path), False, last_write_time, key_path, node.get_volatile(), value_node) + result = ( + len(node_path), + False, + last_write_time, + key_path, + node.get_volatile(), + value_node, + ) yield result - def _printkey_iterator(self, - hive: RegistryHive, - node_path: Sequence[objects.StructType] = None, - recurse: bool = False): + def _printkey_iterator( + self, + hive: RegistryHive, + node_path: Sequence[objects.StructType] = None, + recurse: bool = False, + ): """Method that wraps the more generic key_iterator, to provide output for printkey specifically. @@ -103,96 +135,180 @@ class PrintKey(interfaces.plugins.PluginInterface): Yields: The depth, and a tuple of results (last write time, hive offset, type, path, name, data and volatile) """ - for depth, is_key, last_write_time, key_path, volatile, node in self.key_iterator(hive, node_path, recurse): + for ( + depth, + is_key, + last_write_time, + key_path, + volatile, + node, + ) in self.key_iterator(hive, node_path, recurse): if is_key: try: key_node_name = node.get_name() - except (exceptions.InvalidAddressException, RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + RegistryFormatException, + ) as excp: vollog.debug(excp) key_node_name = renderers.UnreadableValue() - yield (depth, (last_write_time, renderers.format_hints.Hex(hive.hive_offset), "Key", key_path, - key_node_name, renderers.NotApplicableValue(), volatile)) + # if the item is a subkey, use the LastWriteTime of that subkey + last_write_time = conversion.wintime_to_datetime( + node.LastWriteTime.QuadPart + ) + + yield ( + depth, + ( + last_write_time, + renderers.format_hints.Hex(hive.hive_offset), + "Key", + key_path, + key_node_name, + renderers.NotApplicableValue(), + volatile, + ), + ) else: try: value_node_name = node.get_name() or "(Default)" - except (exceptions.InvalidAddressException, RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + RegistryFormatException, + ) as excp: vollog.debug(excp) value_node_name = renderers.UnreadableValue() try: value_type = RegValueTypes(node.Type).name - except (exceptions.InvalidAddressException, RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + RegistryFormatException, + ) as excp: vollog.debug(excp) value_type = renderers.UnreadableValue() if isinstance(value_type, renderers.UnreadableValue): - vollog.debug("Couldn't read registry value type, so data is unreadable") - value_data: Union[interfaces.renderers.BaseAbsentValue, bytes] = renderers.UnreadableValue() + vollog.debug( + "Couldn't read registry value type, so data is unreadable" + ) + value_data: Union[interfaces.renderers.BaseAbsentValue, bytes] = ( + renderers.UnreadableValue() + ) else: try: value_data = node.decode_data() if isinstance(value_data, int): - value_data = format_hints.MultiTypeData(value_data, encoding = 'utf-8') + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-8" + ) elif RegValueTypes(node.Type) == RegValueTypes.REG_BINARY: - value_data = format_hints.MultiTypeData(value_data, show_hex = True) + value_data = format_hints.MultiTypeData( + value_data, show_hex=True + ) elif RegValueTypes(node.Type) == RegValueTypes.REG_MULTI_SZ: - value_data = format_hints.MultiTypeData(value_data, - encoding = 'utf-16-le', - split_nulls = True) + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-16-le", split_nulls=True + ) else: - value_data = format_hints.MultiTypeData(value_data, encoding = 'utf-16-le') - except (ValueError, exceptions.InvalidAddressException, RegistryFormatException) as excp: + value_data = format_hints.MultiTypeData( + value_data, encoding="utf-16-le" + ) + except ( + ValueError, + exceptions.InvalidAddressException, + RegistryFormatException, + ) as excp: vollog.debug(excp) value_data = renderers.UnreadableValue() - result = (depth, (last_write_time, renderers.format_hints.Hex(hive.hive_offset), value_type, key_path, - value_node_name, value_data, volatile)) + result = ( + depth, + ( + last_write_time, + renderers.format_hints.Hex(hive.hive_offset), + value_type, + key_path, + value_node_name, + value_data, + volatile, + ), + ) yield result - def _registry_walker(self, - layer_name: str, - symbol_table: str, - hive_offsets: List[int] = None, - key: str = None, - recurse: bool = False): - - for hive in hivelist.HiveList.list_hives(self.context, - self.config_path, - layer_name = layer_name, - symbol_table = symbol_table, - hive_offsets = hive_offsets): - + def _registry_walker( + self, + layer_name: str, + symbol_table: str, + hive_offsets: List[int] = None, + key: str = None, + recurse: bool = False, + ): + for hive in hivelist.HiveList.list_hives( + self.context, + self.config_path, + layer_name=layer_name, + symbol_table=symbol_table, + hive_offsets=hive_offsets, + ): try: # Walk it if key is not None: - node_path = hive.get_key(key, return_list = True) + node_path = hive.get_key(key, return_list=True) else: node_path = [hive.get_node(hive.root_cell_offset)] - for (x, y) in self._printkey_iterator(hive, node_path, recurse = recurse): + for x, y in self._printkey_iterator(hive, node_path, recurse=recurse): yield (x - len(node_path), y) - except (exceptions.InvalidAddressException, KeyError, RegistryFormatException) as excp: + except ( + exceptions.InvalidAddressException, + KeyError, + RegistryFormatException, + ) as excp: if isinstance(excp, KeyError): - vollog.debug(f"Key '{key}' not found in Hive at offset {hex(hive.hive_offset)}.") + vollog.debug( + f"Key '{key}' not found in Hive at offset {hex(hive.hive_offset)}." + ) elif isinstance(excp, RegistryFormatException): vollog.debug(excp) elif isinstance(excp, exceptions.InvalidAddressException): - vollog.debug(f"Invalid address identified in Hive: {hex(excp.invalid_address)}") - result = (0, (renderers.UnreadableValue(), format_hints.Hex(hive.hive_offset), "Key", - '?\\' + (key or ''), renderers.UnreadableValue(), renderers.UnreadableValue(), - renderers.UnreadableValue())) + vollog.debug( + f"Invalid address identified in Hive: {hex(excp.invalid_address)}" + ) + result = ( + 0, + ( + renderers.UnreadableValue(), + format_hints.Hex(hive.hive_offset), + "Key", + "?\\" + (key or ""), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + ), + ) yield result def run(self): - offset = self.config.get('offset', None) - kernel = self.context.modules[self.config['kernel']] + offset = self.config.get("offset", None) + kernel = self.context.modules[self.config["kernel"]] - return TreeGrid(columns = [('Last Write Time', datetime.datetime), ('Hive Offset', format_hints.Hex), - ('Type', str), ('Key', str), ('Name', str), ('Data', format_hints.MultiTypeData), - ('Volatile', bool)], - generator = self._registry_walker(kernel.layer_name, - kernel.symbol_table_name, - hive_offsets = None if offset is None else [offset], - key = self.config.get('key', None), - recurse = self.config.get('recurse', None))) + return TreeGrid( + columns=[ + ("Last Write Time", datetime.datetime), + ("Hive Offset", format_hints.Hex), + ("Type", str), + ("Key", str), + ("Name", str), + ("Data", format_hints.MultiTypeData), + ("Volatile", bool), + ], + generator=self._registry_walker( + kernel.layer_name, + kernel.symbol_table_name, + hive_offsets=None if offset is None else [offset], + key=self.config.get("key", None), + recurse=self.config.get("recurse", None), + ), + ) diff --git a/volatility3/framework/plugins/windows/registry/userassist.py b/volatility3/framework/plugins/windows/registry/userassist.py index a788f058f..70c75b50b 100644 --- a/volatility3/framework/plugins/windows/registry/userassist.py +++ b/volatility3/framework/plugins/windows/registry/userassist.py @@ -3,17 +3,18 @@ # import codecs +import contextlib import datetime import json import logging import os -from typing import Any, List, Tuple, Generator +from typing import Any, Generator, List, Tuple -from volatility3.framework import exceptions, renderers, constants, interfaces +from volatility3.framework import constants, exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.layers.physical import BufferDataLayer from volatility3.framework.layers.registry import RegistryHive -from volatility3.framework.renderers import format_hints, conversion +from volatility3.framework.renderers import conversion, format_hints from volatility3.framework.symbols import intermed from volatility3.plugins.windows.registry import hivelist @@ -32,15 +33,28 @@ class UserAssist(interfaces.plugins.PluginInterface): self._reg_table_name = None self._win7 = None # taken from http://msdn.microsoft.com/en-us/library/dd378457%28v=vs.85%29.aspx - self._folder_guids = json.load(open(os.path.join(os.path.dirname(__file__), "userassist.json"), "rb")) + try: + with open( + os.path.join(os.path.dirname(__file__), "userassist.json"), "rb" + ) as fp: + self._folder_guids = json.load(fp) + except IOError: + vollog.error("Usersassist data file not found") @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.IntRequirement(name = 'offset', description = "Hive Offset", default = None, optional = True), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.IntRequirement( + name="offset", description="Hive Offset", default=None, optional=True + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] def parse_userassist_data(self, reg_val): @@ -71,30 +85,44 @@ class UserAssist(interfaces.plugins.PluginInterface): return item userassist_layer_name = self.context.layers.free_layer_name("userassist_buffer") - buffer = BufferDataLayer(self.context, self._config_path, userassist_layer_name, userassist_data) + buffer = BufferDataLayer( + self.context, self._config_path, userassist_layer_name, userassist_data + ) self.context.add_layer(buffer) userassist_obj = self.context.object( - object_type = self._reg_table_name + constants.BANG + self._userassist_type_name, - layer_name = userassist_layer_name, - offset = 0) + object_type=self._reg_table_name + + constants.BANG + + self._userassist_type_name, + layer_name=userassist_layer_name, + offset=0, + ) if self._win7: item["id"] = renderers.NotApplicableValue() item["count"] = int(userassist_obj.Count) seconds = (userassist_obj.FocusTime + 500) / 1000.0 - time = datetime.timedelta(seconds = seconds) if seconds > 0 else userassist_obj.FocusTime + time = ( + datetime.timedelta(seconds=seconds) + if seconds > 0 + else userassist_obj.FocusTime + ) item["focus"] = int(userassist_obj.FocusCount) item["time"] = str(time) else: item["id"] = int(userassist_obj.ID) - item["count"] = int(userassist_obj.CountStartingAtFive - if userassist_obj.CountStartingAtFive < 5 else userassist_obj.CountStartingAtFive - 5) + item["count"] = int( + userassist_obj.CountStartingAtFive + if userassist_obj.CountStartingAtFive < 5 + else userassist_obj.CountStartingAtFive - 5 + ) item["focus"] = renderers.NotApplicableValue() item["time"] = renderers.NotApplicableValue() - item["lastupdated"] = conversion.wintime_to_datetime(userassist_obj.LastUpdated.QuadPart) + item["lastupdated"] = conversion.wintime_to_datetime( + userassist_obj.LastUpdated.QuadPart + ) return item @@ -107,151 +135,225 @@ class UserAssist(interfaces.plugins.PluginInterface): elif self._win7 is False: self._userassist_type_name = "_VOL_USERASSIST_TYPES_XP" - self._userassist_size = self.context.symbol_space.get_type(self._reg_table_name + constants.BANG + - self._userassist_type_name).size + self._userassist_size = self.context.symbol_space.get_type( + self._reg_table_name + constants.BANG + self._userassist_type_name + ).size def _win7_or_later(self) -> bool: # TODO: change this if there is a better way of determining the OS version # _KUSER_SHARED_DATA.CookiePad is in Windows 6.1 (Win7) and later - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - return self.context.symbol_space.get_type(kernel.symbol_table_name + constants.BANG + - "_KUSER_SHARED_DATA").has_member('CookiePad') + return self.context.symbol_space.get_type( + kernel.symbol_table_name + constants.BANG + "_KUSER_SHARED_DATA" + ).has_member("CookiePad") - def list_userassist(self, hive: RegistryHive) -> Generator[Tuple[int, Tuple], None, None]: + def list_userassist( + self, hive: RegistryHive + ) -> Generator[Tuple[int, Tuple], None, None]: """Generate userassist data for a registry hive.""" - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - hive_name = hive.hive.cast(kernel.symbol_table_name + constants.BANG + "_CMHIVE").get_name() + hive_name = hive.hive.cast( + kernel.symbol_table_name + constants.BANG + "_CMHIVE" + ).get_name() if self._win7 is None: - try: + with contextlib.suppress(exceptions.SymbolError): self._win7 = self._win7_or_later() - except exceptions.SymbolError: # self._win7 will be None and only registry value rawdata will be output - pass self._determine_userassist_type() - userassist_node_path = hive.get_key("software\\microsoft\\windows\\currentversion\\explorer\\userassist", - return_list = True) + userassist_node_path = hive.get_key( + "software\\microsoft\\windows\\currentversion\\explorer\\userassist", + return_list=True, + ) if not userassist_node_path: vollog.warning("list_userassist did not find a valid node_path (or None)") - return + return None if not isinstance(userassist_node_path, list): vollog.warning("userassist_node_path did not return a list as expected") - return + return None userassist_node = userassist_node_path[-1] # iterate through the GUIDs under the userassist key for guidkey in userassist_node.get_subkeys(): # each guid key should have a Count key in it for countkey in guidkey.get_subkeys(): countkey_path = countkey.get_key_path() - countkey_last_write_time = conversion.wintime_to_datetime(countkey.LastWriteTime.QuadPart) + countkey_last_write_time = conversion.wintime_to_datetime( + countkey.LastWriteTime.QuadPart + ) # output the parent Count key - result: Tuple[int, Tuple[format_hints.Hex, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any]] = ( - 0, (renderers.format_hints.Hex(hive.hive_offset), hive_name, countkey_path, - countkey_last_write_time, "Key", renderers.NotApplicableValue(), renderers.NotApplicableValue(), - renderers.NotApplicableValue(), renderers.NotApplicableValue(), renderers.NotApplicableValue(), - renderers.NotApplicableValue(), renderers.NotApplicableValue()) + result: Tuple[ + int, + Tuple[ + format_hints.Hex, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + Any, + ], + ] = ( + 0, + ( + renderers.format_hints.Hex(hive.hive_offset), + hive_name, + countkey_path, + countkey_last_write_time, + "Key", + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + ), ) yield result # output any subkeys under Count for subkey in countkey.get_subkeys(): - subkey_name = subkey.get_name() - result = (1, ( - renderers.format_hints.Hex(hive.hive_offset), - hive_name, - countkey_path, - countkey_last_write_time, - "Subkey", - subkey_name, - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - renderers.NotApplicableValue(), - )) + result = ( + 1, + ( + renderers.format_hints.Hex(hive.hive_offset), + hive_name, + countkey_path, + countkey_last_write_time, + "Subkey", + subkey_name, + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + ), + ) yield result # output any values under Count for value in countkey.get_values(): - value_name = value.get_name() - try: + with contextlib.suppress(UnicodeDecodeError): value_name = codecs.encode(value_name, "rot_13") - except UnicodeDecodeError: - pass if self._win7: guid = value_name.split("\\")[0] if guid in self._folder_guids: - value_name = value_name.replace(guid, self._folder_guids[guid]) + value_name = value_name.replace( + guid, self._folder_guids[guid] + ) userassist_data_dict = self.parse_userassist_data(value) - result = (1, ( - renderers.format_hints.Hex(hive.hive_offset), - hive_name, - countkey_path, - countkey_last_write_time, - "Value", - value_name, - userassist_data_dict["id"], - userassist_data_dict["count"], - userassist_data_dict["focus"], - userassist_data_dict["time"], - userassist_data_dict["lastupdated"], - format_hints.HexBytes(userassist_data_dict["rawdata"]), - )) + result = ( + 1, + ( + renderers.format_hints.Hex(hive.hive_offset), + hive_name, + countkey_path, + countkey_last_write_time, + "Value", + value_name, + userassist_data_dict["id"], + userassist_data_dict["count"], + userassist_data_dict["focus"], + userassist_data_dict["time"], + userassist_data_dict["lastupdated"], + format_hints.HexBytes(userassist_data_dict["rawdata"]), + ), + ) yield result def _generator(self): - hive_offsets = None - if self.config.get('offset', None) is not None: - hive_offsets = [self.config.get('offset', None)] - kernel = self.context.modules[self.config['kernel']] + if self.config.get("offset", None) is not None: + hive_offsets = [self.config.get("offset", None)] + kernel = self.context.modules[self.config["kernel"]] # get all the user hive offsets or use the one specified - for hive in hivelist.HiveList.list_hives(context = self.context, - base_config_path = self.config_path, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_string = 'ntuser.dat', - hive_offsets = hive_offsets): + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="ntuser.dat", + hive_offsets=hive_offsets, + ): try: yield from self.list_userassist(hive) continue except exceptions.PagedInvalidAddressException as excp: - vollog.debug(f"Invalid address identified in Hive: {hex(excp.invalid_address)}") + vollog.debug( + f"Invalid address identified in Hive: {hex(excp.invalid_address)}" + ) except exceptions.InvalidAddressException as excp: - vollog.debug("Invalid address identified in lower layer {}: {}".format( - excp.layer_name, excp.invalid_address)) + vollog.debug( + "Invalid address identified in lower layer {}: {}".format( + excp.layer_name, excp.invalid_address + ) + ) except KeyError: - vollog.debug("Key '{}' not found in Hive at offset {}.".format( - "software\\microsoft\\windows\\currentversion\\explorer\\userassist", hex(hive.hive_offset))) + vollog.debug( + "Key '{}' not found in Hive at offset {}.".format( + "software\\microsoft\\windows\\currentversion\\explorer\\userassist", + hex(hive.hive_offset), + ) + ) # yield UnreadableValues when an exception occurs for a given hive_offset - result = (0, (renderers.format_hints.Hex(hive.hive_offset), - hive.name if hive.name else renderers.UnreadableValue(), renderers.UnreadableValue(), - renderers.UnreadableValue(), renderers.UnreadableValue(), renderers.UnreadableValue(), - renderers.UnreadableValue(), renderers.UnreadableValue(), renderers.UnreadableValue(), - renderers.UnreadableValue(), renderers.UnreadableValue(), renderers.UnreadableValue())) + result = ( + 0, + ( + renderers.format_hints.Hex(hive.hive_offset), + hive.name if hive.name else renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + renderers.UnreadableValue(), + ), + ) yield result def run(self): - self._reg_table_name = intermed.IntermediateSymbolTable.create(self.context, self._config_path, 'windows', - 'registry') + self._reg_table_name = intermed.IntermediateSymbolTable.create( + self.context, self._config_path, "windows", "registry" + ) - return renderers.TreeGrid([("Hive Offset", renderers.format_hints.Hex), ("Hive Name", str), ("Path", str), - ("Last Write Time", datetime.datetime), ("Type", str), ("Name", str), ("ID", int), - ("Count", int), ("Focus Count", int), ("Time Focused", str), - ("Last Updated", datetime.datetime), ("Raw Data", format_hints.HexBytes)], - self._generator()) + return renderers.TreeGrid( + [ + ("Hive Offset", renderers.format_hints.Hex), + ("Hive Name", str), + ("Path", str), + ("Last Write Time", datetime.datetime), + ("Type", str), + ("Name", str), + ("ID", int), + ("Count", int), + ("Focus Count", int), + ("Time Focused", str), + ("Last Updated", datetime.datetime), + ("Raw Data", format_hints.HexBytes), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/sessions.py b/volatility3/framework/plugins/windows/sessions.py index 6745e95ec..d766b40ea 100644 --- a/volatility3/framework/plugins/windows/sessions.py +++ b/volatility3/framework/plugins/windows/sessions.py @@ -22,48 +22,55 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', - description = 'Windows 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="Windows 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): - kernel = self.context.modules[self.config['kernel']] - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + kernel = self.context.modules[self.config["kernel"]] + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) # Collect all the values as we will want to group them later sessions = {} - for proc in pslist.PsList.list_processes(self.context, - kernel.layer_name, - kernel.symbol_table_name, - filter_func = filter_func): - + for proc in pslist.PsList.list_processes( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + filter_func=filter_func, + ): session_id = proc.get_session_id() # Detect RDP, Console or set default value session_type = renderers.NotAvailableValue() # Construct Username from Process Env - user_domain = '' - user_name = '' + user_domain = "" + user_name = "" for var, val in proc.environment_variables(): - if var.lower() == 'username': + if var.lower() == "username": user_name = val - elif var.lower() == 'userdomain': + elif var.lower() == "userdomain": user_domain = val - if var.lower() == 'sessionname': + if var.lower() == "sessionname": session_type = val # Concat Domain and User - full_user = f'{user_domain}/{user_name}' - if full_user == '/': + full_user = f"{user_domain}/{user_name}" + if full_user == "/": full_user = renderers.NotAvailableValue() # Collect all the values in to a row we can yield after sorting. @@ -73,7 +80,7 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) "process_name": utility.array_to_string(proc.ImageFileName), "user_name": full_user, "process_start": proc.get_create_time(), - "session_type": session_type + "session_type": session_type, } # Add row to correct session so we can sort it later @@ -85,8 +92,14 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) # Group and yield each row for rows in sessions.values(): for row in rows: - yield 0, (row.get('session_id'), row.get('session_type'), row.get('process_id'), - row.get('process_name'), row.get('user_name'), row.get('process_start')) + yield 0, ( + row.get("session_id"), + row.get("session_type"), + row.get("process_id"), + row.get("process_name"), + row.get("user_name"), + row.get("process_start"), + ) def generate_timeline(self): for row in self._generator(): @@ -98,6 +111,14 @@ class Sessions(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface) yield (description, timeliner.TimeLinerType.CREATED, row_data[5]) def run(self): - - return renderers.TreeGrid([("Session ID", int), ('Session Type', str), ("Process ID", int), ("Process", str), - ("User Name", str), ("Create Time", datetime.datetime)], self._generator()) + return renderers.TreeGrid( + [ + ("Session ID", int), + ("Session Type", str), + ("Process ID", int), + ("Process", str), + ("User Name", str), + ("Create Time", datetime.datetime), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/skeleton_key_check.py b/volatility3/framework/plugins/windows/skeleton_key_check.py index cd4a5baec..d321c2cc0 100644 --- a/volatility3/framework/plugins/windows/skeleton_key_check.py +++ b/volatility3/framework/plugins/windows/skeleton_key_check.py @@ -9,7 +9,7 @@ # For a thorough walkthrough on how the R&D was performed to develop this plugin, # please see our blogpost here: # -# +# https://volatility-labs.blogspot.com/2021/10/memory-forensics-r-illustrated.html import io import logging @@ -39,22 +39,33 @@ vollog = logging.getLogger(__name__) class Skeleton_Key_Check(interfaces.plugins.PluginInterface): - """ Looks for signs of Skeleton Key malware """ + """Looks for signs of Skeleton Key malware""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 4, 0) @classmethod def get_requirements(cls): # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.VersionRequirement(name = 'pslist', component = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'vadinfo', component = vadinfo.VadInfo, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'pdbutil', component = pdbutil.PDBUtility, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="pslist", component=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="vadinfo", component=vadinfo.VadInfo, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="pdbutil", component=pdbutil.PDBUtility, version=(1, 0, 0) + ), ] - def _get_pefile_obj(self, pe_table_name: str, layer_name: str, base_address: int) -> pefile.PE: + def _get_pefile_obj( + self, pe_table_name: str, layer_name: str, base_address: int + ) -> pefile.PE: """ Attempts to pefile object from the bytes of the PE file @@ -69,15 +80,17 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): pe_data = io.BytesIO() try: - dos_header = self.context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = base_address, - layer_name = layer_name) + dos_header = self.context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=base_address, + layer_name=layer_name, + ) for offset, data in dos_header.reconstruct(): pe_data.seek(offset) pe_data.write(data) - pe_ret = pefile.PE(data = pe_data.getvalue(), fast_load = True) + pe_ret = pefile.PE(data=pe_data.getvalue(), fast_load=True) except exceptions.InvalidAddressException: vollog.debug("Unable to reconstruct cryptdll.dll in memory") @@ -85,9 +98,12 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): return pe_ret - def _check_for_skeleton_key_vad(self, csystem: interfaces.objects.ObjectInterface, - cryptdll_base: int, - cryptdll_size: int) -> bool: + def _check_for_skeleton_key_vad( + self, + csystem: interfaces.objects.ObjectInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> bool: """ Checks if Initialize and/or Decrypt is hooked by determining if these function pointers reference addresses inside of the cryptdll VAD @@ -99,12 +115,17 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): Returns: bool: if a skeleton key hook is present """ - return not ((cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) and \ - (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size)) + return not ( + (cryptdll_base <= csystem.Initialize <= cryptdll_base + cryptdll_size) + and (cryptdll_base <= csystem.Decrypt <= cryptdll_base + cryptdll_size) + ) - def _check_for_skeleton_key_symbols(self, csystem: interfaces.objects.ObjectInterface, - rc4HmacInitialize: int, - rc4HmacDecrypt: int) -> bool: + def _check_for_skeleton_key_symbols( + self, + csystem: interfaces.objects.ObjectInterface, + rc4HmacInitialize: int, + rc4HmacDecrypt: int, + ) -> bool: """ Uses the PDB information to specifically check if the csystem for RC4HMAC has an initialization pointer to rc4HmacInitialize and a decryption pointer @@ -118,10 +139,16 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): Returns: bool: if a skeleton key hook was found """ - return csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt + return ( + csystem.Initialize != rc4HmacInitialize or csystem.Decrypt != rc4HmacDecrypt + ) - def _construct_ecrypt_array(self, array_start: int, count: int, \ - cryptdll_types: interfaces.context.ModuleInterface) -> interfaces.context.ModuleInterface: + def _construct_ecrypt_array( + self, + array_start: int, + count: int, + cryptdll_types: interfaces.context.ModuleInterface, + ) -> interfaces.context.ModuleInterface: """ Attempts to construct an array of _KERB_ECRYPT structures @@ -135,23 +162,31 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): """ try: - array = cryptdll_types.object(object_type = "array", - offset = array_start, - subtype = cryptdll_types.get_type("_KERB_ECRYPT"), - count = count, - absolute = True) + array = cryptdll_types.object( + object_type="array", + offset=array_start, + subtype=cryptdll_types.get_type("_KERB_ECRYPT"), + count=count, + absolute=True, + ) except exceptions.InvalidAddressException: - vollog.debug("Unable to construct cSystems array at given offset: {:x}".format(array_start)) + vollog.debug( + "Unable to construct cSystems array at given offset: {:x}".format( + array_start + ) + ) array = None return array - def _find_array_with_pdb_symbols(self, cryptdll_symbols: str, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str, - cryptdll_base: int) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: - + def _find_array_with_pdb_symbols( + self, + cryptdll_symbols: str, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + cryptdll_base: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int, int]: """ Finds the CSystems array through use of PDB symbols @@ -167,9 +202,13 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): rc4HmacInitialize: The runtime address of the expected initialization function rc4HmacDecrypt: The runtime address of the expected decryption function """ - cryptdll_module = self.context.module(cryptdll_symbols, layer_name = proc_layer_name, offset = cryptdll_base) + cryptdll_module = self.context.module( + cryptdll_symbols, layer_name=proc_layer_name, offset=cryptdll_base + ) - rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address("rc4HmacInitialize") + rc4HmacInitialize = cryptdll_module.get_absolute_symbol_address( + "rc4HmacInitialize" + ) rc4HmacDecrypt = cryptdll_module.get_absolute_symbol_address("rc4HmacDecrypt") @@ -178,7 +217,9 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): # we do not want to fail just because the count is not in memory # 16 was the size on samples I tested, so I chose it as the default try: - count = cryptdll_types.object(object_type = "unsigned long", offset = count_address) + count = cryptdll_types.object( + object_type="unsigned long", offset=count_address + ) except exceptions.InvalidAddressException: count = 16 @@ -187,15 +228,20 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): array = self._construct_ecrypt_array(array_start, count, cryptdll_types) if array is None: - vollog.debug("The CSystem array is not present in memory. Stopping PDB based analysis.") + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB based analysis." + ) return array, rc4HmacInitialize, rc4HmacDecrypt - def _get_cryptdll_types(self, context: interfaces.context.ContextInterface, - config, - config_path: str, - proc_layer_name: str, - cryptdll_base: int): + def _get_cryptdll_types( + self, + context: interfaces.context.ContextInterface, + config, + config_path: str, + proc_layer_name: str, + cryptdll_base: int, + ): """ Builds a symbol table from the cryptdll types generated after binary analysis @@ -206,19 +252,24 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): proc_layer_name: name of the lsass.exe process layer cryptdll_base: base address of cryptdll.dll inside of lsass.exe """ - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] table_mapping = {"nt_symbols": kernel.symbol_table_name} - cryptdll_symbol_table = intermed.IntermediateSymbolTable.create(context = context, - config_path = config_path, - sub_path = "windows", - filename = "kerb_ecrypt", - table_mapping = table_mapping) + cryptdll_symbol_table = intermed.IntermediateSymbolTable.create( + context=context, + config_path=config_path, + sub_path="windows", + filename="kerb_ecrypt", + table_mapping=table_mapping, + ) - return context.module(cryptdll_symbol_table, proc_layer_name, offset = cryptdll_base) + return context.module( + cryptdll_symbol_table, proc_layer_name, offset=cryptdll_base + ) - def _find_lsass_proc(self, proc_list: Iterable) -> \ - Tuple[interfaces.context.ContextInterface, str]: + def _find_lsass_proc( + self, proc_list: Iterable + ) -> Tuple[interfaces.context.ContextInterface, str]: """ Walks the process list and returns the first valid lsass instances. There should be only one lsass process, but malware will often use the @@ -239,13 +290,17 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): return proc, proc_layer_name except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(proc_id, excp.invalid_address, - excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) return None, None - def _find_cryptdll(self, lsass_proc: interfaces.context.ContextInterface) -> \ - Tuple[int, int]: + def _find_cryptdll( + self, lsass_proc: interfaces.context.ContextInterface + ) -> Tuple[int, int]: """ Finds the base address of cryptdll.dll inside of lsass.exe @@ -262,15 +317,17 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): if isinstance(filename, str) and filename.lower().endswith("cryptdll.dll"): base = vad.get_start() - return base, vad.get_end() - base + return base, vad.get_size() return None, None - def _find_csystems_with_symbols(self, proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int) -> \ - Tuple[interfaces.objects.ObjectInterface, int, int]: + def _find_csystems_with_symbols( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> Tuple[interfaces.objects.ObjectInterface, int, int]: """ Attempts to find CSystems and the expected address of the handlers. Relies on downloading and parsing of the cryptdll PDB file. @@ -288,22 +345,28 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): rc4HmacDecrypt: The expected address of the csystem Decryption function """ try: - cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb(self.context, - interfaces.configuration.path_join( - self.config_path, 'cryptdll'), - proc_layer_name, - "cryptdll.pdb", - cryptdll_base, - cryptdll_size) + cryptdll_symbols = pdbutil.PDBUtility.symbol_table_from_pdb( + self.context, + interfaces.configuration.path_join(self.config_path, "cryptdll"), + proc_layer_name, + "cryptdll.pdb", + cryptdll_base, + cryptdll_size, + ) except exceptions.VolatilityException: - vollog.debug("Unable to use the cryptdll PDB. Stopping PDB symbols based analysis.") + vollog.debug( + "Unable to use the cryptdll PDB. Stopping PDB symbols based analysis." + ) return None, None, None - array, rc4HmacInitialize, rc4HmacDecrypt = \ - self._find_array_with_pdb_symbols(cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base) + array, rc4HmacInitialize, rc4HmacDecrypt = self._find_array_with_pdb_symbols( + cryptdll_symbols, cryptdll_types, proc_layer_name, cryptdll_base + ) if array is None: - vollog.debug("The CSystem array is not present in memory. Stopping PDB symbols based analysis.") + vollog.debug( + "The CSystem array is not present in memory. Stopping PDB symbols based analysis." + ) return array, rc4HmacInitialize, rc4HmacDecrypt @@ -333,10 +396,13 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): return inst.address + inst.size + opnd.mem.disp - def _analyze_cdlocatecsystem(self, function_bytes: bytes, - function_start: int, - cryptdll_types: interfaces.context.ModuleInterface, - proc_layer_name: str) -> Optional[interfaces.objects.ObjectInterface]: + def _analyze_cdlocatecsystem( + self, + function_bytes: bytes, + function_start: int, + cryptdll_types: interfaces.context.ModuleInterface, + proc_layer_name: str, + ) -> Optional[interfaces.objects.ObjectInterface]: """ Performs static analysis on CDLocateCSystem to find the instructions that reference CSystems as well as cCsystems @@ -370,7 +436,12 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): # we do not want to fail just because the count is not in memory # 16 was the size on samples I tested, so I chose it as the default if target_address: - count = int.from_bytes(self.context.layers[proc_layer_name].read(target_address, 4), "little") + count = int.from_bytes( + self.context.layers[proc_layer_name].read( + target_address, 4 + ), + "little", + ) else: count = 16 @@ -392,10 +463,13 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): return array - def _find_csystems_with_export(self, proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - _) -> Optional[interfaces.objects.ObjectInterface]: + def _find_csystems_with_export( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + _, + ) -> Optional[interfaces.objects.ObjectInterface]: """ Uses export table analysis to locate CDLocateCsystem This function references CSystems and cCsystems @@ -410,23 +484,27 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): """ if not has_capstone: - vollog.debug("capstone is not installed so cannot fall back to export table analysis.") + vollog.debug( + "capstone is not installed so cannot fall back to export table analysis." + ) return None - vollog.debug("Unable to perform analysis using PDB symbols, falling back to export table analysis.") + vollog.debug( + "Unable to perform analysis using PDB symbols, falling back to export table analysis." + ) - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) cryptdll = self._get_pefile_obj(pe_table_name, proc_layer_name, cryptdll_base) if not cryptdll: return None - cryptdll.parse_data_directories(directories = [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]]) - if not hasattr(cryptdll, 'DIRECTORY_ENTRY_EXPORT'): + cryptdll.parse_data_directories( + directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] + ) + if not hasattr(cryptdll, "DIRECTORY_ENTRY_EXPORT"): return None # find the location of CDLocateCSystem and then perform static analysis @@ -437,24 +515,34 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): function_start = cryptdll_base + export.address try: - function_bytes = self.context.layers[proc_layer_name].read(function_start, 0x50) + function_bytes = self.context.layers[proc_layer_name].read( + function_start, 0x50 + ) except exceptions.InvalidAddressException: vollog.debug( - "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis.") + "The CDLocateCSystem function is not present in the lsass address space. Stopping export based analysis." + ) break - array = self._analyze_cdlocatecsystem(function_bytes, function_start, cryptdll_types, proc_layer_name) + array = self._analyze_cdlocatecsystem( + function_bytes, function_start, cryptdll_types, proc_layer_name + ) if array is None: - vollog.debug("The CSystem array is not present in memory. Stopping export based analysis.") + vollog.debug( + "The CSystem array is not present in memory. Stopping export based analysis." + ) return array return None - def _find_csystems_with_scanning(self, proc_layer_name: str, - cryptdll_types: interfaces.context.ModuleInterface, - cryptdll_base: int, - cryptdll_size: int) -> List[interfaces.context.ModuleInterface]: + def _find_csystems_with_scanning( + self, + proc_layer_name: str, + cryptdll_types: interfaces.context.ModuleInterface, + cryptdll_base: int, + cryptdll_size: int, + ) -> List[interfaces.context.ModuleInterface]: """ Performs scanning to find potential RC4 HMAC csystem instances @@ -480,22 +568,22 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): # scan for potential instances of RC4 HMAC # the signature is based on the type being 0x17 # and the block size member being 1 in all test samples - for address in proc_layer.scan(self.context, - scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), - sections = [(cryptdll_base, cryptdll_size)]): - + for address in proc_layer.scan( + self.context, + scanners.BytesScanner(b"\x17\x00\x00\x00\x01\x00\x00\x00"), + sections=[(cryptdll_base, cryptdll_size)], + ): # this occurs across page boundaries if not proc_layer.is_valid(address, ecrypt_size): continue - kerb = cryptdll_types.object("_KERB_ECRYPT", - offset = address, - absolute = True) + kerb = cryptdll_types.object("_KERB_ECRYPT", offset=address, absolute=True) # ensure the Encrypt and Finish pointers are inside the VAD # these are not manipulated in the attack - if (cryptdll_base < kerb.Encrypt < cryptdll_end) and \ - (cryptdll_base < kerb.Finish < cryptdll_end): + if (cryptdll_base < kerb.Encrypt < cryptdll_end) and ( + cryptdll_base < kerb.Finish < cryptdll_end + ): csystems.append(kerb) return csystems @@ -509,60 +597,64 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): Args: procs: the process list filtered to lsass.exe instances """ - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] if not symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name): vollog.info("This plugin only supports 64bit Windows memory samples") - return + return None lsass_proc, proc_layer_name = self._find_lsass_proc(procs) if not lsass_proc: vollog.info( - "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed.") - return + "Unable to find a valid lsass.exe process in the process list. This should never happen. Analysis cannot proceed." + ) + return None cryptdll_base, cryptdll_size = self._find_cryptdll(lsass_proc) if not cryptdll_base: - vollog.info("Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed.") - return + vollog.info( + "Unable to find the location of cryptdll.dll inside of lsass.exe. Analysis cannot proceed." + ) + return None # the custom type information from binary analysis - cryptdll_types = self._get_cryptdll_types(self.context, - self.config, - self.config_path, - proc_layer_name, - cryptdll_base) + cryptdll_types = self._get_cryptdll_types( + self.context, self.config, self.config_path, proc_layer_name, cryptdll_base + ) # attempt to find the array and symbols directly from the PDB - csystems, rc4HmacInitialize, rc4HmacDecrypt = \ - self._find_csystems_with_symbols(proc_layer_name, - cryptdll_types, - cryptdll_base, - cryptdll_size) + csystems, rc4HmacInitialize, rc4HmacDecrypt = self._find_csystems_with_symbols( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) # if we can't find cSystems through the PDB then # we fall back to export analysis and scanning # we keep the address of the rc4 functions from the PDB # though as its our only source to get them if csystems is None: - fallback_sources = [self._find_csystems_with_export, - self._find_csystems_with_scanning] + fallback_sources = [ + self._find_csystems_with_export, + self._find_csystems_with_scanning, + ] for source in fallback_sources: - csystems = source(proc_layer_name, - cryptdll_types, - cryptdll_base, - cryptdll_size) + csystems = source( + proc_layer_name, cryptdll_types, cryptdll_base, cryptdll_size + ) if csystems is not None: break if csystems is None: - vollog.info("Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed.") - return + vollog.info( + "Unable to find CSystems inside of cryptdll.dll. Analysis cannot proceed." + ) + return None for csystem in csystems: - if not self.context.layers[proc_layer_name].is_valid(csystem.vol.offset, csystem.vol.size): + if not self.context.layers[proc_layer_name].is_valid( + csystem.vol.offset, csystem.vol.size + ): continue # filter for RC4 HMAC @@ -571,12 +663,21 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): # use the specific symbols if present, otherwise use the vad start and size if rc4HmacInitialize and rc4HmacDecrypt: - skeleton_key_present = self._check_for_skeleton_key_symbols(csystem, rc4HmacInitialize, rc4HmacDecrypt) + skeleton_key_present = self._check_for_skeleton_key_symbols( + csystem, rc4HmacInitialize, rc4HmacDecrypt + ) else: - skeleton_key_present = self._check_for_skeleton_key_vad(csystem, cryptdll_base, cryptdll_size) + skeleton_key_present = self._check_for_skeleton_key_vad( + csystem, cryptdll_base, cryptdll_size + ) - yield 0, (lsass_proc.UniqueProcessId, "lsass.exe", skeleton_key_present, \ - format_hints.Hex(csystem.Initialize), format_hints.Hex(csystem.Decrypt)) + yield 0, ( + lsass_proc.UniqueProcessId, + "lsass.exe", + skeleton_key_present, + format_hints.Hex(csystem.Initialize), + format_hints.Hex(csystem.Decrypt), + ) def _lsass_proc_filter(self, proc): """ @@ -590,13 +691,22 @@ class Skeleton_Key_Check(interfaces.plugins.PluginInterface): return process_name != "lsass.exe" def run(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] return renderers.TreeGrid( - [("PID", int), ("Process", str), ("Skeleton Key Found", bool), ("rc4HmacInitialize", format_hints.Hex), - ("rc4HmacDecrypt", format_hints.Hex)], + [ + ("PID", int), + ("Process", str), + ("Skeleton Key Found", bool), + ("rc4HmacInitialize", format_hints.Hex), + ("rc4HmacDecrypt", format_hints.Hex), + ], self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = self._lsass_proc_filter))) + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=self._lsass_proc_filter, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/ssdt.py b/volatility3/framework/plugins/windows/ssdt.py index 0d921535d..6a47c36e9 100644 --- a/volatility3/framework/plugins/windows/ssdt.py +++ b/volatility3/framework/plugins/windows/ssdt.py @@ -24,14 +24,23 @@ class SSDT(plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'modules', plugin = modules.Modules, version = (1, 0, 0)), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="modules", plugin=modules.Modules, version=(1, 0, 0) + ), ] @classmethod - def build_module_collection(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> contexts.ModuleCollection: + def build_module_collection( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> contexts.ModuleCollection: """Builds a collection of modules. Args: @@ -47,7 +56,6 @@ class SSDT(plugins.PluginInterface): context_modules = [] for mod in mods: - try: module_name_with_ext = mod.BaseDllName.get_string() except exceptions.InvalidAddressException: @@ -60,45 +68,52 @@ class SSDT(plugins.PluginInterface): if module_name in constants.windows.KERNEL_MODULE_NAMES: symbol_table_name = symbol_table - context_module = contexts.SizedModule.create(context = context, - module_name = module_name, - layer_name = layer_name, - offset = mod.DllBase, - size = mod.SizeOfImage, - symbol_table_name = symbol_table_name) + context_module = contexts.SizedModule.create( + context=context, + module_name=module_name, + layer_name=layer_name, + offset=mod.DllBase, + size=mod.SizeOfImage, + symbol_table_name=symbol_table_name, + ) context_modules.append(context_module) return contexts.ModuleCollection(context_modules) def _generator(self) -> Iterator[Tuple[int, Tuple[int, int, Any, Any]]]: - - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] layer_name = kernel.layer_name - collection = self.build_module_collection(self.context, layer_name, kernel.symbol_table_name) + collection = self.build_module_collection( + self.context, layer_name, kernel.symbol_table_name + ) - kvo = self.context.layers[layer_name].config['kernel_virtual_offset'] - ntkrnlmp = self.context.module(kernel.symbol_table_name, layer_name = layer_name, offset = kvo) + kvo = self.context.layers[layer_name].config["kernel_virtual_offset"] + ntkrnlmp = self.context.module( + kernel.symbol_table_name, layer_name=layer_name, offset=kvo + ) # this is just one way to enumerate the native (NT) service table. # to do the same thing for the Win32K service table, we would need Win32K.sys symbol support ## we could also find nt!KeServiceDescriptorTable (NT) and KeServiceDescriptorTableShadow (NT, Win32K) service_table_address = ntkrnlmp.get_symbol("KiServiceTable").address service_limit_address = ntkrnlmp.get_symbol("KiServiceLimit").address - service_limit = ntkrnlmp.object(object_type = "int", offset = service_limit_address) + service_limit = ntkrnlmp.object(object_type="int", offset=service_limit_address) # on 32-bit systems the table indexes are 32-bits and contain pointers (unsigned) # on 64-bit systems the indexes are also 32-bits but they're offsets from the # base address of the table and can be negative, so we need a signed data type - is_kernel_64 = symbols.symbol_table_is_64bit(self.context, kernel.symbol_table_name) + is_kernel_64 = symbols.symbol_table_is_64bit( + self.context, kernel.symbol_table_name + ) if is_kernel_64: array_subtype = "long" - def kvo_calulator(func: int) -> int: + def kvo_calculator(func: int) -> int: return kvo + service_table_address + (func >> 4) - find_address = kvo_calulator + find_address = kvo_calculator else: array_subtype = "unsigned long" @@ -107,26 +122,52 @@ class SSDT(plugins.PluginInterface): find_address = passthrough - functions = ntkrnlmp.object(object_type = "array", - offset = service_table_address, - subtype = ntkrnlmp.get_type(array_subtype), - count = service_limit) + functions = ntkrnlmp.object( + object_type="array", + offset=service_table_address, + subtype=ntkrnlmp.get_type(array_subtype), + count=service_limit, + ) for idx, function_obj in enumerate(functions): - function = find_address(function_obj) - module_symbols = collection.get_module_symbols_by_absolute_location(function) + module_symbols = collection.get_module_symbols_by_absolute_location( + function + ) for module_name, symbol_generator in module_symbols: symbols_found = False for symbol in symbol_generator: symbols_found = True - yield (0, (idx, format_hints.Hex(function), module_name, symbol.split(constants.BANG)[1])) + yield ( + 0, + ( + idx, + format_hints.Hex(function), + module_name, + symbol.split(constants.BANG)[1], + ), + ) if not symbols_found: - yield (0, (idx, format_hints.Hex(function), module_name, renderers.NotAvailableValue())) + yield ( + 0, + ( + idx, + format_hints.Hex(function), + module_name, + renderers.NotAvailableValue(), + ), + ) def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([("Index", int), ("Address", format_hints.Hex), ("Module", str), ("Symbol", str)], - self._generator()) + return renderers.TreeGrid( + [ + ("Index", int), + ("Address", format_hints.Hex), + ("Module", str), + ("Symbol", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/strings.py b/volatility3/framework/plugins/windows/strings.py index e79f64741..0eaa65884 100644 --- a/volatility3/framework/plugins/windows/strings.py +++ b/volatility3/framework/plugins/windows/strings.py @@ -25,28 +25,39 @@ class Strings(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process ID to include (all other processes are excluded)", - optional = True), - requirements.URIRequirement(name = "strings_file", description = "Strings file") + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process ID to include (all other processes are excluded)", + optional=True, + ), + requirements.URIRequirement( + name="strings_file", description="Strings file" + ), ] # TODO: Make URLRequirement that can accept a file address which the framework can open def run(self): - return renderers.TreeGrid([("String", str), ("Physical Address", format_hints.Hex), ("Result", str)], - self._generator()) + return renderers.TreeGrid( + [("String", str), ("Physical Address", format_hints.Hex), ("Result", str)], + self._generator(), + ) def _generator(self) -> Generator[Tuple, None, None]: """Generates results from a strings file.""" - string_list: List[Tuple[int,bytes]] = [] + string_list: List[Tuple[int, bytes]] = [] # Test strings file format is accurate accessor = resources.ResourceAccessor() - strings_fp = accessor.open(self.config['strings_file'], "rb") + strings_fp = accessor.open(self.config["strings_file"], "rb") line = strings_fp.readline() count: float = 0 while line: @@ -57,24 +68,35 @@ class Strings(interfaces.plugins.PluginInterface): except ValueError: vollog.error(f"Line in unrecognized format: line {count}") line = strings_fp.readline() - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - revmap = self.generate_mapping(self.context, - kernel.layer_name, - kernel.symbol_table_name, - progress_callback = self._progress_callback, - pid_list = self.config['pid']) + revmap = self.generate_mapping( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + progress_callback=self._progress_callback, + pid_list=self.config["pid"], + ) last_prog: float = 0 - line_count: float = 0 + line_count: float = 0 num_strings = len(string_list) for offset, string in string_list: line_count += 1 try: - revmap_list = [name + ":" + hex(offset) for (name, offset) in revmap[offset >> 12]] + revmap_list = [ + name + ":" + hex(offset) for (name, offset) in revmap[offset >> 12] + ] except (IndexError, KeyError): revmap_list = ["FREE MEMORY"] - yield (0, (str(string, 'latin-1'), format_hints.Hex(offset), ", ".join(revmap_list))) + yield ( + 0, + ( + str(string, "latin-1"), + format_hints.Hex(offset), + ", ".join(revmap_list), + ), + ) prog = line_count / num_strings * 100 if round(prog, 1) > last_prog: last_prog = round(prog, 1) @@ -97,12 +119,14 @@ class Strings(interfaces.plugins.PluginInterface): return int(offset), string @classmethod - def generate_mapping(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str, - progress_callback: constants.ProgressCallback = None, - pid_list: Optional[List[int]] = None) -> Dict[int, Set[Tuple[str, int]]]: + def generate_mapping( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + progress_callback: constants.ProgressCallback = None, + pid_list: Optional[List[int]] = None, + ) -> Dict[int, Set[Tuple[str, int]]]: """Creates a reverse mapping between virtual addresses and physical addresses. @@ -122,39 +146,55 @@ class Strings(interfaces.plugins.PluginInterface): reverse_map: Dict[int, Set[Tuple[str, int]]] = dict() if isinstance(layer, intel.Intel): # We don't care about errors, we just wanted chunks that map correctly - for mapval in layer.mapping(0x0, layer.maximum_address, ignore_errors = True): + for mapval in layer.mapping(0x0, layer.maximum_address, ignore_errors=True): offset, _, mapped_offset, mapped_size, maplayer = mapval for val in range(mapped_offset, mapped_offset + mapped_size, 0x1000): - cur_set = reverse_map.get(mapped_offset >> 12, set()) + cur_set = reverse_map.get(val >> 12, set()) cur_set.add(("kernel", offset)) - reverse_map[mapped_offset >> 12] = cur_set + reverse_map[val >> 12] = cur_set if progress_callback: - progress_callback((offset * 100) / layer.maximum_address, "Creating reverse kernel map") + progress_callback( + (offset * 100) / layer.maximum_address, + "Creating reverse kernel map", + ) # TODO: Include kernel modules - for process in pslist.PsList.list_processes(context, layer_name, symbol_table): + for process in pslist.PsList.list_processes( + context, layer_name, symbol_table + ): if not filter(process): proc_id = "Unknown" try: proc_id = process.UniqueProcessId proc_layer_name = process.add_process_layer() except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format( - proc_id, excp.invalid_address, excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) continue proc_layer = context.layers[proc_layer_name] if isinstance(proc_layer, linear.LinearlyMappedLayer): - for mapval in proc_layer.mapping(0x0, proc_layer.maximum_address, ignore_errors = True): + for mapval in proc_layer.mapping( + 0x0, proc_layer.maximum_address, ignore_errors=True + ): mapped_offset, _, offset, mapped_size, maplayer = mapval - for val in range(mapped_offset, mapped_offset + mapped_size, 0x1000): + for val in range( + mapped_offset, mapped_offset + mapped_size, 0x1000 + ): cur_set = reverse_map.get(mapped_offset >> 12, set()) - cur_set.add((f"Process {process.UniqueProcessId}", offset)) + cur_set.add( + (f"Process {process.UniqueProcessId}", offset) + ) reverse_map[mapped_offset >> 12] = cur_set # FIXME: make the progress for all processes, rather than per-process if progress_callback: - progress_callback((offset * 100) / layer.maximum_address, - f"Creating mapping for task {process.UniqueProcessId}") + progress_callback( + (offset * 100) / layer.maximum_address, + f"Creating mapping for task {process.UniqueProcessId}", + ) return reverse_map diff --git a/volatility3/framework/plugins/windows/svcscan.py b/volatility3/framework/plugins/windows/svcscan.py index b6adbb0b0..10de46e2a 100644 --- a/volatility3/framework/plugins/windows/svcscan.py +++ b/volatility3/framework/plugins/windows/svcscan.py @@ -4,45 +4,113 @@ import logging import os -from typing import List +from typing import Dict, List, NamedTuple, Optional, Tuple, Union, cast -from volatility3.framework import interfaces, renderers, constants, symbols, exceptions +from volatility3.framework import ( + constants, + exceptions, + interfaces, + objects, + renderers, + symbols, +) from volatility3.framework.configuration import requirements from volatility3.framework.layers import scanners from volatility3.framework.renderers import format_hints from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import versions from volatility3.framework.symbols.windows.extensions import services -from volatility3.plugins.windows import poolscanner, vadyarascan, pslist +from volatility3.plugins.windows import poolscanner, pslist, vadyarascan +from volatility3.plugins.windows.registry import hivelist vollog = logging.getLogger(__name__) +ServiceBinaryInfo = NamedTuple( + "ServiceBinaryInfo", + [ + ("dll", Union[str, interfaces.renderers.BaseAbsentValue]), + ("binary", Union[str, interfaces.renderers.BaseAbsentValue]), + ], +) + + class SvcScan(interfaces.plugins.PluginInterface): """Scans for windows services.""" _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'poolscanner', plugin = poolscanner.PoolScanner, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'vadyarascan', plugin = vadyarascan.VadYaraScan, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="poolscanner", plugin=poolscanner.PoolScanner, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="vadyarascan", plugin=vadyarascan.VadYaraScan, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), ] @staticmethod - def get_record_tuple(service_record: interfaces.objects.ObjectInterface): - return (format_hints.Hex(service_record.vol.offset), service_record.Order, service_record.get_pid(), - service_record.Start.description, service_record.State.description, service_record.get_type(), - service_record.get_name(), service_record.get_display(), service_record.get_binary()) + def get_record_tuple( + service_record: interfaces.objects.ObjectInterface, + binary_info: ServiceBinaryInfo, + ): + return ( + format_hints.Hex(service_record.vol.offset), + service_record.Order, + service_record.get_pid(), + service_record.Start.description, + service_record.State.description, + service_record.get_type(), + service_record.get_name(), + service_record.get_display(), + service_record.get_binary(), + binary_info.binary, + binary_info.dll, + ) + + # These checks must be completed from newest -> oldest OS version. + _win_version_file_map: List[Tuple[versions.OsDistinguisher, bool, str]] = [ + (versions.is_win10_25398_or_later, True, "services-win10-25398-x64"), + (versions.is_win10_19041_or_later, True, "services-win10-19041-x64"), + (versions.is_win10_19041_or_later, False, "services-win10-19041-x86"), + (versions.is_win10_18362_or_later, True, "services-win10-18362-x64"), + (versions.is_win10_18362_or_later, False, "services-win10-18362-x86"), + (versions.is_win10_17763_or_later, False, "services-win10-17763-x86"), + (versions.is_win10_16299_or_later, True, "services-win10-16299-x64"), + (versions.is_win10_16299_or_later, False, "services-win10-16299-x86"), + (versions.is_win10_15063, True, "services-win10-15063-x64"), + (versions.is_win10_15063, False, "services-win10-15063-x86"), + (versions.is_win10_up_to_15063, True, "services-win8-x64"), + (versions.is_win10_up_to_15063, False, "services-win8-x86"), + (versions.is_windows_8_or_later, True, "services-win8-x64"), + (versions.is_windows_8_or_later, True, "services-win8-x86"), + (versions.is_vista_or_later, True, "services-vista-x64"), + (versions.is_vista_or_later, False, "services-vista-x86"), + (versions.is_windows_xp, False, "services-xp-x86"), + (versions.is_xp_or_2003, True, "services-xp-2003-x64"), + ] @staticmethod - def create_service_table(context: interfaces.context.ContextInterface, symbol_table: str, config_path: str) -> str: + def create_service_table( + context: interfaces.context.ContextInterface, + symbol_table: str, + config_path: str, + ) -> str: """Constructs a symbol table containing the symbols for services depending upon the operating system in use. @@ -57,53 +125,138 @@ class SvcScan(interfaces.plugins.PluginInterface): native_types = context.symbol_space[symbol_table].natives is_64bit = symbols.symbol_table_is_64bit(context, symbol_table) - if versions.is_windows_xp(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-xp-x86" - elif versions.is_xp_or_2003(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-xp-2003-x64" - elif versions.is_win10_16299_or_later(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-win10-16299-x64" - elif versions.is_win10_16299_or_later(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-win10-16299-x86" - elif versions.is_win10_up_to_15063(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-win8-x64" - elif versions.is_win10_up_to_15063(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-win8-x86" - elif versions.is_win10_15063(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-win10-15063-x64" - elif versions.is_win10_15063(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-win10-15063-x86" - elif versions.is_windows_8_or_later(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-win8-x64" - elif versions.is_windows_8_or_later(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-win8-x86" - elif versions.is_vista_or_later(context = context, symbol_table = symbol_table) and is_64bit: - symbol_filename = "services-vista-x64" - elif versions.is_vista_or_later(context = context, symbol_table = symbol_table) and not is_64bit: - symbol_filename = "services-vista-x86" - else: + try: + symbol_filename = next( + filename + for version_check, for_64bit, filename in SvcScan._win_version_file_map + if is_64bit == for_64bit + and version_check(context=context, symbol_table=symbol_table) + ) + except StopIteration: raise NotImplementedError("This version of Windows is not supported!") - return intermed.IntermediateSymbolTable.create(context, - config_path, - os.path.join("windows", "services"), - symbol_filename, - class_types = services.class_types, - native_types = native_types) + return intermed.IntermediateSymbolTable.create( + context, + config_path, + os.path.join("windows", "services"), + symbol_filename, + class_types=services.class_types, + native_types=native_types, + ) + + def _get_service_key(self, kernel) -> Optional[objects.StructType]: + for hive in hivelist.HiveList.list_hives( + context=self.context, + base_config_path=interfaces.configuration.path_join( + self.config_path, "hivelist" + ), + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_string="machine\\system", + ): + # Get ControlSet\Services. + try: + return cast( + objects.StructType, hive.get_key(r"CurrentControlSet\Services") + ) + except (KeyError, exceptions.InvalidAddressException): + try: + return cast( + objects.StructType, hive.get_key(r"ControlSet001\Services") + ) + except (KeyError, exceptions.InvalidAddressException): + vollog.log( + constants.LOGLEVEL_VVVV, + "Could not retrieve any control set from SYSTEM hive", + ) + + return None + + @staticmethod + def _get_service_dll( + service_key, + ) -> Union[str, interfaces.renderers.BaseAbsentValue]: + try: + param_key = next( + key + for key in service_key.get_subkeys() + if key.get_name() == "Parameters" + ) + return ( + next( + val + for val in param_key.get_values() + if val.get_name() == "ServiceDll" + ) + .decode_data() + .decode("utf-16") + .rstrip("\x00") + ) + + except UnicodeDecodeError: + return renderers.UnparsableValue() + except StopIteration: + return renderers.UnreadableValue() + + @staticmethod + def _get_service_binary( + service_key, + ) -> Union[str, interfaces.renderers.BaseAbsentValue]: + try: + return ( + next( + val + for val in service_key.get_values() + if val.get_name() == "ImagePath" + ) + .decode_data() + .decode("utf-16") + .rstrip("\x00") + ) + + except UnicodeDecodeError: + return renderers.UnparsableValue() + except StopIteration: + return renderers.UnreadableValue() + + @staticmethod + def _get_service_binary_map( + services_key: interfaces.objects.ObjectInterface, + ) -> Dict[str, ServiceBinaryInfo]: + services = services_key.get_subkeys() + return { + service_key.get_name(): ServiceBinaryInfo( + SvcScan._get_service_dll(service_key), + SvcScan._get_service_binary(service_key), + ) + for service_key in services + } def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - service_table_name = self.create_service_table(self.context, kernel.symbol_table_name, - self.config_path) + service_table_name = self.create_service_table( + self.context, kernel.symbol_table_name, self.config_path + ) - relative_tag_offset = self.context.symbol_space.get_type(service_table_name + constants.BANG + - "_SERVICE_RECORD").relative_child_offset("Tag") + # Building the dictionary ahead of time is much better for performance + # vs looking up each service's DLL individually. + services_key = self._get_service_key(kernel) + service_binary_dll_map = ( + self._get_service_binary_map(services_key) + if services_key is not None + else {} + ) + + relative_tag_offset = self.context.symbol_space.get_type( + service_table_name + constants.BANG + "_SERVICE_RECORD" + ).relative_child_offset("Tag") filter_func = pslist.PsList.create_name_filter(["services.exe"]) - is_vista_or_later = versions.is_vista_or_later(context = self.context, - symbol_table = kernel.symbol_table_name) + is_vista_or_later = versions.is_vista_or_later( + context=self.context, symbol_table=kernel.symbol_table_name + ) if is_vista_or_later: service_tag = b"serH" @@ -112,39 +265,57 @@ class SvcScan(interfaces.plugins.PluginInterface): seen = [] - for task in pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func): - + for task in pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ): proc_id = "Unknown" try: proc_id = task.UniqueProcessId proc_layer_name = task.add_process_layer() except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(proc_id, excp.invalid_address, - excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) continue layer = self.context.layers[proc_layer_name] - for offset in layer.scan(context = self.context, - scanner = scanners.BytesScanner(needle = service_tag), - sections = vadyarascan.VadYaraScan.get_vad_maps(task)): - + for offset in layer.scan( + context=self.context, + scanner=scanners.BytesScanner(needle=service_tag), + sections=vadyarascan.VadYaraScan.get_vad_maps(task), + ): if not is_vista_or_later: - service_record = self.context.object(service_table_name + constants.BANG + "_SERVICE_RECORD", - offset = offset - relative_tag_offset, - layer_name = proc_layer_name) + service_record = self.context.object( + service_table_name + constants.BANG + "_SERVICE_RECORD", + offset=offset - relative_tag_offset, + layer_name=proc_layer_name, + ) if not service_record.is_valid(): continue - yield (0, self.get_record_tuple(service_record)) + service_info = service_binary_dll_map.get( + service_record.get_name(), + ServiceBinaryInfo( + renderers.UnreadableValue(), renderers.UnreadableValue() + ), + ) + yield ( + 0, + self.get_record_tuple(service_record, service_info), + ) else: - service_header = self.context.object(service_table_name + constants.BANG + "_SERVICE_HEADER", - offset = offset, - layer_name = proc_layer_name) + service_header = self.context.object( + service_table_name + constants.BANG + "_SERVICE_HEADER", + offset=offset, + layer_name=proc_layer_name, + ) if not service_header.is_valid(): continue @@ -156,17 +327,31 @@ class SvcScan(interfaces.plugins.PluginInterface): if service_record in seen: break seen.append(service_record) - yield (0, self.get_record_tuple(service_record)) + service_info = service_binary_dll_map.get( + service_record.get_name(), + ServiceBinaryInfo( + renderers.UnreadableValue(), renderers.UnreadableValue() + ), + ) + yield ( + 0, + self.get_record_tuple(service_record, service_info), + ) def run(self): - return renderers.TreeGrid([ - ('Offset', format_hints.Hex), - ('Order', int), - ('PID', int), - ('Start', str), - ('State', str), - ('Type', str), - ('Name', str), - ('Display', str), - ('Binary', str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Order", int), + ("PID", int), + ("Start", str), + ("State", str), + ("Type", str), + ("Name", str), + ("Display", str), + ("Binary", str), + ("Binary (Registry)", str), + ("Dll", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/symlinkscan.py b/volatility3/framework/plugins/windows/symlinkscan.py index ef970b296..89fdf142e 100644 --- a/volatility3/framework/plugins/windows/symlinkscan.py +++ b/volatility3/framework/plugins/windows/symlinkscan.py @@ -20,16 +20,20 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa @classmethod def get_requirements(cls): return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), ] @classmethod - def scan_symlinks(cls, - context: interfaces.context.ContextInterface, - layer_name: str, - symbol_table: str) -> \ - Iterable[interfaces.objects.ObjectInterface]: + def scan_symlinks( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[interfaces.objects.ObjectInterface]: """Scans for links using the poolscanner module and constraints. Args: @@ -41,18 +45,22 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa A list of symlink objects found by scanning memory for the Symlink pool signatures """ - constraints = poolscanner.PoolScanner.builtin_constraints(symbol_table, [b'Sym\xe2', b'Symb']) - - for result in poolscanner.PoolScanner.generate_pool_scan(context, layer_name, symbol_table, constraints): + constraints = poolscanner.PoolScanner.builtin_constraints( + symbol_table, [b"Sym\xe2", b"Symb"] + ) + for result in poolscanner.PoolScanner.generate_pool_scan( + context, layer_name, symbol_table, constraints + ): _constraint, mem_object, _header = result yield mem_object def _generator(self): - kernel = self.context.modules[self.config['kernel']] - - for link in self.scan_symlinks(self.context, kernel.layer_name, kernel.symbol_table_name): + kernel = self.context.modules[self.config["kernel"]] + for link in self.scan_symlinks( + self.context, kernel.layer_name, kernel.symbol_table_name + ): try: from_name = link.get_link_name() except (ValueError, exceptions.InvalidAddressException): @@ -63,7 +71,15 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa except exceptions.InvalidAddressException: continue - yield (0, (format_hints.Hex(link.vol.offset), link.get_create_time(), from_name, to_name)) + yield ( + 0, + ( + format_hints.Hex(link.vol.offset), + link.get_create_time(), + from_name, + to_name, + ), + ) def generate_timeline(self): for row in self._generator(): @@ -72,9 +88,12 @@ class SymlinkScan(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterfa yield (description, timeliner.TimeLinerType.CREATED, row_data[1]) def run(self): - return renderers.TreeGrid([ - ("Offset", format_hints.Hex), - ("CreateTime", datetime.datetime), - ("From Name", str), - ("To Name", str), - ], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("CreateTime", datetime.datetime), + ("From Name", str), + ("To Name", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/truecrypt.py b/volatility3/framework/plugins/windows/truecrypt.py new file mode 100644 index 000000000..81250a749 --- /dev/null +++ b/volatility3/framework/plugins/windows/truecrypt.py @@ -0,0 +1,144 @@ +# This file is Copyright 2024 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 Iterable, Generator, List, Tuple + +from volatility3.framework import constants, interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces.configuration import RequirementInterface +from volatility3.framework.interfaces.objects import ObjectInterface +from volatility3.framework.objects import Bytes, DataFormatInfo, Integer, StructType +from volatility3.framework.objects.templates import ObjectTemplate +from volatility3.framework.objects.utility import array_to_string +from volatility3.framework.renderers import format_hints +from volatility3.framework.symbols import intermed +from volatility3.framework.symbols.windows.extensions import pe + +from volatility3.plugins.windows import modules + + +class Passphrase(interfaces.plugins.PluginInterface): + """TrueCrypt Cached Passphrase Finder""" + + _version = (0, 1, 0) + _required_framework_version = (2, 5, 2) + + @classmethod + def get_requirements(cls) -> List[RequirementInterface]: + return [ + requirements.ModuleRequirement( + "kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.VersionRequirement( + name="modules", component=modules.Modules, version=(1, 1, 0) + ), + requirements.IntRequirement( + name="min-length", + description="Minimum length of passphrases to identify", + default=5, + optional=True, + ), + ] + + def scan_module( + self, module_base: int, layer_name: str + ) -> Generator[Tuple[int, str], None, None]: + """Scans the TrueCrypt kernel module for cached passphrases. + + Args: + module_base: the module's DLL base + layer_name: the name of the layer in which the module resides + + Generates: + A tuple of the offset at which a password is found, and the password + """ + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) + dos_header: pe.IMAGE_DOS_HEADER = self.context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + layer_name, + module_base, + ) + data_section: StructType = next( + sec + for sec in dos_header.get_nt_header().get_sections() + if array_to_string(sec.Name) == ".data" + ) + base: int = data_section.VirtualAddress + module_base + size: int = data_section.Misc.VirtualSize + # Looking at `Length` in TrueCrypt/Common/Password.h::Password struct + DWORD_SIZE_BYTES: int = 4 + format = DataFormatInfo( + length=DWORD_SIZE_BYTES, byteorder="little", signed=True + ) + int32 = ObjectTemplate( + Integer, pe_table_name + constants.BANG + "int", data_format=format + ) + count, not_aligned = divmod(size, DWORD_SIZE_BYTES) + if not_aligned: + raise ValueError("PE data section not DWORD-aligned!") + lengths = self.context.object( + pe_table_name + constants.BANG + "array", + layer_name, + base, + count=count, + subtype=int32, + ) + min_length = self.config.get("min-length") + for length in lengths: + # TrueCrypt maximum password length is 64 + # (see TrueCrypt/Common/Password.h) + if not min_length <= length <= 64: + continue + offset = length.vol["offset"] + DWORD_SIZE_BYTES + passphrase: Bytes = self.context.object( + pe_table_name + constants.BANG + "bytes", + layer_name, + offset, + length=length, + ) + # TrueCrypt/Common/Password.c permits chars in the range + # [0x20, 0x7F). + if not all(0x20 <= c < 0x7F for c in passphrase): + continue + # TrueCrypt/Common/Password.h::Password struct is padded with + # 3 zero bytes to keep 64-byte alignment. + buf: Bytes = self.context.object( + pe_table_name + constants.BANG + "bytes", + layer_name, + offset + length + 1, # +1 for '\0'-terminated password string + length=3, + ) + if any(buf): + continue + # Password found. + yield offset, passphrase.decode(encoding="ascii") + + def _generator(self): + kernel = self.context.modules[self.config["kernel"]] + mods: Iterable[ObjectInterface] = modules.Modules.list_modules( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + truecrypt_module_base = next( + mod.DllBase + for mod in mods + if mod.BaseDllName.get_string().lower() == "truecrypt.sys" + ) + for offset, password in self.scan_module( + truecrypt_module_base, kernel.layer_name + ): + yield (0, (format_hints.Hex(offset), len(password), password)) + + def run(self) -> renderers.TreeGrid: + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Length", int), + ("Password", str), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/vadinfo.py b/volatility3/framework/plugins/windows/vadinfo.py index 9fa1458d1..abc6142fe 100644 --- a/volatility3/framework/plugins/windows/vadinfo.py +++ b/volatility3/framework/plugins/windows/vadinfo.py @@ -33,7 +33,7 @@ winnt_protections = { class VadInfo(interfaces.plugins.PluginInterface): """Lists process memory ranges.""" - _required_framework_version = (2, 0, 0) + _required_framework_version = (2, 4, 0) _version = (2, 0, 0) MAXSIZE_DEFAULT = 1024 * 1024 * 1024 # 1 Gb @@ -44,33 +44,50 @@ class VadInfo(interfaces.plugins.PluginInterface): @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements - return [requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - # TODO: Convert this to a ListRequirement so that people can filter on sets of ranges - requirements.IntRequirement(name = 'address', - description = "Process virtual memory address to include " \ - "(all other address ranges are excluded). This must be " \ - "a base address, not an address within the desired range.", - optional = True), - requirements.ListRequirement(name = 'pid', - description = 'Filter on specific process IDs', - element_type = int, - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.BooleanRequirement(name = 'dump', - description = "Extract listed memory ranges", - default = False, - optional = True), - requirements.IntRequirement(name = 'maxsize', - description = "Maximum size for dumped VAD sections " \ - "(all the bigger sections will be ignored)", - default = cls.MAXSIZE_DEFAULT, - optional = True), - ] + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + # TODO: Convert this to a ListRequirement so that people can filter on sets of ranges + requirements.IntRequirement( + name="address", + description="Process virtual memory address to include " + "(all other address ranges are excluded).", + optional=True, + ), + requirements.ListRequirement( + name="pid", + description="Filter on specific process IDs", + element_type=int, + optional=True, + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed memory ranges", + default=False, + optional=True, + ), + requirements.IntRequirement( + name="maxsize", + description="Maximum size for dumped VAD sections " + "(all the bigger sections will be ignored)", + default=cls.MAXSIZE_DEFAULT, + optional=True, + ), + ] @classmethod - def protect_values(cls, context: interfaces.context.ContextInterface, layer_name: str, - symbol_table: str) -> Iterable[int]: + def protect_values( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + symbol_table: str, + ) -> Iterable[int]: """Look up the array of memory protection constants from the memory sample. These don't change often, but if they do in the future, then finding them dynamically versus hard-coding here will ensure we parse @@ -83,15 +100,21 @@ class VadInfo(interfaces.plugins.PluginInterface): """ kvo = context.layers[layer_name].config["kernel_virtual_offset"] - ntkrnlmp = context.module(symbol_table, layer_name = layer_name, offset = kvo) + ntkrnlmp = context.module(symbol_table, layer_name=layer_name, offset=kvo) addr = ntkrnlmp.get_symbol("MmProtectToValue").address - values = ntkrnlmp.object(object_type = "array", offset = addr, subtype = ntkrnlmp.get_type("int"), count = 32) + values = ntkrnlmp.object( + object_type="array", offset=addr, subtype=ntkrnlmp.get_type("int"), count=32 + ) return values # type: ignore @classmethod - def list_vads(cls, proc: interfaces.objects.ObjectInterface, - filter_func: Callable[[interfaces.objects.ObjectInterface], bool] = lambda _: False) -> \ - Generator[interfaces.objects.ObjectInterface, None, None]: + def list_vads( + cls, + proc: interfaces.objects.ObjectInterface, + filter_func: Callable[ + [interfaces.objects.ObjectInterface], bool + ] = lambda _: False, + ) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Lists the Virtual Address Descriptors of a specific process. Args: @@ -106,12 +129,14 @@ class VadInfo(interfaces.plugins.PluginInterface): yield vad @classmethod - def vad_dump(cls, - context: interfaces.context.ContextInterface, - proc: interfaces.objects.ObjectInterface, - vad: interfaces.objects.ObjectInterface, - open_method: Type[interfaces.plugins.FileHandlerInterface], - maxsize: int = MAXSIZE_DEFAULT) -> Optional[interfaces.plugins.FileHandlerInterface]: + def vad_dump( + cls, + context: interfaces.context.ContextInterface, + proc: interfaces.objects.ObjectInterface, + vad: interfaces.objects.ObjectInterface, + open_method: Type[interfaces.plugins.FileHandlerInterface], + maxsize: int = MAXSIZE_DEFAULT, + ) -> Optional[interfaces.plugins.FileHandlerInterface]: """Extracts the complete data for Vad as a FileInterface. Args: @@ -132,8 +157,10 @@ class VadInfo(interfaces.plugins.PluginInterface): vollog.debug("Unable to find the starting/ending VPN member") return None - if maxsize > 0 and (vad_end - vad_start) > maxsize: - vollog.debug(f"Skip VAD dump {vad_start:#x}-{vad_end:#x} due to maxsize limit") + if 0 < maxsize < vad.get_size(): + vollog.debug( + f"Skip VAD dump {vad_start:#x}-{vad_end:#x} due to maxsize limit" + ) return None proc_id = "Unknown" @@ -141,8 +168,11 @@ class VadInfo(interfaces.plugins.PluginInterface): proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(proc_id, excp.invalid_address, - excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) return None proc_layer = context.layers[proc_layer_name] @@ -151,9 +181,10 @@ class VadInfo(interfaces.plugins.PluginInterface): file_handle = open_method(file_name) chunk_size = 1024 * 1024 * 10 offset = vad_start - while offset < vad_end: - to_read = min(chunk_size, vad_end - offset) - data = proc_layer.read(offset, to_read, pad = True) + vad_size = vad.get_size() + while offset < vad_start + vad_size: + to_read = min(chunk_size, vad_start + vad_size - offset) + data = proc_layer.read(offset, to_read, pad=True) if not data: break file_handle.write(data) @@ -166,50 +197,85 @@ class VadInfo(interfaces.plugins.PluginInterface): return file_handle def _generator(self, procs): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] + kernel_layer = self.context.layers[kernel.layer_name] def passthrough(_: interfaces.objects.ObjectInterface) -> bool: return False filter_func = passthrough - if self.config.get('address', None) is not None: + if self.config.get("address", None) is not None: def filter_function(x: interfaces.objects.ObjectInterface) -> bool: - return x.get_start() not in [self.config['address']] + return not (x.get_start() <= self.config["address"] <= x.get_end()) filter_func = filter_function for proc in procs: process_name = utility.array_to_string(proc.ImageFileName) - for vad in self.list_vads(proc, filter_func = filter_func): - + for vad in self.list_vads(proc, filter_func=filter_func): file_output = "Disabled" - if self.config['dump']: - file_handle = self.vad_dump(self.context, proc, vad, self.open, self.config['maxsize']) + if self.config["dump"]: + file_handle = self.vad_dump( + self.context, proc, vad, self.open, self.config["maxsize"] + ) file_output = "Error outputting file" if file_handle: file_handle.close() file_output = file_handle.preferred_filename - yield (0, (proc.UniqueProcessId, process_name, format_hints.Hex(vad.vol.offset), - format_hints.Hex(vad.get_start()), format_hints.Hex(vad.get_end()), vad.get_tag(), - vad.get_protection( - self.protect_values(self.context, kernel.layer_name, kernel.symbol_table_name), - winnt_protections), vad.get_commit_charge(), vad.get_private_memory(), - format_hints.Hex(vad.get_parent()), vad.get_file_name(), file_output)) + yield ( + 0, + ( + proc.UniqueProcessId, + process_name, + format_hints.Hex(kernel_layer.canonicalize(vad.vol.offset)), + format_hints.Hex(vad.get_start()), + format_hints.Hex(vad.get_end()), + vad.get_tag(), + vad.get_protection( + self.protect_values( + self.context, + kernel.layer_name, + kernel.symbol_table_name, + ), + winnt_protections, + ), + vad.get_commit_charge(), + vad.get_private_memory(), + format_hints.Hex(vad.get_parent()), + vad.get_file_name(), + file_output, + ), + ) def run(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - 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), ("Offset", format_hints.Hex), - ("Start VPN", format_hints.Hex), ("End VPN", format_hints.Hex), ("Tag", str), - ("Protection", str), ("CommitCharge", int), ("PrivateMemory", int), - ("Parent", format_hints.Hex), ("File", str), ("File output", str)], - self._generator( - pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func))) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("Start VPN", format_hints.Hex), + ("End VPN", format_hints.Hex), + ("Tag", str), + ("Protection", str), + ("CommitCharge", int), + ("PrivateMemory", int), + ("Parent", format_hints.Hex), + ("File", str), + ("File output", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/vadwalk.py b/volatility3/framework/plugins/windows/vadwalk.py new file mode 100644 index 000000000..930388b3a --- /dev/null +++ b/volatility3/framework/plugins/windows/vadwalk.py @@ -0,0 +1,93 @@ +# 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 +from typing import Generator, Iterator, List, Tuple + +from volatility3.framework import interfaces, renderers +from volatility3.framework.configuration import requirements +from volatility3.framework.objects import utility +from volatility3.framework.renderers import format_hints +from volatility3.plugins.windows import pslist, vadinfo + +vollog = logging.getLogger(__name__) + + +class VadWalk(interfaces.plugins.PluginInterface): + """Walk the VAD tree.""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="vadinfo", plugin=vadinfo.VadInfo, 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, procs: Generator[interfaces.objects.ObjectInterface, None, None] + ) -> Iterator[Tuple]: + for proc in procs: + for vad in vadinfo.VadInfo.list_vads(proc): + if vad: + yield ( + 0, + ( + proc.UniqueProcessId, + utility.array_to_string(proc.ImageFileName), + format_hints.Hex(vad.vol.offset), + format_hints.Hex( + vad.get_parent() + & self.context.layers[vad.vol.layer_name].address_mask + ), + format_hints.Hex(vad.get_left_child()), + format_hints.Hex(vad.get_right_child()), + format_hints.Hex(vad.get_start()), + format_hints.Hex(vad.get_end()), + vad.get_tag(), + ), + ) + + def run(self) -> renderers.TreeGrid: + kernel = self.context.modules[self.config["kernel"]] + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) + + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Offset", format_hints.Hex), + ("Parent", format_hints.Hex), + ("Left", format_hints.Hex), + ("Right", format_hints.Hex), + ("Start", format_hints.Hex), + ("End", format_hints.Hex), + ("Tag", str), + ], + self._generator( + pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ) + ), + ) diff --git a/volatility3/framework/plugins/windows/vadyarascan.py b/volatility3/framework/plugins/windows/vadyarascan.py index 06a87d003..d795818e9 100644 --- a/volatility3/framework/plugins/windows/vadyarascan.py +++ b/volatility3/framework/plugins/windows/vadyarascan.py @@ -17,60 +17,73 @@ vollog = logging.getLogger(__name__) class VadYaraScan(interfaces.plugins.PluginInterface): """Scans all the Virtual Address Descriptor memory maps using yara.""" - _required_framework_version = (2, 0, 0) - _version = (1, 0, 0) + _required_framework_version = (2, 4, 0) + _version = (1, 0, 1) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: - return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.BooleanRequirement(name = "wide", - description = "Match wide (unicode) strings", - default = False, - optional = True), - requirements.StringRequirement(name = "yara_rules", - description = "Yara rules (as a string)", - optional = True), - requirements.URIRequirement(name = "yara_file", description = "Yara rules (as a file)", optional = True), - # This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code - # As such, there's a separate option to run compiled files, as happened with yara-3.9 and later - requirements.URIRequirement(name = "yara_compiled_file", - description = "Yara compiled rules (as a file)", - optional = True), - requirements.IntRequirement(name = "max_size", - default = 0x40000000, - description = "Set the maximum size (default is 1GB)", - optional = True), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.VersionRequirement(name = 'yarascanner', component = yarascan.YaraScanner, - version = (2, 0, 0)), - requirements.ListRequirement(name = 'pid', - element_type = int, - description = "Process IDs to include (all other processes are excluded)", - optional = True) + # create a list of requirements for vadyarascan + vadyarascan_requirements = [ + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.VersionRequirement( + name="yarascanner", component=yarascan.YaraScanner, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="yarascan", plugin=yarascan.YaraScan, version=(1, 2, 0) + ), + requirements.ListRequirement( + name="pid", + element_type=int, + description="Process IDs to include (all other processes are excluded)", + optional=True, + ), ] + # get base yarascan requirements for command line options + yarascan_requirements = yarascan.YaraScan.get_yarascan_option_requirements() + + # return the combined requirements + return yarascan_requirements + vadyarascan_requirements + def _generator(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] rules = yarascan.YaraScan.process_yara_options(dict(self.config)) - filter_func = pslist.PsList.create_pid_filter(self.config.get('pid', None)) + filter_func = pslist.PsList.create_pid_filter(self.config.get("pid", None)) - for task in pslist.PsList.list_processes(context = self.context, - layer_name = kernel.layer_name, - symbol_table = kernel.symbol_table_name, - filter_func = filter_func): + for task in pslist.PsList.list_processes( + context=self.context, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + filter_func=filter_func, + ): layer_name = task.add_process_layer() layer = self.context.layers[layer_name] - for offset, rule_name, name, value in layer.scan(context = self.context, - scanner = yarascan.YaraScanner(rules = rules), - sections = self.get_vad_maps(task)): - yield 0, (format_hints.Hex(offset), task.UniqueProcessId, rule_name, name, value) + for offset, rule_name, name, value in layer.scan( + context=self.context, + scanner=yarascan.YaraScanner(rules=rules), + sections=self.get_vad_maps(task), + ): + yield 0, ( + format_hints.Hex(offset), + task.UniqueProcessId, + rule_name, + name, + value, + ) @staticmethod - def get_vad_maps(task: interfaces.objects.ObjectInterface) -> Iterable[Tuple[int, int]]: + def get_vad_maps( + task: interfaces.objects.ObjectInterface, + ) -> Iterable[Tuple[int, int]]: """Creates a map of start/end addresses within a virtual address descriptor tree. @@ -82,10 +95,16 @@ class VadYaraScan(interfaces.plugins.PluginInterface): """ vad_root = task.get_vad_root() for vad in vad_root.traverse(): - end = vad.get_end() - start = vad.get_start() - yield (start, end - start) + yield (vad.get_start(), vad.get_size()) def run(self): - return renderers.TreeGrid([('Offset', format_hints.Hex), ('PID', int), ('Rule', str), ('Component', str), - ('Value', bytes)], self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("PID", int), + ("Rule", str), + ("Component", str), + ("Value", bytes), + ], + self._generator(), + ) diff --git a/volatility3/framework/plugins/windows/verinfo.py b/volatility3/framework/plugins/windows/verinfo.py index 70d473d2e..1c6615804 100644 --- a/volatility3/framework/plugins/windows/verinfo.py +++ b/volatility3/framework/plugins/windows/verinfo.py @@ -20,7 +20,9 @@ vollog = logging.getLogger(__name__) try: import pefile except ImportError: - vollog.info("Python pefile module not found, plugin (and dependent plugins) not available") + vollog.info( + "Python pefile module not found, plugin (and dependent plugins) not available" + ) raise @@ -35,40 +37,65 @@ class VerInfo(interfaces.plugins.PluginInterface): ## TODO: we might add a regex option on the name later, but otherwise we're good ## TODO: and we don't want any CLI options from pslist, modules, or moddump return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]), - requirements.PluginRequirement(name = 'pslist', plugin = pslist.PsList, version = (2, 0, 0)), - requirements.PluginRequirement(name = 'modules', plugin = modules.Modules, version = (1, 0, 0)), - requirements.VersionRequirement(name = 'dlllist', component = dlllist.DllList, version = (2, 0, 0)), - requirements.BooleanRequirement(name = "extensive", - description = "Search physical layer for version information", - optional = True, - default = False), + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="pslist", plugin=pslist.PsList, version=(2, 0, 0) + ), + requirements.PluginRequirement( + name="modules", plugin=modules.Modules, version=(1, 0, 0) + ), + requirements.VersionRequirement( + name="dlllist", component=dlllist.DllList, version=(2, 0, 0) + ), + requirements.BooleanRequirement( + name="extensive", + description="Search physical layer for version information", + optional=True, + default=False, + ), ] @classmethod - def find_version_info(cls, context: interfaces.context.ContextInterface, layer_name: str, - filename: str) -> Optional[Tuple[int, int, int, int]]: + def find_version_info( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + filename: str, + ) -> Optional[Tuple[int, int, int, int]]: """Searches for an original filename, then tracks back to find the VS_VERSION_INFO and read the fixed version information structure""" premable_max_distance = 0x500 filename = "OriginalFilename\x00" + filename - iterator = context.layers[layer_name].scan(context = context, - scanner = scanners.BytesScanner(bytes(filename, 'utf-16be'))) + iterator = context.layers[layer_name].scan( + context=context, scanner=scanners.BytesScanner(bytes(filename, "utf-16be")) + ) for offset in iterator: - data = context.layers[layer_name].read(offset - premable_max_distance, premable_max_distance) + data = context.layers[layer_name].read( + offset - premable_max_distance, premable_max_distance + ) vs_ver_info = b"\xbd\x04\xef\xfe" verinfo_offset = data.find(vs_ver_info) + len(vs_ver_info) if verinfo_offset >= 0: - structure = ' Tuple[int, int, int, int]: + def get_version_information( + cls, + context: interfaces.context.ContextInterface, + pe_table_name: str, + layer_name: str, + base_address: int, + ) -> Tuple[int, int, int, int]: """Get File and Product version information from PE files. Args: @@ -83,16 +110,20 @@ class VerInfo(interfaces.plugins.PluginInterface): pe_data = io.BytesIO() - dos_header = context.object(pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", - offset = base_address, - layer_name = layer_name) + dos_header = context.object( + pe_table_name + constants.BANG + "_IMAGE_DOS_HEADER", + offset=base_address, + layer_name=layer_name, + ) for offset, data in dos_header.reconstruct(): pe_data.seek(offset) pe_data.write(data) - pe = pefile.PE(data = pe_data.getvalue(), fast_load = True) - pe.parse_data_directories([pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]) + pe = pefile.PE(data=pe_data.getvalue(), fast_load=True) + pe.parse_data_directories( + [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]] + ) if isinstance(pe.VS_FIXEDFILEINFO, list): # pefile >= 2018.8.8 (estimated) @@ -110,9 +141,12 @@ class VerInfo(interfaces.plugins.PluginInterface): return major, minor, product, build - def _generator(self, procs: Generator[interfaces.objects.ObjectInterface, None, None], - mods: Generator[interfaces.objects.ObjectInterface, None, None], session_layers: Generator[str, None, - None]): + def _generator( + self, + procs: Generator[interfaces.objects.ObjectInterface, None, None], + mods: Generator[interfaces.objects.ObjectInterface, None, None], + session_layers: Generator[str, None, None], + ): """Generates a list of PE file version info for processes, dlls, and modules. @@ -121,16 +155,16 @@ class VerInfo(interfaces.plugins.PluginInterface): mods: of modules session_layers: of layers in the session to be checked """ - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - pe_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "windows", - "pe", - class_types = pe.class_types) + pe_table_name = intermed.IntermediateSymbolTable.create( + self.context, self.config_path, "windows", "pe", class_types=pe.class_types + ) # TODO: Fix this so it works with more than just intel layers - physical_layer_name = self.context.layers[kernel.layer_name].config.get('memory_layer', None) + physical_layer_name = self.context.layers[kernel.layer_name].config.get( + "memory_layer", None + ) for mod in mods: try: @@ -138,21 +172,40 @@ class VerInfo(interfaces.plugins.PluginInterface): except exceptions.InvalidAddressException: BaseDllName = renderers.UnreadableValue() - session_layer_name = modules.Modules.find_session_layer(self.context, session_layers, mod.DllBase) + session_layer_name = modules.Modules.find_session_layer( + self.context, session_layers, mod.DllBase + ) try: - (major, minor, product, build) = self.get_version_information(self._context, pe_table_name, - session_layer_name, mod.DllBase) + (major, minor, product, build) = self.get_version_information( + self._context, pe_table_name, session_layer_name, mod.DllBase + ) except (exceptions.InvalidAddressException, TypeError, AttributeError): (major, minor, product, build) = [renderers.UnreadableValue()] * 4 - if (not isinstance(BaseDllName, renderers.UnreadableValue) and physical_layer_name is not None - and self.config['extensive']): - result = self.find_version_info(self._context, physical_layer_name, BaseDllName) + if ( + not isinstance(BaseDllName, renderers.UnreadableValue) + and physical_layer_name is not None + and self.config["extensive"] + ): + result = self.find_version_info( + self._context, physical_layer_name, BaseDllName + ) if result is not None: (major, minor, product, build) = result # the pid and process are not applicable for kernel modules - yield (0, (renderers.NotApplicableValue(), renderers.NotApplicableValue(), format_hints.Hex(mod.DllBase), - BaseDllName, major, minor, product, build)) + yield ( + 0, + ( + renderers.NotApplicableValue(), + renderers.NotApplicableValue(), + format_hints.Hex(mod.DllBase), + BaseDllName, + major, + minor, + product, + build, + ), + ) # now go through the process and dll lists for proc in procs: @@ -161,12 +214,14 @@ class VerInfo(interfaces.plugins.PluginInterface): proc_id = proc.UniqueProcessId proc_layer_name = proc.add_process_layer() except exceptions.InvalidAddressException as excp: - vollog.debug("Process {}: invalid address {} in layer {}".format(proc_id, excp.invalid_address, - excp.layer_name)) + vollog.debug( + "Process {}: invalid address {} in layer {}".format( + proc_id, excp.invalid_address, excp.layer_name + ) + ) continue for entry in proc.load_order_modules(): - try: BaseDllName = entry.BaseDllName.get_string() except exceptions.InvalidAddressException: @@ -178,28 +233,56 @@ class VerInfo(interfaces.plugins.PluginInterface): DllBase = renderers.UnreadableValue() try: - (major, minor, product, build) = self.get_version_information(self._context, pe_table_name, - proc_layer_name, entry.DllBase) + (major, minor, product, build) = self.get_version_information( + self._context, pe_table_name, proc_layer_name, entry.DllBase + ) except (exceptions.InvalidAddressException, ValueError, AttributeError): (major, minor, product, build) = [renderers.UnreadableValue()] * 4 - yield (0, (proc.UniqueProcessId, - proc.ImageFileName.cast("string", - max_length = proc.ImageFileName.vol.count, - errors = "replace"), DllBase, BaseDllName, major, minor, product, - build)) + yield ( + 0, + ( + proc.UniqueProcessId, + proc.ImageFileName.cast( + "string", + max_length=proc.ImageFileName.vol.count, + errors="replace", + ), + DllBase, + BaseDllName, + major, + minor, + product, + build, + ), + ) def run(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] - procs = pslist.PsList.list_processes(self.context, kernel.layer_name, kernel.symbol_table_name) + procs = pslist.PsList.list_processes( + self.context, kernel.layer_name, kernel.symbol_table_name + ) - mods = modules.Modules.list_modules(self.context, kernel.layer_name, kernel.symbol_table_name) + mods = modules.Modules.list_modules( + self.context, kernel.layer_name, kernel.symbol_table_name + ) # populate the session layers for kernel modules - session_layers = modules.Modules.get_session_layers(self.context, kernel.layer_name, kernel.symbol_table_name) - - return renderers.TreeGrid([("PID", int), ("Process", str), ("Base", format_hints.Hex), ("Name", str), - ("Major", int), ("Minor", int), ("Product", int), ("Build", int)], - self._generator(procs, mods, session_layers)) + session_layers = modules.Modules.get_session_layers( + self.context, kernel.layer_name, kernel.symbol_table_name + ) + return renderers.TreeGrid( + [ + ("PID", int), + ("Process", str), + ("Base", format_hints.Hex), + ("Name", str), + ("Major", int), + ("Minor", int), + ("Product", int), + ("Build", int), + ], + self._generator(procs, mods, session_layers), + ) diff --git a/volatility3/framework/plugins/windows/virtmap.py b/volatility3/framework/plugins/windows/virtmap.py index 9a241d3d8..5190bec8d 100644 --- a/volatility3/framework/plugins/windows/virtmap.py +++ b/volatility3/framework/plugins/windows/virtmap.py @@ -22,68 +22,98 @@ class VirtMap(interfaces.plugins.PluginInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: # Since we're calling the plugin, make sure we have the plugin's requirements return [ - requirements.ModuleRequirement(name = 'kernel', description = 'Windows kernel', - architectures = ["Intel32", "Intel64"]) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ) ] def _generator(self, map): for entry in sorted(map): - for (start, end) in map[entry]: + for start, end in map[entry]: yield (0, (entry, format_hints.Hex(start), format_hints.Hex(end))) @classmethod - def determine_map(cls, module: interfaces.context.ModuleInterface) -> \ - Dict[str, List[Tuple[int, int]]]: + def determine_map( + cls, module: interfaces.context.ModuleInterface + ) -> Dict[str, List[Tuple[int, int]]]: """Returns the virtual map from a windows kernel module.""" layer = module.context.layers[module.layer_name] if not isinstance(layer, intel.Intel): raise result: Dict[str, List[Tuple[int, int]]] = {} - system_va_type = module.get_enumeration('_MI_SYSTEM_VA_TYPE') - large_page_size = (layer.page_size ** 2) // module.get_type("_MMPTE").size + system_va_type = module.get_enumeration("_MI_SYSTEM_VA_TYPE") + large_page_size = (layer.page_size**2) // module.get_type("_MMPTE").size - if module.has_symbol('MiVisibleState'): - symbol = module.get_symbol('MiVisibleState') - visible_state = module.object(object_type = 'pointer', - offset = symbol.address, - subtype = module.get_type('_MI_VISIBLE_STATE')).dereference() - if hasattr(visible_state, 'SystemVaRegions'): + if module.has_symbol("MiVisibleState"): + symbol = module.get_symbol("MiVisibleState") + visible_state = module.object( + object_type="pointer", + offset=symbol.address, + subtype=module.get_type("_MI_VISIBLE_STATE"), + ).dereference() + if hasattr(visible_state, "SystemVaRegions"): for i in range(visible_state.SystemVaRegions.count): lookup = system_va_type.lookup(i) region_range = result.get(lookup, []) region_range.append( - (visible_state.SystemVaRegions[i].BaseAddress, visible_state.SystemVaRegions[i].NumberOfBytes)) + ( + visible_state.SystemVaRegions[i].BaseAddress, + visible_state.SystemVaRegions[i].NumberOfBytes, + ) + ) result[lookup] = region_range - elif hasattr(visible_state, 'SystemVaType'): - system_range_start = module.object(object_type = "pointer", - offset = module.get_symbol("MmSystemRangeStart").address) - result = cls._enumerate_system_va_type(large_page_size, system_range_start, module, - visible_state.SystemVaType) + elif hasattr(visible_state, "SystemVaType"): + system_range_start = module.object( + object_type="pointer", + offset=module.get_symbol("MmSystemRangeStart").address, + ) + result = cls._enumerate_system_va_type( + large_page_size, + system_range_start, + module, + visible_state.SystemVaType, + ) else: - raise exceptions.SymbolError(None, module.name, "Required structures not found") - elif module.has_symbol('MiSystemVaType'): - system_range_start = module.object(object_type = "pointer", - offset = module.get_symbol("MmSystemRangeStart").address) - symbol = module.get_symbol('MiSystemVaType') + raise exceptions.SymbolError( + None, module.name, "Required structures not found" + ) + elif module.has_symbol("MiSystemVaType"): + system_range_start = module.object( + object_type="pointer", + offset=module.get_symbol("MmSystemRangeStart").address, + ) + symbol = module.get_symbol("MiSystemVaType") array_count = (0xFFFFFFFF + 1 - system_range_start) // large_page_size - type_array = module.object(object_type = 'array', - offset = symbol.address, - count = array_count, - subtype = module.get_type('char')) + type_array = module.object( + object_type="array", + offset=symbol.address, + count=array_count, + subtype=module.get_type("char"), + ) - result = cls._enumerate_system_va_type(large_page_size, system_range_start, module, type_array) + result = cls._enumerate_system_va_type( + large_page_size, system_range_start, module, type_array + ) else: - raise exceptions.SymbolError(None, module.name, "Required structures not found") + raise exceptions.SymbolError( + None, module.name, "Required structures not found" + ) return result @classmethod - def _enumerate_system_va_type(cls, large_page_size: int, system_range_start: int, - module: interfaces.context.ModuleInterface, - type_array: interfaces.objects.ObjectInterface) -> Dict[str, List[Tuple[int, int]]]: + def _enumerate_system_va_type( + cls, + large_page_size: int, + system_range_start: int, + module: interfaces.context.ModuleInterface, + type_array: interfaces.objects.ObjectInterface, + ) -> Dict[str, List[Tuple[int, int]]]: result: Dict[str, List[Tuple[int, int]]] = {} - system_va_type = module.get_enumeration('_MI_SYSTEM_VA_TYPE') + system_va_type = module.get_enumeration("_MI_SYSTEM_VA_TYPE") start = system_range_start prev_entry = -1 cur_size = large_page_size @@ -102,21 +132,30 @@ class VirtMap(interfaces.plugins.PluginInterface): return result @classmethod - def scannable_sections(cls, module: interfaces.context.ModuleInterface) -> Generator[Tuple[int, int], None, None]: + def scannable_sections( + cls, module: interfaces.context.ModuleInterface + ) -> Generator[Tuple[int, int], None, None]: mapping = cls.determine_map(module) for entry in mapping: - if 'Unused' not in entry: + if "Unused" not in entry: for value in mapping[entry]: yield value def run(self): - kernel = self.context.modules[self.config['kernel']] + kernel = self.context.modules[self.config["kernel"]] layer = self.context.layers[kernel.layer_name] - module = self.context.module(kernel.symbol_table_name, - layer_name = layer.name, - offset = layer.config['kernel_virtual_offset']) + module = self.context.module( + kernel.symbol_table_name, + layer_name=layer.name, + offset=layer.config["kernel_virtual_offset"], + ) - return renderers.TreeGrid([("Region", str), ("Start offset", format_hints.Hex), - ("End offset", format_hints.Hex)], - self._generator(self.determine_map(module = module))) + return renderers.TreeGrid( + [ + ("Region", str), + ("Start offset", format_hints.Hex), + ("End offset", format_hints.Hex), + ], + self._generator(self.determine_map(module=module)), + ) diff --git a/volatility3/framework/plugins/yarascan.py b/volatility3/framework/plugins/yarascan.py index 0ef55ff4b..11c708607 100644 --- a/volatility3/framework/plugins/yarascan.py +++ b/volatility3/framework/plugins/yarascan.py @@ -16,10 +16,12 @@ vollog = logging.getLogger(__name__) try: import yara - if tuple([int(x) for x in yara.__version__.split('.')]) < (3, 8): + if tuple([int(x) for x in yara.__version__.split(".")]) < (3, 8): raise ImportError except ImportError: - vollog.info("Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available") + vollog.info( + "Python Yara (>3.8.0) module not found, plugin (and dependent plugins) not available" + ) raise @@ -32,69 +34,117 @@ class YaraScanner(interfaces.layers.ScannerInterface): if rules is None: raise ValueError("No rules provided to YaraScanner") self._rules = rules + self.st_object = not tuple([int(x) for x in yara.__version__.split(".")]) < ( + 4, + 3, + ) - def __call__(self, data: bytes, data_offset: int) -> Iterable[Tuple[int, str, str, bytes]]: - for match in self._rules.match(data = data): - for offset, name, value in match.strings: - yield (offset + data_offset, match.rule, name, value) + def __call__( + self, data: bytes, data_offset: int + ) -> Iterable[Tuple[int, str, str, bytes]]: + for match in self._rules.match(data=data): + if self.st_object: + for match_string in match.strings: + for instance in match_string.instances: + yield ( + instance.offset + data_offset, + match.rule, + match_string.identifier, + instance.matched_data, + ) + else: + for offset, name, value in match.strings: + yield (offset + data_offset, match.rule, name, value) class YaraScan(plugins.PluginInterface): """Scans kernel memory using yara rules (string or file).""" _required_framework_version = (2, 0, 0) - _version = (1, 1, 0) + _version = (1, 2, 0) # TODO: When the major version is bumped, take the opportunity to rename the yara_rules config to yara_string # or something that makes more sense @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + """Returns the requirements needed to run yarascan directly, combining the TranslationLayerRequirement + and the requirements from get_yarascan_option_requirements.""" + return cls.get_yarascan_option_requirements() + [ + requirements.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ) + ] + + @classmethod + def get_yarascan_option_requirements( + cls, + ) -> List[interfaces.configuration.RequirementInterface]: + """Returns the requirements needed for the command lines options used by yarascan. This can + then also be used by other plugins that are using yarascan. This does not include a + TranslationLayerRequirement or a ModuleRequirement.""" return [ - requirements.TranslationLayerRequirement(name = 'primary', - description = "Memory layer for the kernel", - architectures = ["Intel32", "Intel64"]), - requirements.BooleanRequirement(name = "insensitive", - description = "Makes the search case insensitive", - default = False, - optional = True), - requirements.BooleanRequirement(name = "wide", - description = "Match wide (unicode) strings", - default = False, - optional = True), - requirements.StringRequirement(name = "yara_rules", - description = "Yara rules (as a string)", - optional = True), - requirements.URIRequirement(name = "yara_file", description = "Yara rules (as a file)", optional = True), + requirements.BooleanRequirement( + name="insensitive", + description="Makes the search case insensitive", + default=False, + optional=True, + ), + requirements.BooleanRequirement( + name="wide", + description="Match wide (unicode) strings", + default=False, + optional=True, + ), + requirements.StringRequirement( + name="yara_rules", description="Yara rules (as a string)", optional=True + ), + requirements.URIRequirement( + name="yara_file", description="Yara rules (as a file)", optional=True + ), # This additional requirement is to follow suit with upstream, who feel that compiled rules could potentially be used to execute malicious code # As such, there's a separate option to run compiled files, as happened with yara-3.9 and later - requirements.URIRequirement(name = "yara_compiled_file", - description = "Yara compiled rules (as a file)", - optional = True), - requirements.IntRequirement(name = "max_size", - default = 0x40000000, - description = "Set the maximum size (default is 1GB)", - optional = True) + requirements.URIRequirement( + name="yara_compiled_file", + description="Yara compiled rules (as a file)", + optional=True, + ), + requirements.IntRequirement( + name="max_size", + default=0x40000000, + description="Set the maximum size (default is 1GB)", + optional=True, + ), ] @classmethod def process_yara_options(cls, config: Dict[str, Any]): rules = None - if config.get('yara_rules', None) is not None: - rule = config['yara_rules'] + if config.get("yara_rules", None) is not None: + rule = config["yara_rules"] if rule[0] not in ["{", "/"]: rule = f'"{rule}"' - if config.get('case', False): + if config.get("case", False): rule += " nocase" - if config.get('wide', False): + if config.get("wide", False): rule += " wide ascii" - rules = yara.compile(sources = {'n': f'rule r1 {{strings: $a = {rule} condition: $a}}'}) - elif config.get('yara_source', None) is not None: - rules = yara.compile(source = config['yara_source']) - elif config.get('yara_file', None) is not None: - rules = yara.compile(file = resources.ResourceAccessor().open(config['yara_file'], "rb")) - elif config.get('yara_compiled_file', None) is not None: - rules = yara.load(file = resources.ResourceAccessor().open(config['yara_compiled_file'], "rb")) + rules = yara.compile( + sources={"n": f"rule r1 {{strings: $a = {rule} condition: $a}}"} + ) + elif config.get("yara_source", None) is not None: + rules = yara.compile(source=config["yara_source"]) + elif config.get("yara_file", None) is not None: + rules = yara.compile( + file=resources.ResourceAccessor().open(config["yara_file"], "rb") + ) + elif config.get("yara_compiled_file", None) is not None: + rules = yara.load( + file=resources.ResourceAccessor().open( + config["yara_compiled_file"], "rb" + ) + ) else: vollog.error("No yara rules, nor yara rules file were specified") return rules @@ -102,10 +152,19 @@ class YaraScan(plugins.PluginInterface): def _generator(self): rules = self.process_yara_options(dict(self.config)) - layer = self.context.layers[self.config['primary']] - for offset, rule_name, name, value in layer.scan(context = self.context, scanner = YaraScanner(rules = rules)): + layer = self.context.layers[self.config["primary"]] + for offset, rule_name, name, value in layer.scan( + context=self.context, scanner=YaraScanner(rules=rules) + ): yield 0, (format_hints.Hex(offset), rule_name, name, value) def run(self): - return renderers.TreeGrid([('Offset', format_hints.Hex), ('Rule', str), ('Component', str), ('Value', bytes)], - self._generator()) + return renderers.TreeGrid( + [ + ("Offset", format_hints.Hex), + ("Rule", str), + ("Component", str), + ("Value", bytes), + ], + self._generator(), + ) diff --git a/volatility3/framework/renderers/__init__.py b/volatility3/framework/renderers/__init__.py index 5773861d9..43bb59a21 100644 --- a/volatility3/framework/renderers/__init__.py +++ b/volatility3/framework/renderers/__init__.py @@ -10,7 +10,7 @@ import collections import collections.abc import datetime import logging -from typing import Any, Callable, Iterable, List, Optional, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, TypeVar, Union from volatility3.framework import interfaces from volatility3.framework.interfaces import renderers @@ -49,8 +49,13 @@ class NotAvailableValue(interfaces.renderers.BaseAbsentValue): class TreeNode(interfaces.renderers.TreeNode): """Class representing a particular node in a tree grid.""" - def __init__(self, path: str, treegrid: 'TreeGrid', parent: Optional[interfaces.renderers.TreeNode], - values: List[interfaces.renderers.BaseTypes]) -> None: + def __init__( + self, + path: str, + treegrid: "TreeGrid", + parent: Optional[interfaces.renderers.TreeNode], + values: List[interfaces.renderers.BaseTypes], + ) -> None: if not isinstance(treegrid, TreeGrid): raise TypeError("Treegrid must be an instance of TreeGrid") self._treegrid = treegrid @@ -71,20 +76,30 @@ class TreeNode(interfaces.renderers.TreeNode): def _validate_values(self, values: List[interfaces.renderers.BaseTypes]) -> None: """A function for raising exceptions if a given set of values is invalid according to the column properties.""" - if not (isinstance(values, collections.abc.Sequence) and len(values) == len(self._treegrid.columns)): + if not ( + isinstance(values, collections.abc.Sequence) + and len(values) == len(self._treegrid.columns) + ): raise TypeError( - "Values must be a list of objects made up of simple types and number the same as the columns") + "Values must be a list of objects made up of simple types and number the same as the columns" + ) for index in range(len(self._treegrid.columns)): column = self._treegrid.columns[index] val = values[index] if not isinstance(val, (column.type, interfaces.renderers.BaseAbsentValue)): raise TypeError( "Values item with index {} is the wrong type for column {} (got {} but expected {})".format( - index, column.name, type(val), column.type)) + index, column.name, type(val), column.type + ) + ) # TODO: Consider how to deal with timezone naive/aware datetimes (and alert plugin uses to be precise) # if isinstance(val, datetime.datetime): # tznaive = val.tzinfo is None or val.tzinfo.utcoffset(val) is None + def asdict(self) -> Dict[str, Any]: + """Returns the contents of the node as a dictionary""" + return self._values._asdict() + @property def values(self) -> List[interfaces.renderers.BaseTypes]: """Returns the list of values from the particular node, based on column @@ -122,12 +137,16 @@ class TreeNode(interfaces.renderers.TreeNode): changed = path.split(TreeGrid.path_sep) changed_index = len(changed) - 1 if int(components[changed_index]) >= int(changed[-1]): - components[changed_index] = str(int(components[changed_index]) + (1 if added else -1)) + components[changed_index] = str( + int(components[changed_index]) + (1 if added else -1) + ) self._path = TreeGrid.path_sep.join(components) def RowStructureConstructor(names: List[str]): - return collections.namedtuple("RowStructure", [TreeGrid.sanitize_name(name) for name in names]) + return collections.namedtuple( + "RowStructure", [TreeGrid.sanitize_name(name) for name in names] + ) class TreeGrid(interfaces.renderers.TreeGrid): @@ -145,8 +164,11 @@ class TreeGrid(interfaces.renderers.TreeGrid): path_sep = "|" - def __init__(self, columns: List[Tuple[str, interfaces.renderers.BaseTypes]], - generator: Optional[Iterable[Tuple[int, Tuple]]]) -> None: + def __init__( + self, + columns: List[Tuple[str, interfaces.renderers.BaseTypes]], + generator: Optional[Iterable[Tuple[int, Tuple]]], + ) -> None: """Constructs a TreeGrid object using a specific set of columns. The TreeGrid itself is a root element, that can have children but no values. @@ -163,13 +185,18 @@ class TreeGrid(interfaces.renderers.TreeGrid): converted_columns: List[interfaces.renderers.Column] = [] if len(columns) < 1: raise ValueError("Columns must be a list containing at least one column") - for (name, column_type) in columns: + for name, column_type in columns: is_simple_type = issubclass(column_type, self.base_types) if not is_simple_type: - raise TypeError("Column {}'s type is not a simple type: {}".format(name, - column_type.__class__.__name__)) + raise TypeError( + "Column {}'s type is not a simple type: {}".format( + name, column_type.__class__.__name__ + ) + ) converted_columns.append(interfaces.renderers.Column(name, column_type)) - self.RowStructure = RowStructureConstructor([column.name for column in converted_columns]) + self.RowStructure = RowStructureConstructor( + [column.name for column in converted_columns] + ) self._columns = converted_columns if generator is None: generator = [] @@ -181,14 +208,20 @@ class TreeGrid(interfaces.renderers.TreeGrid): def sanitize_name(text: str) -> str: output = "" for letter in text.lower(): - if letter != ' ': - output += (letter if letter in 'abcdefghiljklmnopqrstuvwxyz_0123456789' else '_') + if letter != " ": + output += ( + letter + if letter in "abcdefghiljklmnopqrstuvwxyz_0123456789" + else "_" + ) return output - def populate(self, - function: interfaces.renderers.VisitorSignature = None, - initial_accumulator: Any = None, - fail_on_errors: bool = True) -> Optional[Exception]: + def populate( + self, + function: interfaces.renderers.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. @@ -209,7 +242,7 @@ class TreeGrid(interfaces.renderers.TreeGrid): if not self.populated: try: prev_nodes: List[interfaces.renderers.TreeNode] = [] - for (level, item) in self._generator: + for level, item in self._generator: parent_index = min(len(prev_nodes), level) parent = prev_nodes[parent_index - 1] if parent_index > 0 else None treenode = self._append(parent, item) @@ -242,7 +275,9 @@ class TreeGrid(interfaces.renderers.TreeGrid): """Returns the number of rows populated.""" return self._row_count - def children(self, node: Optional[interfaces.renderers.TreeNode]) -> List[interfaces.renderers.TreeNode]: + def children( + self, node: Optional[interfaces.renderers.TreeNode] + ) -> List[interfaces.renderers.TreeNode]: """Returns the subnodes of a particular node in order.""" return [node for node, _ in self._find_children(node)] @@ -269,12 +304,19 @@ class TreeGrid(interfaces.renderers.TreeGrid): raise TypeError("Node must be a valid node within the TreeGrid") return node.values - def _append(self, parent: Optional[interfaces.renderers.TreeNode], values: Any) -> TreeNode: + def _append( + self, parent: Optional[interfaces.renderers.TreeNode], values: Any + ) -> TreeNode: """Adds a new node at the top level if parent is None, or under the parent node otherwise, after all other children.""" return self._insert(parent, None, values) - def _insert(self, parent: Optional[interfaces.renderers.TreeNode], position: Optional[int], values: Any) -> TreeNode: + def _insert( + self, + parent: Optional[interfaces.renderers.TreeNode], + position: Optional[int], + values: Any, + ) -> TreeNode: """Inserts an element into the tree at a specific position.""" parent_path = "" children = self._find_children(parent) @@ -285,7 +327,9 @@ class TreeGrid(interfaces.renderers.TreeGrid): else: newpath = parent_path + str(position) for node, _ in children[position:]: - self.visit(node, lambda child, _: child.path_changed(newpath, True), None) + self.visit( + node, lambda child, _: child.path_changed(newpath, True), None + ) tree_item = TreeNode(newpath, self, parent, values) if position is None: @@ -304,11 +348,13 @@ class TreeGrid(interfaces.renderers.TreeGrid): _T = TypeVar("_T") - def visit(self, - node: Optional[interfaces.renderers.TreeNode], - function: Callable[[interfaces.renderers.TreeNode, _T], _T], - initial_accumulator: _T, - sort_key: Optional[interfaces.renderers.ColumnSortKey] = None): + def visit( + self, + node: Optional[interfaces.renderers.TreeNode], + function: Callable[[interfaces.renderers.TreeNode, _T], _T], + initial_accumulator: _T, + sort_key: Optional[interfaces.renderers.ColumnSortKey] = 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 @@ -334,24 +380,30 @@ class TreeGrid(interfaces.renderers.TreeGrid): if children is not None: if sort_key is not None: sort_key_not_none = sort_key # Only necessary because of mypy - children = sorted(children, key = lambda x: sort_key_not_none(x[0].values)) + children = sorted( + children, key=lambda x: sort_key_not_none(x[0].values) + ) if not sort_key.ascending: children = reversed(children) accumulator = self._visit(children, function, accumulator, sort_key) return accumulator - def _visit(self, - list_of_children: List[interfaces.renderers.TreeNode], - function: Callable, - accumulator: _T, - sort_key: Optional[interfaces.renderers.ColumnSortKey] = None) -> _T: + def _visit( + self, + list_of_children: List[interfaces.renderers.TreeNode], + function: Callable, + accumulator: _T, + sort_key: Optional[interfaces.renderers.ColumnSortKey] = None, + ) -> _T: """Visits all the nodes in a tree, calling function on each one.""" if list_of_children is not None: for n, children in list_of_children: accumulator = function(n, accumulator) if sort_key is not None: sort_key_not_none = sort_key # Only necessary because of mypy - children = sorted(children, key = lambda x: sort_key_not_none(x[0].values)) + children = sorted( + children, key=lambda x: sort_key_not_none(x[0].values) + ) if not sort_key.ascending: children = reversed(children) accumulator = self._visit(children, function, accumulator, sort_key) @@ -359,8 +411,9 @@ class TreeGrid(interfaces.renderers.TreeGrid): class ColumnSortKey(interfaces.renderers.ColumnSortKey): - - def __init__(self, treegrid: TreeGrid, column_name: str, ascending: bool = True) -> None: + def __init__( + self, treegrid: TreeGrid, column_name: str, ascending: bool = True + ) -> None: _index = None self._type = None self.ascending = ascending diff --git a/volatility3/framework/renderers/conversion.py b/volatility3/framework/renderers/conversion.py index 996cf03a5..bb18fcc8a 100644 --- a/volatility3/framework/renderers/conversion.py +++ b/volatility3/framework/renderers/conversion.py @@ -1,7 +1,7 @@ # 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 datetime import ipaddress import socket @@ -11,7 +11,9 @@ from typing import Union from volatility3.framework import interfaces, renderers -def wintime_to_datetime(wintime: int) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: +def wintime_to_datetime( + wintime: int, +) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: unix_time = wintime // 10000000 if unix_time == 0: return renderers.NotApplicableValue() @@ -23,14 +25,16 @@ def wintime_to_datetime(wintime: int) -> Union[interfaces.renderers.BaseAbsentVa return renderers.UnparsableValue() -def unixtime_to_datetime(unixtime: int) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: - ret: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] = renderers.UnparsableValue() +def unixtime_to_datetime( + unixtime: int, +) -> Union[interfaces.renderers.BaseAbsentValue, datetime.datetime]: + ret: Union[interfaces.renderers.BaseAbsentValue, datetime.datetime] = ( + renderers.UnparsableValue() + ) if unixtime > 0: - try: + with contextlib.suppress(ValueError): ret = datetime.datetime.utcfromtimestamp(unixtime) - except ValueError: - pass return ret @@ -51,8 +55,8 @@ def round(addr: int, align: int, up: bool = False) -> int: return addr else: if up: - return (addr + (align - (addr % align))) - return (addr - (addr % align)) + return addr + (align - (addr % align)) + return addr - (addr % align) # For vol3 devs: @@ -88,7 +92,7 @@ def convert_ipv6(packed_ip): def convert_port(port_as_integer): - return (port_as_integer >> 8) | ((port_as_integer & 0xff) << 8) + return (port_as_integer >> 8) | ((port_as_integer & 0xFF) << 8) def convert_network_four_tuple(family, four_tuple): @@ -100,11 +104,19 @@ def convert_network_four_tuple(family, four_tuple): """ if family == socket.AF_INET: - ret = (convert_ipv4(four_tuple[0]), convert_port(four_tuple[1]), convert_ipv4(four_tuple[2]), - convert_port(four_tuple[3])) + ret = ( + convert_ipv4(four_tuple[0]), + convert_port(four_tuple[1]), + convert_ipv4(four_tuple[2]), + convert_port(four_tuple[3]), + ) elif family == socket.AF_INET6: - ret = (convert_ipv6(four_tuple[0]), convert_port(four_tuple[1]), convert_ipv6(four_tuple[2]), - convert_port(four_tuple[3])) + ret = ( + convert_ipv6(four_tuple[0]), + convert_port(four_tuple[1]), + convert_ipv6(four_tuple[2]), + convert_port(four_tuple[3]), + ) else: ret = None diff --git a/volatility3/framework/renderers/format_hints.py b/volatility3/framework/renderers/format_hints.py index f386d8e9d..6120b77c9 100644 --- a/volatility3/framework/renderers/format_hints.py +++ b/volatility3/framework/renderers/format_hints.py @@ -29,23 +29,26 @@ class HexBytes(bytes): class MultiTypeData(bytes): """The contents are supposed to be a string, but may contain binary data.""" - def __new__(cls: Type['MultiTypeData'], - original: Union[int, bytes], - encoding: str = 'utf-16-le', - split_nulls: bool = False, - show_hex: bool = False) -> 'MultiTypeData': - + def __new__( + cls: Type["MultiTypeData"], + original: Union[int, bytes], + encoding: str = "utf-16-le", + split_nulls: bool = False, + show_hex: bool = False, + ) -> "MultiTypeData": if isinstance(original, int): data = str(original).encode(encoding) else: data = original return super().__new__(cls, data) - def __init__(self, - original: bytes, - encoding: str = 'utf-16-le', - split_nulls: bool = False, - show_hex: bool = False) -> None: + def __init__( + self, + original: bytes, + encoding: str = "utf-16-le", + split_nulls: bool = False, + show_hex: bool = False, + ) -> None: self.converted_int: bool = False if isinstance(original, int): self.converted_int = True @@ -55,8 +58,11 @@ class MultiTypeData(bytes): bytes.__init__(original) def __eq__(self, other): - return super(self) == super(other) and \ - self.converted_int == other.converted_int and \ - self.encoding == other.encoding and \ - self.split_nulls == other.split_nulls and \ - self.show_hex == other.show_hex + return ( + isinstance(other, self.__class__) + and super() == super(self.__class__, other) + and self.converted_int == other.converted_int + and self.encoding == other.encoding + and self.split_nulls == other.split_nulls + and self.show_hex == other.show_hex + ) diff --git a/volatility3/framework/symbols/__init__.py b/volatility3/framework/symbols/__init__.py index d6ecb5393..d1e7a104d 100644 --- a/volatility3/framework/symbols/__init__.py +++ b/volatility3/framework/symbols/__init__.py @@ -12,8 +12,12 @@ from volatility3.framework import constants, exceptions, interfaces, objects vollog = logging.getLogger(__name__) -SymbolSpaceReturnType = TypeVar("SymbolSpaceReturnType", interfaces.objects.Template, - interfaces.symbols.SymbolInterface, Dict[str, Any]) +SymbolSpaceReturnType = TypeVar( + "SymbolSpaceReturnType", + interfaces.objects.Template, + interfaces.symbols.SymbolInterface, + Dict[str, Any], +) class SymbolType(enum.Enum): @@ -31,7 +35,9 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): def __init__(self) -> None: super().__init__() - self._dict: Dict[str, interfaces.symbols.BaseSymbolTableInterface] = collections.OrderedDict() + self._dict: Dict[str, interfaces.symbols.BaseSymbolTableInterface] = ( + collections.OrderedDict() + ) # Permanently cache all resolved symbols self._resolved: Dict[str, interfaces.objects.Template] = {} self._resolved_symbols: Dict[str, interfaces.objects.Template] = {} @@ -63,16 +69,20 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): for symbol_name in self._dict[table].get_symbols_by_type(type_name): yield table + constants.BANG + symbol_name - def get_symbols_by_location(self, offset: int, size: int = 0, table_name: str = None) -> Iterable[str]: + def get_symbols_by_location( + self, offset: int, size: int = 0, table_name: str = None + ) -> Iterable[str]: """Returns all symbols that exist at a specific relative address.""" - table_list: Iterable[interfaces.symbols.BaseSymbolTableInterface] = self._dict.values() + table_list: Iterable[interfaces.symbols.BaseSymbolTableInterface] = ( + self._dict.values() + ) if table_name is not None: if table_name in self._dict: table_list = [self._dict[table_name]] else: table_list = [] for table in table_list: - for symbol_name in table.get_symbols_by_location(offset = offset, size = size): + for symbol_name in table.get_symbols_by_location(offset=offset, size=size): yield table.name + constants.BANG + symbol_name ### Space functions @@ -118,16 +128,18 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): def __init__(self, type_name: str, **kwargs) -> None: vollog.debug(f"Unresolved reference: {type_name}") - super().__init__(type_name = type_name, **kwargs) + super().__init__(type_name=type_name, **kwargs) - def _weak_resolve(self, resolve_type: SymbolType, name: str) -> SymbolSpaceReturnType: + def _weak_resolve( + self, resolve_type: SymbolType, name: str + ) -> SymbolSpaceReturnType: """Takes a symbol name and resolves it with ReferentialTemplates.""" if resolve_type == SymbolType.TYPE: - get_function = 'get_type' + get_function = "get_type" elif resolve_type == SymbolType.SYMBOL: - get_function = 'get_symbol' + get_function = "get_symbol" elif resolve_type == SymbolType.ENUM: - get_function = 'get_enumeration' + get_function = "get_enumeration" else: raise TypeError("Weak_resolve called without a proper SymbolType") @@ -138,8 +150,11 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): try: return getattr(self._dict[table_name], get_function)(component_name) except KeyError as e: - raise exceptions.SymbolError(component_name, table_name, - f'Type {name} references missing Type/Symbol/Enum: {e}') + raise exceptions.SymbolError( + component_name, + table_name, + f"Type {name} references missing Type/Symbol/Enum: {e}", + ) raise exceptions.SymbolError(name, None, f"Malformed name: {name}") def _iterative_resolve(self, traverse_list): @@ -148,10 +163,15 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): replacements = set() # Whole Symbols that still need traversing while traverse_list: - template_traverse_list, traverse_list = [self._resolved[traverse_list[0]]], traverse_list[1:] + template_traverse_list, traverse_list = [ + self._resolved[traverse_list[0]] + ], traverse_list[1:] # Traverse a single symbol looking for any ReferenceTemplate objects while template_traverse_list: - traverser, template_traverse_list = template_traverse_list[0], template_traverse_list[1:] + traverser, template_traverse_list = ( + template_traverse_list[0], + template_traverse_list[1:], + ) for child in traverser.children: if isinstance(child, objects.templates.ReferenceTemplate): # If we haven't seen it before, subresolve it and also add it @@ -159,15 +179,20 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): if child.vol.type_name not in self._resolved: traverse_list.append(child.vol.type_name) try: - self._resolved[child.vol.type_name] = self._weak_resolve( - SymbolType.TYPE, child.vol.type_name) + self._resolved[child.vol.type_name] = ( + self._weak_resolve( + SymbolType.TYPE, child.vol.type_name + ) + ) except exceptions.SymbolError: - self._resolved[child.vol.type_name] = self.UnresolvedTemplate(child.vol.type_name) + self._resolved[child.vol.type_name] = ( + self.UnresolvedTemplate(child.vol.type_name) + ) # Stash the replacement replacements.add((traverser, child)) elif child.children: template_traverse_list.append(child) - for (parent, child) in replacements: + for parent, child in replacements: parent.replace_child(child, self._resolved[child.vol.type_name]) def get_type(self, type_name: str) -> interfaces.objects.Template: @@ -184,8 +209,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): table_name = None index = type_name.find(constants.BANG) if index > 0: - table_name, type_name = type_name[:index], type_name[index + 1:] - raise exceptions.SymbolError(type_name, table_name, f"Unresolvable symbol requested: {type_name}") + table_name, type_name = type_name[:index], type_name[index + 1 :] + raise exceptions.SymbolError( + type_name, table_name, f"Unresolvable symbol requested: {type_name}" + ) return self._resolved[type_name] def get_symbol(self, symbol_name: str) -> interfaces.symbols.SymbolInterface: @@ -197,18 +224,22 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): table_name = None index = symbol_name.find(constants.BANG) if index > 0: - table_name, symbol_name = symbol_name[:index], symbol_name[index + 1:] - raise exceptions.SymbolError(symbol_name, table_name, f"Unresolvable Symbol: {symbol_name}") + table_name, symbol_name = symbol_name[:index], symbol_name[index + 1 :] + raise exceptions.SymbolError( + symbol_name, table_name, f"Unresolvable Symbol: {symbol_name}" + ) return retval - def _subresolve(self, object_template: interfaces.objects.Template) -> interfaces.objects.Template: + def _subresolve( + self, object_template: interfaces.objects.Template + ) -> interfaces.objects.Template: """Ensure an ObjectTemplate doesn't contain any ReferenceTemplates""" for child in object_template.children: if isinstance(child, objects.templates.ReferenceTemplate): new_child = self.get_type(child.vol.type_name) else: new_child = self._subresolve(child) - object_template.replace_child(old_child = child, new_child = new_child) + object_template.replace_child(old_child=child, new_child=new_child) return object_template def get_enumeration(self, enum_name: str) -> interfaces.objects.Template: @@ -219,8 +250,10 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): table_name = None index = enum_name.find(constants.BANG) if index > 0: - table_name, enum_name = enum_name[:index], enum_name[index + 1:] - raise exceptions.SymbolError(enum_name, table_name, f"Unresolvable Enumeration: {enum_name}") + table_name, enum_name = enum_name[:index], enum_name[index + 1 :] + raise exceptions.SymbolError( + enum_name, table_name, f"Unresolvable Enumeration: {enum_name}" + ) return retval def _membership(self, member_type: SymbolType, name: str) -> bool: @@ -255,7 +288,14 @@ class SymbolSpace(interfaces.symbols.SymbolSpaceInterface): return self._membership(SymbolType.ENUM, name) -def symbol_table_is_64bit(context: interfaces.context.ContextInterface, symbol_table_name: str) -> bool: +def symbol_table_is_64bit( + context: interfaces.context.ContextInterface, symbol_table_name: str +) -> bool: """Returns a boolean as to whether a particular symbol table within a context is 64-bit or not.""" - return context.symbol_space.get_type(symbol_table_name + constants.BANG + "pointer").size == 8 + return ( + context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ).size + == 8 + ) diff --git a/volatility3/framework/symbols/generic/__init__.py b/volatility3/framework/symbols/generic/__init__.py index 68b4e9252..9d6da5aa4 100644 --- a/volatility3/framework/symbols/generic/__init__.py +++ b/volatility3/framework/symbols/generic/__init__.py @@ -10,39 +10,48 @@ from volatility3.framework import objects, interfaces class GenericIntelProcess(objects.StructType): - - def _add_process_layer(self, - context: interfaces.context.ContextInterface, - dtb: Union[int, interfaces.objects.ObjectInterface], - config_prefix: str = None, - preferred_name: str = None) -> str: + def _add_process_layer( + self, + context: interfaces.context.ContextInterface, + dtb: Union[int, interfaces.objects.ObjectInterface], + config_prefix: str = None, + preferred_name: str = None, + ) -> str: """Constructs a new layer based on the process's DirectoryTableBase.""" if config_prefix is None: # TODO: Ensure collisions can't happen by verifying the config_prefix is empty - random_prefix = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) - for _ in range(8)) - config_prefix = interfaces.configuration.path_join("temporary", "_" + random_prefix) + random_prefix = "".join( + random.SystemRandom().choice(string.ascii_uppercase + string.digits) + for _ in range(8) + ) + config_prefix = interfaces.configuration.path_join( + "temporary", "_" + random_prefix + ) # Figure out a suitable name we can use for the new layer if preferred_name is None: - preferred_name = context.layers.free_layer_name(prefix = self.vol.layer_name + "_Process") + preferred_name = context.layers.free_layer_name( + prefix=self.vol.layer_name + "_Process" + ) else: if preferred_name in context.layers: - preferred_name = context.layers.free_layer_name(prefix = preferred_name) + preferred_name = context.layers.free_layer_name(prefix=preferred_name) # Copy the parent's config and then make suitable changes parent_layer = context.layers[self.vol.layer_name] parent_config = parent_layer.build_configuration() # It's an intel layer, because we hardwire the "memory_layer" config option # FIXME: this could be for other architectures if we don't hardwire this/these values - parent_config['memory_layer'] = parent_layer.config['memory_layer'] - parent_config['page_map_offset'] = dtb + parent_config["memory_layer"] = parent_layer.config["memory_layer"] + parent_config["page_map_offset"] = dtb # Set the new configuration and construct the layer config_path = interfaces.configuration.path_join(config_prefix, preferred_name) context.config.splice(config_path, parent_config) - new_layer = parent_layer.__class__(context, config_path = config_path, name = preferred_name) + new_layer = parent_layer.__class__( + context, config_path=config_path, name=preferred_name + ) # Add the constructed layer and return the name context.layers.add_layer(new_layer) diff --git a/volatility3/framework/symbols/intermed.py b/volatility3/framework/symbols/intermed.py index a6a7a0fae..24e00bbd8 100644 --- a/volatility3/framework/symbols/intermed.py +++ b/volatility3/framework/symbols/intermed.py @@ -14,7 +14,13 @@ from abc import ABCMeta from typing import Any, Dict, Generator, Iterable, List, Mapping, Optional, Tuple, Type from volatility3 import schemas, symbols -from volatility3.framework import class_subclasses, constants, exceptions, interfaces, objects +from volatility3.framework import ( + class_subclasses, + constants, + exceptions, + interfaces, + objects, +) from volatility3.framework.configuration import requirements from volatility3.framework.layers import resources from volatility3.framework.symbols import metadata, native @@ -74,16 +80,20 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): These are documented in JSONSchema JSON files located in volatility3/schemas. """ - def __init__(self, - context: interfaces.context.ContextInterface, - config_path: str, - name: str, - isf_url: str, - native_types: interfaces.symbols.NativeTableInterface = None, - table_mapping: Optional[Dict[str, str]] = None, - validate: bool = True, - class_types: Optional[Mapping[str, Type[interfaces.objects.ObjectInterface]]] = None, - symbol_mask: int = 0) -> None: + def __init__( + self, + context: interfaces.context.ContextInterface, + config_path: str, + name: str, + isf_url: str, + native_types: interfaces.symbols.NativeTableInterface = None, + table_mapping: Optional[Dict[str, str]] = None, + validate: bool = True, + class_types: Optional[ + Mapping[str, Type[interfaces.objects.ObjectInterface]] + ] = None, + symbol_mask: int = 0, + ) -> None: """Instantiates a SymbolTable based on an IntermediateSymbolFormat JSON file. This is validated against the appropriate schema. The validation can be disabled by passing validate = False, but this should almost never be done. @@ -102,45 +112,56 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): # Check there are no obvious errors # Open the file and test the version self._versions = dict([(x.version, x) for x in class_subclasses(ISFormatTable)]) - fp = resources.ResourceAccessor().open(isf_url) - reader = codecs.getreader("utf-8") - json_object = json.load(reader(fp)) # type: ignore - fp.close() + with resources.ResourceAccessor().open(isf_url) as fp: + reader = codecs.getreader("utf-8") + json_object = json.load(reader(fp)) # type: ignore # Validation is expensive, but we cache to store the hashes of successfully validated json objects if validate and not schemas.validate(json_object): - raise exceptions.SymbolSpaceError(f"File does not pass version validation: {isf_url}") + raise exceptions.SymbolSpaceError( + f"File does not pass version validation: {isf_url}" + ) - metadata = json_object.get('metadata', None) + metadata = json_object.get("metadata", None) if not metadata: - raise exceptions.SymbolSpaceError(f"Invalid ISF file attempted to be parsed: {isf_url}") + raise exceptions.SymbolSpaceError( + f"Invalid ISF file attempted to be parsed: {isf_url}" + ) # Determine the delegate or throw an exception - self._delegate = self._closest_version(metadata.get('format', "0.0.0"), - self._versions)(context, config_path, name, json_object, native_types, - table_mapping) + self._delegate = self._closest_version( + metadata.get("format", "0.0.0"), self._versions + )(context, config_path, name, json_object, native_types, table_mapping) if self._delegate.version < constants.ISF_MINIMUM_SUPPORTED: - raise RuntimeError("ISF version {} is no longer supported: {}".format(metadata.get('format', "0.0.0"), - isf_url)) + raise RuntimeError( + "ISF version {} is no longer supported: {}".format( + metadata.get("format", "0.0.0"), isf_url + ) + ) elif self._delegate.version < constants.ISF_MINIMUM_DEPRECATED: - vollog.warning(f"ISF version {metadata.get('format', '0.0.0')} has been deprecated: {isf_url}") + vollog.warning( + f"ISF version {metadata.get('format', '0.0.0')} has been deprecated: {isf_url}" + ) # Inherit - super().__init__(context, - config_path, - name, - native_types or self._delegate.natives, - table_mapping = table_mapping, - class_types = class_types) + super().__init__( + context, + config_path, + name, + native_types or self._delegate.natives, + table_mapping=table_mapping, + class_types=class_types, + ) # Since we've been created with parameters, ensure our config is populated likewise - self.config['isf_url'] = isf_url - self.config['symbol_mask'] = symbol_mask + self.config["isf_url"] = isf_url + self.config["symbol_mask"] = symbol_mask @staticmethod - def _closest_version(version: str, versions: Dict[Tuple[int, int, int], Type['ISFormatTable']]) \ - -> Type['ISFormatTable']: + def _closest_version( + version: str, versions: Dict[Tuple[int, int, int], Type["ISFormatTable"]] + ) -> Type["ISFormatTable"]: """Determines the highest suitable handler for specified version format. @@ -154,23 +175,26 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): supported_versions = [x for x in versions if x[0] == major and x[1] >= minor] if not supported_versions: raise ValueError( - f"No Intermediate Format interface versions support file interface version: {version}") + f"No Intermediate Format interface versions support file interface version: {version}" + ) return versions[max(supported_versions)] - symbols = _construct_delegate_function('symbols', True) - types = _construct_delegate_function('types', True) - enumerations = _construct_delegate_function('enumerations', True) - metadata = _construct_delegate_function('metadata', True) - clear_symbol_cache = _construct_delegate_function('clear_symbol_cache') - get_type = _construct_delegate_function('get_type') - get_symbol = _construct_delegate_function('get_symbol') - get_enumeration = _construct_delegate_function('get_enumeration') - get_type_class = _construct_delegate_function('get_type_class') - set_type_class = _construct_delegate_function('set_type_class') - del_type_class = _construct_delegate_function('del_type_class') + symbols = _construct_delegate_function("symbols", True) + types = _construct_delegate_function("types", True) + enumerations = _construct_delegate_function("enumerations", True) + metadata = _construct_delegate_function("metadata", True) + clear_symbol_cache = _construct_delegate_function("clear_symbol_cache") + get_type = _construct_delegate_function("get_type") + get_symbol = _construct_delegate_function("get_symbol") + get_enumeration = _construct_delegate_function("get_enumeration") + get_type_class = _construct_delegate_function("get_type_class") + set_type_class = _construct_delegate_function("set_type_class") + del_type_class = _construct_delegate_function("del_type_class") @classmethod - def file_symbol_url(cls, sub_path: str, filename: Optional[str] = None) -> Generator[str, None, None]: + def file_symbol_url( + cls, sub_path: str, filename: Optional[str] = None + ) -> Generator[str, None, None]: """Returns an iterator of appropriate file-scheme symbol URLs that can be opened by a ResourceAccessor class. @@ -188,40 +212,57 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): zip_match = "/".join(os.path.split(filename)) # Check user symbol directory first, then fallback to the framework's library to allow for overloading - vollog.log(constants.LOGLEVEL_VVVV, f"Searching for symbols in {', '.join(symbols.__path__)}") + vollog.log( + constants.LOGLEVEL_VVVV, + f"Searching for symbols in {', '.join(symbols.__path__)}", + ) for path in symbols.__path__: if not os.path.isabs(path): path = os.path.abspath(os.path.join(__file__, path)) for extension in extensions: # Hopefully these will not be large lists, otherwise this might be slow try: - for found in pathlib.Path(path).joinpath(sub_path).resolve().rglob(filename + extension): + for found in ( + pathlib.Path(path) + .joinpath(sub_path) + .resolve() + .rglob(filename + extension) + ): yield found.as_uri() except FileNotFoundError: # If there's no linux symbols, don't cry about it pass # Finally try looking in zip files - zip_path = os.path.join(path, sub_path + ".zip") - if os.path.exists(zip_path): + for zip_path in ( + pathlib.Path(path).joinpath(sub_path).resolve().rglob(filename + ".zip") + ): # We have a zipfile, so run through it and look for sub files that match the filename with zipfile.ZipFile(zip_path) as zfile: for name in zfile.namelist(): for extension in extensions: # By ending with an extension (and therefore, not /), we should not return any directories - if name.endswith(zip_match + extension) or (zip_match == "*" and name.endswith(extension)): - yield "jar:file:" + str(pathlib.Path(zip_path)) + "!" + name + if name.endswith(zip_match + extension) or ( + zip_match == "*" and name.endswith(extension) + ): + yield "jar:file:" + str( + pathlib.Path(zip_path) + ) + "!" + name @classmethod - def create(cls, - context: interfaces.context.ContextInterface, - config_path: str, - sub_path: str, - filename: str, - native_types: Optional[interfaces.symbols.NativeTableInterface] = None, - table_mapping: Optional[Dict[str, str]] = None, - class_types: Optional[Mapping[str, Type[interfaces.objects.ObjectInterface]]] = None, - symbol_mask: int = 0) -> str: + def create( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + sub_path: str, + filename: str, + native_types: Optional[interfaces.symbols.NativeTableInterface] = None, + table_mapping: Optional[Dict[str, str]] = None, + class_types: Optional[ + Mapping[str, Type[interfaces.objects.ObjectInterface]] + ] = None, + symbol_mask: int = 0, + ) -> str: """Takes a context and loads an intermediate symbol table based on a filename. @@ -239,16 +280,20 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): """ urls = list(cls.file_symbol_url(sub_path, filename)) if not urls: - raise FileNotFoundError("No symbol files found at provided filename: {}", filename) + raise FileNotFoundError( + "No symbol files found at provided filename: {}", filename + ) table_name = context.symbol_space.free_table_name(filename) - table = cls(context = context, - config_path = config_path, - name = table_name, - isf_url = urls[0], - native_types = native_types, - table_mapping = table_mapping, - class_types = class_types, - symbol_mask = symbol_mask) + table = cls( + context=context, + config_path=config_path, + name=table_name, + isf_url=urls[0], + native_types=native_types, + table_mapping=table_mapping, + class_types=class_types, + symbol_mask=symbol_mask, + ) context.symbol_space.append(table) return table_name @@ -256,21 +301,26 @@ class IntermediateSymbolTable(interfaces.symbols.SymbolTableInterface): def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return super().get_requirements() + [ requirements.StringRequirement( - "isf_url", description = "JSON file containing the symbols encoded in the Intermediate Symbol Format"), + "isf_url", + description="JSON file containing the symbols encoded in the Intermediate Symbol Format", + ), ] -class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass = ABCMeta): +class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass=ABCMeta): """Provide a base class to identify all subclasses.""" + version = (0, 0, 0) - def __init__(self, - context: interfaces.context.ContextInterface, - config_path: str, - name: str, - json_object: Any, - native_types: interfaces.symbols.NativeTableInterface = None, - table_mapping: Optional[Dict[str, str]] = None) -> None: + def __init__( + self, + context: interfaces.context.ContextInterface, + config_path: str, + name: str, + json_object: Any, + native_types: interfaces.symbols.NativeTableInterface = None, + table_mapping: Optional[Dict[str, str]] = None, + ) -> None: self._json_object = json_object self._validate_json() self.name = name @@ -278,7 +328,7 @@ class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass = ABCMeta if nt is None: raise TypeError("Native table not provided") nt.name = name + "_natives" - super().__init__(context, config_path, name, nt, table_mapping = table_mapping) + super().__init__(context, config_path, name, nt, table_mapping=table_mapping) self._overrides: Dict[str, Type[interfaces.objects.ObjectInterface]] = {} self._symbol_cache: Dict[str, interfaces.symbols.SymbolInterface] = {} @@ -289,9 +339,12 @@ class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass = ABCMeta classes = {"x64": native.x64NativeTable, "x86": native.x86NativeTable} for nc in sorted(classes): native_class = classes[nc] - for base_type in self._json_object['base_types']: + for base_type in self._json_object["base_types"]: try: - if self._json_object['base_types'][base_type]['length'] != native_class.get_type(base_type).size: + if ( + self._json_object["base_types"][base_type]["length"] + != native_class.get_type(base_type).size + ): break except TypeError: # TODO: determine whether we should give voids a size - We don't give voids a length, whereas microsoft seemingly do @@ -304,9 +357,13 @@ class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass = ABCMeta # TODO: Check the format and make use of the other metadata def _validate_json(self) -> None: - if ('user_types' not in self._json_object or 'base_types' not in self._json_object - or 'metadata' not in self._json_object or 'symbols' not in self._json_object - or 'enums' not in self._json_object): + if ( + "user_types" not in self._json_object + or "base_types" not in self._json_object + or "metadata" not in self._json_object + or "symbols" not in self._json_object + or "enums" not in self._json_object + ): raise exceptions.SymbolSpaceError("Malformed JSON file provided") @property @@ -322,6 +379,7 @@ class ISFormatTable(interfaces.symbols.SymbolTableInterface, metaclass = ABCMeta class Version1Format(ISFormatTable): """Class for storing intermediate debugging data as objects and classes.""" + version = (0, 0, 1) def get_symbol(self, name: str) -> interfaces.symbols.SymbolInterface: @@ -330,35 +388,39 @@ class Version1Format(ISFormatTable): # note that this should invalidate/update the cache if self._symbol_cache.get(name, None): return self._symbol_cache[name] - symbol = self._json_object['symbols'].get(name, None) + symbol = self._json_object["symbols"].get(name, None) if not symbol: raise exceptions.SymbolError(name, self.name, f"Unknown symbol: {name}") - address = symbol['address'] - if self.config.get('symbol_mask', 0): - address = address & self.config['symbol_mask'] + address = symbol["address"] + if self.config.get("symbol_mask", 0): + address = address & self.config["symbol_mask"] - self._symbol_cache[name] = interfaces.symbols.SymbolInterface(name = name, address = address) + self._symbol_cache[name] = interfaces.symbols.SymbolInterface( + name=name, address=address + ) return self._symbol_cache[name] @property def symbols(self) -> Iterable[str]: """Returns an iterator of the symbol names.""" - return list(self._json_object.get('symbols', {})) + return list(self._json_object.get("symbols", {})) @property def enumerations(self) -> Iterable[str]: """Returns an iterator of the available enumerations.""" - return list(self._json_object.get('enums', {})) + return list(self._json_object.get("enums", {})) @property def types(self) -> Iterable[str]: """Returns an iterator of the symbol type names.""" - return list(self._json_object.get('user_types', {})) + list(self.natives.types) + return list(self._json_object.get("user_types", {})) + list(self.natives.types) def get_type_class(self, name: str) -> Type[interfaces.objects.ObjectInterface]: return self._overrides.get(name, objects.AggregateType) - def set_type_class(self, name: str, clazz: Type[interfaces.objects.ObjectInterface]) -> None: + def set_type_class( + self, name: str, clazz: Type[interfaces.objects.ObjectInterface] + ) -> None: if name not in self.types: raise ValueError(f"Symbol type not in {self.name} SymbolTable: {name}") self._overrides[name] = clazz @@ -367,112 +429,144 @@ class Version1Format(ISFormatTable): if name in self._overrides: del self._overrides[name] - def _interdict_to_template(self, dictionary: Dict[str, Any]) -> interfaces.objects.Template: + def _interdict_to_template( + self, dictionary: Dict[str, Any] + ) -> interfaces.objects.Template: """Converts an intermediate format dict into an object template.""" if not dictionary: - raise exceptions.SymbolSpaceError(f"Invalid intermediate dictionary: {dictionary}") + raise exceptions.SymbolSpaceError( + f"Invalid intermediate dictionary: {dictionary}" + ) - type_name = dictionary['kind'] - if type_name == 'base': - type_name = dictionary['name'] + type_name = dictionary["kind"] + if type_name == "base": + type_name = dictionary["name"] if type_name in self.natives.types: # The symbol is a native type - native_template = self.natives.get_type(self.name + constants.BANG + type_name) + native_template = self.natives.get_type( + self.name + constants.BANG + type_name + ) # Add specific additional parameters, etc update = {} - if type_name == 'array': - update['count'] = dictionary['count'] - update['subtype'] = self._interdict_to_template(dictionary['subtype']) - elif type_name == 'pointer': - if dictionary.get('base', None): - base_type = self.natives.get_type(self.name + constants.BANG + dictionary['base']) - update['data_format'] = base_type.vol['data_format'] - update['subtype'] = self._interdict_to_template(dictionary['subtype']) - elif type_name == 'enum': - update = self._lookup_enum(dictionary['name']) - elif type_name == 'bitfield': + if type_name == "array": + update["count"] = dictionary["count"] + update["subtype"] = self._interdict_to_template(dictionary["subtype"]) + elif type_name == "pointer": + if dictionary.get("base", None): + base_type = self.natives.get_type( + self.name + constants.BANG + dictionary["base"] + ) + update["data_format"] = base_type.vol["data_format"] + update["subtype"] = self._interdict_to_template(dictionary["subtype"]) + elif type_name == "enum": + update = self._lookup_enum(dictionary["name"]) + elif type_name == "bitfield": update = { - 'start_bit': dictionary['bit_position'], - 'end_bit': dictionary['bit_position'] + dictionary['bit_length'] + "start_bit": dictionary["bit_position"], + "end_bit": dictionary["bit_position"] + dictionary["bit_length"], } - update['base_type'] = self._interdict_to_template(dictionary['type']) + update["base_type"] = self._interdict_to_template(dictionary["type"]) # We do *not* call native_template.clone(), since it slows everything down a lot # We require that the native.get_type method always returns a newly constructed python object native_template.update_vol(**update) return native_template # Otherwise - if dictionary['kind'] not in objects.AggregateTypes.values(): - raise exceptions.SymbolSpaceError(f"Unknown Intermediate format: {dictionary}") + if dictionary["kind"] not in objects.AggregateTypes.values(): + raise exceptions.SymbolSpaceError( + f"Unknown Intermediate format: {dictionary}" + ) - reference_name = dictionary['name'] + reference_name = dictionary["name"] if constants.BANG not in reference_name: reference_name = self.name + constants.BANG + reference_name else: reference_parts = reference_name.split(constants.BANG) - reference_name = (self.table_mapping.get(reference_parts[0], reference_parts[0]) + constants.BANG + - constants.BANG.join(reference_parts[1:])) + reference_name = ( + self.table_mapping.get(reference_parts[0], reference_parts[0]) + + constants.BANG + + constants.BANG.join(reference_parts[1:]) + ) - return objects.templates.ReferenceTemplate(type_name = reference_name) + return objects.templates.ReferenceTemplate(type_name=reference_name) def _lookup_enum(self, name: str) -> Dict[str, Any]: """Looks up an enumeration and returns a dictionary of __init__ parameters for an Enum.""" - lookup = self._json_object['enums'].get(name, None) + lookup = self._json_object["enums"].get(name, None) if not lookup: raise exceptions.SymbolSpaceError(f"Unknown enumeration: {name}") - result = {"choices": copy.deepcopy(lookup['constants']), "base_type": self.natives.get_type(lookup['base'])} + result = { + "choices": copy.deepcopy(lookup["constants"]), + "base_type": self.natives.get_type(lookup["base"]), + } return result def get_enumeration(self, enum_name: str) -> interfaces.objects.Template: """Resolves an individual enumeration.""" if constants.BANG in enum_name: - raise exceptions.SymbolError(enum_name, self.name, - f"Enumeration for a different table requested: {enum_name}") - if enum_name not in self._json_object['enums']: + raise exceptions.SymbolError( + enum_name, + self.name, + f"Enumeration for a different table requested: {enum_name}", + ) + if enum_name not in self._json_object["enums"]: # Fall back to the natives table - raise exceptions.SymbolError(enum_name, self.name, - f"Enumeration not found in {self.name} table: {enum_name}") - curdict = self._json_object['enums'][enum_name] - base_type = self.natives.get_type(curdict['base']) + raise exceptions.SymbolError( + enum_name, + self.name, + f"Enumeration not found in {self.name} table: {enum_name}", + ) + curdict = self._json_object["enums"][enum_name] + base_type = self.natives.get_type(curdict["base"]) # The size isn't actually used, the base-type defines it. - return objects.templates.ObjectTemplate(type_name = self.name + constants.BANG + enum_name, - object_class = objects.Enumeration, - base_type = base_type, - choices = curdict['constants']) + return objects.templates.ObjectTemplate( + type_name=self.name + constants.BANG + enum_name, + object_class=objects.Enumeration, + base_type=base_type, + choices=curdict["constants"], + ) def get_type(self, type_name: str) -> interfaces.objects.Template: """Resolves an individual symbol.""" if constants.BANG in type_name: index = type_name.find(constants.BANG) - table_name, type_name = type_name[:index], type_name[index + 1:] + table_name, type_name = type_name[:index], type_name[index + 1 :] raise exceptions.SymbolError( - type_name, table_name, - f"Symbol for a different table requested: {table_name + constants.BANG + type_name}") - if type_name not in self._json_object['user_types']: + type_name, + table_name, + f"Symbol for a different table requested: {table_name + constants.BANG + type_name}", + ) + if type_name not in self._json_object["user_types"]: # Fall back to the natives table return self.natives.get_type(self.name + constants.BANG + type_name) - curdict = self._json_object['user_types'][type_name] + curdict = self._json_object["user_types"][type_name] members = {} - for member_name in curdict['fields']: - interdict = curdict['fields'][member_name] - member = (interdict['offset'], self._interdict_to_template(interdict['type'])) + for member_name in curdict["fields"]: + interdict = curdict["fields"][member_name] + member = ( + interdict["offset"], + self._interdict_to_template(interdict["type"]), + ) members[member_name] = member object_class = self.get_type_class(type_name) if object_class == objects.AggregateType: for clazz in objects.AggregateTypes: - if objects.AggregateTypes[clazz] == curdict['kind']: + if objects.AggregateTypes[clazz] == curdict["kind"]: object_class = clazz - return objects.templates.ObjectTemplate(type_name = self.name + constants.BANG + type_name, - object_class = object_class, - size = curdict['length'], - members = members) + return objects.templates.ObjectTemplate( + type_name=self.name + constants.BANG + type_name, + object_class=object_class, + size=curdict["length"], + members=members, + ) class Version2Format(Version1Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (2, 0, 0) def _get_natives(self) -> Optional[interfaces.symbols.NativeTableInterface]: @@ -481,9 +575,12 @@ class Version2Format(Version1Format): classes = {"x64": native.x64NativeTable, "x86": native.x86NativeTable} for nc in sorted(classes): native_class = classes[nc] - for base_type in self._json_object['base_types']: + for base_type in self._json_object["base_types"]: try: - if self._json_object['base_types'][base_type]['size'] != native_class.get_type(base_type).size: + if ( + self._json_object["base_types"][base_type]["size"] + != native_class.get_type(base_type).size + ): break except TypeError: # TODO: determine whether we should give voids a size - We don't give voids a length, whereas microsoft seemingly do @@ -497,162 +594,184 @@ class Version2Format(Version1Format): """Resolves an individual symbol.""" if constants.BANG in type_name: index = type_name.find(constants.BANG) - table_name, type_name = type_name[:index], type_name[index + 1:] + table_name, type_name = type_name[:index], type_name[index + 1 :] raise exceptions.SymbolError( - type_name, table_name, - f"Symbol for a different table requested: {table_name + constants.BANG + type_name}") - if type_name not in self._json_object['user_types']: + type_name, + table_name, + f"Symbol for a different table requested: {table_name + constants.BANG + type_name}", + ) + if type_name not in self._json_object["user_types"]: # Fall back to the natives table if type_name in self.natives.types: return self.natives.get_type(self.name + constants.BANG + type_name) else: - raise exceptions.SymbolError(type_name, self.name, f"Unknown symbol: {type_name}") - curdict = self._json_object['user_types'][type_name] + raise exceptions.SymbolError( + type_name, self.name, f"Unknown symbol: {type_name}" + ) + curdict = self._json_object["user_types"][type_name] members = {} - for member_name in curdict['fields']: - interdict = curdict['fields'][member_name] - member = (interdict['offset'], self._interdict_to_template(interdict['type'])) + for member_name in curdict["fields"]: + interdict = curdict["fields"][member_name] + member = ( + interdict["offset"], + self._interdict_to_template(interdict["type"]), + ) members[member_name] = member object_class = self.get_type_class(type_name) if object_class == objects.AggregateType: for clazz in objects.AggregateTypes: - if objects.AggregateTypes[clazz] == curdict['kind']: + if objects.AggregateTypes[clazz] == curdict["kind"]: object_class = clazz - return objects.templates.ObjectTemplate(type_name = self.name + constants.BANG + type_name, - object_class = object_class, - size = curdict['size'], - members = members) + return objects.templates.ObjectTemplate( + type_name=self.name + constants.BANG + type_name, + object_class=object_class, + size=curdict["size"], + members=members, + ) class Version3Format(Version2Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (2, 1, 0) def get_symbol(self, name: str) -> interfaces.symbols.SymbolInterface: """Returns the symbol given by the symbol name.""" if self._symbol_cache.get(name, None): return self._symbol_cache[name] - symbol = self._json_object['symbols'].get(name, None) + symbol = self._json_object["symbols"].get(name, None) if not symbol: raise exceptions.SymbolError(name, self.name, f"Unknown symbol: {name}") - address = symbol['address'] - if self.config.get('symbol_mask', 0): - address = address & self.config['symbol_mask'] + address = symbol["address"] + if self.config.get("symbol_mask", 0): + address = address & self.config["symbol_mask"] symbol_type = None - if 'type' in symbol: - symbol_type = self._interdict_to_template(symbol['type']) + if "type" in symbol: + symbol_type = self._interdict_to_template(symbol["type"]) - self._symbol_cache[name] = interfaces.symbols.SymbolInterface(name = name, address = address, - type = symbol_type) + self._symbol_cache[name] = interfaces.symbols.SymbolInterface( + name=name, address=address, type=symbol_type + ) return self._symbol_cache[name] class Version4Format(Version3Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (4, 0, 0) format_mapping = { - 'int': objects.Integer, - 'float': objects.Float, - 'void': objects.Integer, - 'bool': objects.Boolean, - 'char': objects.Char + "int": objects.Integer, + "float": objects.Float, + "void": objects.Integer, + "bool": objects.Boolean, + "char": objects.Char, } def _get_natives(self) -> Optional[interfaces.symbols.NativeTableInterface]: """Determines the appropriate native_types to use from the JSON data.""" native_dict = {} - base_types = self._json_object['base_types'] + base_types = self._json_object["base_types"] for base_type in base_types: # Void are ignored because voids are not a volatility primitive, they are a specific Volatility object - if base_type != 'void': + if base_type != "void": current = base_types[base_type] # TODO: Fix up the typing of this, it bugs out because of the tuple assignment - if current['kind'] not in self.format_mapping: + if current["kind"] not in self.format_mapping: raise ValueError("Unsupported base kind") - format_val = (current['size'], current['endian'], current['signed']) - object_type = self.format_mapping[current['kind']] - if base_type == 'pointer': + format_val = (current["size"], current["endian"], current["signed"]) + object_type = self.format_mapping[current["kind"]] + if base_type == "pointer": object_type = objects.Pointer native_dict[base_type] = (object_type, format_val) - return native.NativeTable(name = "native", native_dictionary = native_dict) + return native.NativeTable(name="native", native_dictionary=native_dict) class Version5Format(Version4Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (4, 1, 0) def get_symbol(self, name: str) -> interfaces.symbols.SymbolInterface: """Returns the symbol given by the symbol name.""" if self._symbol_cache.get(name, None): return self._symbol_cache[name] - symbol = self._json_object['symbols'].get(name, None) + symbol = self._json_object["symbols"].get(name, None) if not symbol: raise exceptions.SymbolError(name, self.name, f"Unknown symbol: {name}") - address = symbol['address'] - if self.config.get('symbol_mask', 0): - address = address & self.config['symbol_mask'] + address = symbol["address"] + if self.config.get("symbol_mask", 0): + address = address & self.config["symbol_mask"] symbol_type = None - if 'type' in symbol: - symbol_type = self._interdict_to_template(symbol['type']) + if "type" in symbol: + symbol_type = self._interdict_to_template(symbol["type"]) symbol_constant_data = None - if 'constant_data' in symbol: - symbol_constant_data = base64.b64decode(symbol.get('constant_data')) + if "constant_data" in symbol: + symbol_constant_data = base64.b64decode(symbol.get("constant_data")) - self._symbol_cache[name] = interfaces.symbols.SymbolInterface(name = name, - address = address, - type = symbol_type, - constant_data = symbol_constant_data) + self._symbol_cache[name] = interfaces.symbols.SymbolInterface( + name=name, + address=address, + type=symbol_type, + constant_data=symbol_constant_data, + ) return self._symbol_cache[name] class Version6Format(Version5Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (6, 0, 0) @property def metadata(self) -> Optional[interfaces.symbols.MetadataInterface]: """Returns a MetadataInterface object.""" - if self._json_object.get('metadata', {}).get('windows'): - return metadata.WindowsMetadata(self._json_object['metadata']['windows']) - if self._json_object.get('metadata', {}).get('linux'): - return metadata.LinuxMetadata(self._json_object['metadata']['linux']) + if self._json_object.get("metadata", {}).get("windows"): + return metadata.WindowsMetadata(self._json_object["metadata"]["windows"]) + if self._json_object.get("metadata", {}).get("linux"): + return metadata.LinuxMetadata(self._json_object["metadata"]["linux"]) return None class Version7Format(Version6Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (6, 1, 0) class Version8Format(Version7Format): """Class for storing intermediate debugging data as objects and classes.""" + version = (6, 2, 0) - def _process_fields(self, fields: Dict[str, Dict[str, Any]]) -> Dict[Any, Tuple[int, interfaces.objects.Template]]: + def _process_fields( + self, fields: Dict[str, Dict[str, Any]] + ) -> Dict[Any, Tuple[int, interfaces.objects.Template]]: """For each type field, it walks its tree of subtypes, reducing the hierarchy to just one level. It creates a tuple of offset and object templates for each field. """ members = {} for new_offset, member_name, member_value in self._reduce_fields(fields): - member = (new_offset, self._interdict_to_template(member_value['type'])) + member = (new_offset, self._interdict_to_template(member_value["type"])) members[member_name] = member return members - def _reduce_fields(self, - fields: Dict[str, Dict[str, Any]], - parent_offset: int = 0) -> Generator[Tuple[int, str, Dict], None, None]: + def _reduce_fields( + self, fields: Dict[str, Dict[str, Any]], parent_offset: int = 0 + ) -> Generator[Tuple[int, str, Dict], None, None]: """Reduce the fields bringing them one level up. It supports anonymous types such as structs or unions in any level of depth.""" for member_name, member_value in fields.items(): - new_offset = parent_offset + member_value.get('offset', 0) - if member_value.get('anonymous', False) and isinstance(member_value, dict): + new_offset = parent_offset + member_value.get("offset", 0) + if member_value.get("anonymous", False) and isinstance(member_value, dict): # Gets the subtype from the json ISF and recursively reduce its fields - subtype = self._json_object['user_types'].get(member_value['type']['name'], {}) - yield from self._reduce_fields(subtype['fields'], new_offset) + subtype = self._json_object["user_types"].get( + member_value["type"]["name"], {} + ) + yield from self._reduce_fields(subtype["fields"], new_offset) else: yield new_offset, member_name, member_value @@ -660,24 +779,28 @@ class Version8Format(Version7Format): """Resolves an individual symbol.""" index = type_name.find(constants.BANG) if index != -1: - table_name, type_name = type_name[:index], type_name[index + 1:] + table_name, type_name = type_name[:index], type_name[index + 1 :] raise exceptions.SymbolError( - type_name, table_name, - f"Symbol for a different table requested: {table_name + constants.BANG + type_name}") + type_name, + table_name, + f"Symbol for a different table requested: {table_name + constants.BANG + type_name}", + ) - type_definition = self._json_object['user_types'].get(type_name) + type_definition = self._json_object["user_types"].get(type_name) if type_definition is None: # Fall back to the natives table return self.natives.get_type(self.name + constants.BANG + type_name) - members = self._process_fields(type_definition['fields']) + members = self._process_fields(type_definition["fields"]) object_class = self.get_type_class(type_name) if object_class == objects.AggregateType: for clazz in objects.AggregateTypes: - if objects.AggregateTypes[clazz] == type_definition['kind']: + if objects.AggregateTypes[clazz] == type_definition["kind"]: object_class = clazz - return objects.templates.ObjectTemplate(type_name = self.name + constants.BANG + type_name, - object_class = object_class, - size = type_definition['size'], - members = members) + return objects.templates.ObjectTemplate( + type_name=self.name + constants.BANG + type_name, + object_class=object_class, + size=type_definition["size"], + members=members, + ) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index d59a95db5..c4e2587f4 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -1,7 +1,7 @@ # 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 # -from typing import Iterator, List, Tuple +from typing import Iterator, List, Tuple, Optional, Union from volatility3 import framework from volatility3.framework import constants, exceptions, interfaces, objects @@ -17,115 +17,150 @@ class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable): super().__init__(*args, **kwargs) # Set-up Linux specific types - self.set_type_class('file', extensions.struct_file) - self.set_type_class('list_head', extensions.list_head) - self.set_type_class('mm_struct', extensions.mm_struct) - self.set_type_class('super_block', extensions.super_block) - self.set_type_class('task_struct', extensions.task_struct) - self.set_type_class('vm_area_struct', extensions.vm_area_struct) - self.set_type_class('qstr', extensions.qstr) - self.set_type_class('dentry', extensions.dentry) - self.set_type_class('fs_struct', extensions.fs_struct) - self.set_type_class('files_struct', extensions.files_struct) - self.set_type_class('vfsmount', extensions.vfsmount) - self.set_type_class('kobject', extensions.kobject) + self.set_type_class("file", extensions.struct_file) + self.set_type_class("list_head", extensions.list_head) + self.set_type_class("mm_struct", extensions.mm_struct) + self.set_type_class("super_block", extensions.super_block) + self.set_type_class("task_struct", extensions.task_struct) + self.set_type_class("vm_area_struct", extensions.vm_area_struct) + self.set_type_class("qstr", extensions.qstr) + self.set_type_class("dentry", extensions.dentry) + self.set_type_class("fs_struct", extensions.fs_struct) + self.set_type_class("files_struct", extensions.files_struct) + self.set_type_class("kobject", extensions.kobject) + self.set_type_class("cred", extensions.cred) + # Might not exist in the current symbols + self.optional_set_type_class("module", extensions.module) + self.optional_set_type_class("bpf_prog", extensions.bpf_prog) + self.optional_set_type_class("kernel_cap_struct", extensions.kernel_cap_struct) + self.optional_set_type_class("kernel_cap_t", extensions.kernel_cap_t) - if 'mnt_namespace' in self.types: - self.set_type_class('mnt_namespace', extensions.mnt_namespace) + # Mount + self.set_type_class("vfsmount", extensions.vfsmount) + # Might not exist in older kernels or the current symbols + self.optional_set_type_class("mount", extensions.mount) + self.optional_set_type_class("mnt_namespace", extensions.mnt_namespace) - if 'module' in self.types: - self.set_type_class('module', extensions.module) + # Network + self.set_type_class("net", extensions.net) + self.set_type_class("socket", extensions.socket) + self.set_type_class("sock", extensions.sock) + self.set_type_class("inet_sock", extensions.inet_sock) + self.set_type_class("unix_sock", extensions.unix_sock) + # Might not exist in older kernels or the current symbols + self.optional_set_type_class("netlink_sock", extensions.netlink_sock) + self.optional_set_type_class("vsock_sock", extensions.vsock_sock) + self.optional_set_type_class("packet_sock", extensions.packet_sock) + self.optional_set_type_class("bt_sock", extensions.bt_sock) + self.optional_set_type_class("xdp_sock", extensions.xdp_sock) - if 'mount' in self.types: - self.set_type_class('mount', extensions.mount) + # Only found in 6.1+ kernels + self.optional_set_type_class("maple_tree", extensions.maple_tree) class LinuxUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful linux functions.""" - _version = (2, 0, 0) + _version = (2, 1, 0) _required_framework_version = (2, 0, 0) framework.require_interface_version(*_required_framework_version) - # based on __d_path from the Linux kernel @classmethod - def _do_get_path(cls, rdentry, rmnt, dentry, vfsmnt) -> str: + def _get_path_file(cls, task, filp) -> str: + """Returns the file pathname relative to the task's root directory. - ret_path: List[str] = [] + Args: + task (task_struct): A reference task + filp (file *): A pointer to an open file - while dentry != rdentry or vfsmnt != rmnt: - dname = dentry.path() - if dname == "": - break + Returns: + str: File pathname relative to the task's root directory. + """ + rdentry = task.fs.get_root_dentry() + rmnt = task.fs.get_root_mnt() + vfsmnt = filp.get_vfsmnt() + dentry = filp.get_dentry() - ret_path.insert(0, dname.strip('/')) - if dentry == vfsmnt.get_mnt_root() or dentry == dentry.d_parent: - if vfsmnt.get_mnt_parent() == vfsmnt: + return cls.do_get_path(rdentry, rmnt, dentry, vfsmnt) + + @classmethod + def get_path_mnt(cls, task, mnt) -> str: + """Returns the mount point pathname relative to the task's root directory. + + Args: + task (task_struct): A reference task + mnt (vfsmount or mount): A mounted filesystem or a mount point. + - kernels < 3.3.8 type is 'vfsmount' + - kernels >= 3.3.8 type is 'mount' + + Returns: + str: Pathname of the mount point relative to the task's root directory. + """ + rdentry = task.fs.get_root_dentry() + rmnt = task.fs.get_root_mnt() + + vfsmnt = mnt.get_vfsmnt_current() + dentry = mnt.get_dentry_current() + + return cls.do_get_path(rdentry, rmnt, dentry, vfsmnt) + + @classmethod + def do_get_path(cls, rdentry, rmnt, dentry, vfsmnt) -> Union[None, str]: + """Returns a pathname of the mount point or file + It mimics the Linux kernel prepend_path function. + + Args: + rdentry (dentry *): A pointer to the root dentry + rmnt (vfsmount *): A pointer to the root vfsmount + dentry (dentry *): A pointer to the dentry + vfsmnt (vfsmount *): A pointer to the vfsmount + + Returns: + str: Pathname of the mount point or file + """ + + path_reversed = [] + while dentry != rdentry or not vfsmnt.is_equal(rmnt): + if dentry == vfsmnt.get_mnt_root() or dentry.is_root(): + # Escaped? + if dentry != vfsmnt.get_mnt_root(): break - dentry = vfsmnt.get_mnt_mountpoint() - vfsmnt = vfsmnt.get_mnt_parent() + # Global root? + if not vfsmnt.has_parent(): + break + + dentry = vfsmnt.get_dentry_parent() + vfsmnt = vfsmnt.get_vfsmnt_parent() continue parent = dentry.d_parent + dname = dentry.d_name.name_as_str() + path_reversed.append(dname.strip("/")) dentry = parent - # if we did not gather any valid dentrys in the path, then the entire file is - # either 1) smeared out of memory or 2) de-allocated and corresponding structures overwritten - # we return an empty string in this case to avoid confusion with something like a handle to the root - # directory (e.g., "/") - if not ret_path: - return "" - - ret_val = '/'.join([str(p) for p in ret_path if p != ""]) - - if ret_val.startswith(("socket:", "pipe:")): - if ret_val.find("]") == -1: - try: - inode = dentry.d_inode - ino = inode.i_ino - except exceptions.InvalidAddressException: - ino = 0 - - ret_val = ret_val[:-1] + f":[{ino}]" - else: - ret_val = ret_val.replace("/", "") - - elif ret_val != "inotify": - ret_val = '/' + ret_val - - return ret_val - - # method used by 'older' kernels - # TODO: lookup when dentry_operations->d_name was merged into the mainline kernel for exact version - @classmethod - def _get_path_file(cls, task, filp) -> str: - rdentry = task.fs.get_root_dentry() - rmnt = task.fs.get_root_mnt() - dentry = filp.get_dentry() - vfsmnt = filp.get_vfsmnt() - - return LinuxUtilities._do_get_path(rdentry, rmnt, dentry, vfsmnt) + path = "/" + "/".join(reversed(path_reversed)) + return path @classmethod def _get_new_sock_pipe_path(cls, context, task, filp) -> str: + """Returns the sock pipe pathname relative to the task's root directory. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + task (task_struct): A reference task + filp (file *): A pointer to a sock pipe open file + + Returns: + str: Sock pipe pathname relative to the task's root directory. + """ dentry = filp.get_dentry() + kernel_module = cls.get_module_from_volobj_type(context, dentry) + sym_addr = dentry.d_op.d_dname - - symbol_table_arr = sym_addr.vol.type_name.split("!") - symbol_table = None - if len(symbol_table_arr) == 2: - symbol_table = symbol_table_arr[0] - - for module_name in context.modules.get_modules_by_symbol_tables(symbol_table): - kernel_module = context.modules[module_name] - break - else: - raise ValueError(f"No module using the symbol table {symbol_table}") - symbs = list(kernel_module.get_symbols_by_absolute_location(sym_addr)) if len(symbs) == 1: @@ -153,12 +188,26 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return ret - # a 'file' structure doesn't have enough information to properly restore its full path - # we need the root mount information from task_struct to determine this @classmethod def path_for_file(cls, context, task, filp) -> str: + """Returns a file (or sock pipe) pathname relative to the task's root directory. + + A 'file' structure doesn't have enough information to properly restore its + full path we need the root mount information from task_struct to determine this + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + task (task_struct): A reference task + filp (file *): A pointer to an open file + + Returns: + str: A file (or sock pipe) pathname relative to the task's root directory. + """ + + # Memory smear protection: Check that both the file and dentry pointers are valid. try: dentry = filp.get_dentry() + dentry.is_root() except exceptions.InvalidAddressException: return "" @@ -169,7 +218,11 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): # TODO COMPARE THIS IN LSOF OUTPUT TO VOL2 try: - if dentry.d_op and dentry.d_op.has_member("d_dname") and dentry.d_op.d_dname: + if ( + dentry.d_op + and dentry.d_op.has_member("d_dname") + and dentry.d_op.d_dname + ): dname_is_valid = True except exceptions.InvalidAddressException: @@ -183,44 +236,66 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): return ret @classmethod - def files_descriptors_for_process(cls, context: interfaces.context.ContextInterface, symbol_table: str, - task: interfaces.objects.ObjectInterface): + def files_descriptors_for_process( + cls, + context: interfaces.context.ContextInterface, + symbol_table: str, + task: interfaces.objects.ObjectInterface, + ): + # task.files can be null + if not task.files: + return None fd_table = task.files.get_fds() if fd_table == 0: - return + return None max_fds = task.files.get_max_fds() # corruption check if max_fds > 500000: - return + return None - file_type = symbol_table + constants.BANG + 'file' + file_type = symbol_table + constants.BANG + "file" - fds = objects.utility.array_of_pointers(fd_table, count = max_fds, subtype = file_type, context = context) + fds = objects.utility.array_of_pointers( + fd_table, count=max_fds, subtype=file_type, context=context + ) - for (fd_num, filp) in enumerate(fds): + for fd_num, filp in enumerate(fds): if filp != 0: full_path = LinuxUtilities.path_for_file(context, task, filp) yield fd_num, filp, full_path @classmethod - def mask_mods_list(cls, context: interfaces.context.ContextInterface, layer_name: str, - mods: Iterator[interfaces.objects.ObjectInterface]) -> List[Tuple[str, int, int]]: + def mask_mods_list( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + mods: Iterator[interfaces.objects.ObjectInterface], + ) -> List[Tuple[str, int, int]]: """ A helper function to mask the starting and end address of kernel modules """ mask = context.layers[layer_name].address_mask - return [(utility.array_to_string(mod.name), mod.get_module_base() & mask, - (mod.get_module_base() & mask) + mod.get_core_size()) for mod in mods] + return [ + ( + utility.array_to_string(mod.name), + mod.get_module_base() & mask, + (mod.get_module_base() & mask) + mod.get_core_size(), + ) + for mod in mods + ] @classmethod def generate_kernel_handler_info( - cls, context: interfaces.context.ContextInterface, kernel_module_name: str, - mods_list: Iterator[interfaces.objects.ObjectInterface]) -> List[Tuple[str, int, int]]: + cls, + context: interfaces.context.ContextInterface, + kernel_module_name: str, + mods_list: Iterator[interfaces.objects.ObjectInterface], + ) -> List[Tuple[str, int, int]]: """ A helper function that gets the beginning and end address of the kernel module """ @@ -235,13 +310,17 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): end_addr = kernel.object_from_symbol("_etext") end_addr = end_addr.vol.offset & mask - return [(constants.linux.KERNEL_NAME, start_addr, end_addr)] + \ - LinuxUtilities.mask_mods_list(context, kernel.layer_name, mods_list) + return [ + (constants.linux.KERNEL_NAME, start_addr, end_addr) + ] + LinuxUtilities.mask_mods_list(context, kernel.layer_name, mods_list) @classmethod - def lookup_module_address(cls, kernel_module: interfaces.context.ModuleInterface, - handlers: List[Tuple[str, int, int]], - target_address: int): + def lookup_module_address( + cls, + kernel_module: interfaces.context.ModuleInterface, + handlers: List[Tuple[str, int, int]], + target_address: int, + ): """ Searches between the start and end address of the kernel module using target_address. Returns the module and symbol name of the address provided. @@ -254,11 +333,16 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): if start <= target_address <= end: mod_name = name if name == constants.linux.KERNEL_NAME: - symbols = list(kernel_module.get_symbols_by_absolute_location(target_address)) + symbols = list( + kernel_module.get_symbols_by_absolute_location(target_address) + ) if len(symbols): - symbol_name = symbols[0].split(constants.BANG)[1] if constants.BANG in symbols[0] else \ - symbols[0] + symbol_name = ( + symbols[0].split(constants.BANG)[1] + if constants.BANG in symbols[0] + else symbols[0] + ) break @@ -267,6 +351,71 @@ class LinuxUtilities(interfaces.configuration.VersionableInterface): @classmethod def walk_internal_list(cls, vmlinux, struct_name, list_member, list_start): while list_start: - list_struct = vmlinux.object(object_type = struct_name, offset = list_start.vol.offset) + list_struct = vmlinux.object( + object_type=struct_name, offset=list_start.vol.offset + ) yield list_struct list_start = getattr(list_struct, list_member) + + @classmethod + def container_of( + cls, + addr: int, + type_name: str, + member_name: str, + vmlinux: interfaces.context.ModuleInterface, + ) -> Optional[interfaces.objects.ObjectInterface]: + """Cast a member of a structure out to the containing structure. + It mimicks the Linux kernel macro container_of() see include/linux.kernel.h + + Args: + addr: The pointer to the member. + type_name: The type of the container struct this is embedded in. + member_name: The name of the member within the struct. + vmlinux: The kernel symbols object + + Returns: + The constructed object or None + """ + + if not addr: + return None + + type_dec = vmlinux.get_type(type_name) + member_offset = type_dec.relative_child_offset(member_name) + container_addr = addr - member_offset + return vmlinux.object( + object_type=type_name, offset=container_addr, absolute=True + ) + + @classmethod + def get_module_from_volobj_type( + cls, + context: interfaces.context.ContextInterface, + volobj: interfaces.objects.ObjectInterface, + ) -> interfaces.context.ModuleInterface: + """Get the vmlinux from a vol obj + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + volobj (vol object): A vol object + + Raises: + ValueError: If it cannot obtain any module from the symbol table + + Returns: + A kernel object (vmlinux) + """ + symbol_table_arr = volobj.vol.type_name.split("!", 1) + symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None + + module_names = context.modules.get_modules_by_symbol_tables(symbol_table) + module_names = list(module_names) + + if not module_names: + raise ValueError(f"No module using the symbol table '{symbol_table}'") + + kernel_module_name = module_names[0] + kernel = context.modules[kernel_module_name] + + return kernel diff --git a/volatility3/framework/symbols/linux/bash.py b/volatility3/framework/symbols/linux/bash.py index f1f40ace1..9df2bb255 100644 --- a/volatility3/framework/symbols/linux/bash.py +++ b/volatility3/framework/symbols/linux/bash.py @@ -7,8 +7,7 @@ from volatility3.framework.symbols.linux.extensions import bash class BashIntermedSymbols(intermed.IntermediateSymbolTable): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_type_class('hist_entry', bash.hist_entry) + self.set_type_class("hist_entry", bash.hist_entry) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 6792ab19c..d73d0cfb9 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,14 +4,20 @@ import collections.abc import logging -from typing import Generator, Iterable, Iterator, Optional, Tuple +import socket as socket_module +from typing import Generator, Iterable, Iterator, Optional, Tuple, List from volatility3.framework import constants +from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY +from volatility3.framework.constants.linux import IP_PROTOCOLS, IPV6_PROTOCOLS +from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS +from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES +from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES +from volatility3.framework.constants.linux import CAPABILITIES from volatility3.framework import exceptions, objects, interfaces, symbols from volatility3.framework.layers import linear from volatility3.framework.objects import utility -from volatility3.framework.symbols import generic, linux -from volatility3.framework.symbols import intermed +from volatility3.framework.symbols import generic, linux, intermed from volatility3.framework.symbols.linux.extensions import elf vollog = logging.getLogger(__name__) @@ -20,7 +26,6 @@ vollog = logging.getLogger(__name__) class module(generic.GenericIntelProcess): - def get_module_base(self): if self.has_member("core_layout"): return self.core_layout.base @@ -30,27 +35,26 @@ class module(generic.GenericIntelProcess): def get_init_size(self): if self.has_member("init_layout"): return self.init_layout.size - elif self.has_member("init_size"): return self.init_size - - raise AttributeError("module -> get_init_size: Unable to determine .init section size of module") + raise AttributeError( + "module -> get_init_size: Unable to determine .init section size of module" + ) def get_core_size(self): if self.has_member("core_layout"): return self.core_layout.size - elif self.has_member("core_size"): return self.core_size - - raise AttributeError("module -> get_core_size: Unable to determine core size of module") + raise AttributeError( + "module -> get_core_size: Unable to determine core size of module" + ) def get_module_core(self): if self.has_member("core_layout"): return self.core_layout.base elif self.has_member("module_core"): return self.module_core - raise AttributeError("module -> get_module_core: Unable to get module core") def get_module_init(self): @@ -58,76 +62,125 @@ class module(generic.GenericIntelProcess): return self.init_layout.base elif self.has_member("module_init"): return self.module_init - raise AttributeError("module -> get_module_core: Unable to get module init") def get_name(self): - """ Get the name of the module as a string """ + """Get the name of the module as a string""" return utility.array_to_string(self.name) def _get_sect_count(self, grp): - """ Try to determine the number of valid sections """ + """Try to determine the number of valid sections""" arr = self._context.object( - self.get_symbol_table().name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = grp.attrs, - subtype = self._context.symbol_space.get_type(self.get_symbol_table().name + constants.BANG + "pointer"), - count = 25) + self.get_symbol_table_name() + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=grp.attrs, + subtype=self._context.symbol_space.get_type( + self.get_symbol_table_name() + constants.BANG + "pointer" + ), + count=25, + ) idx = 0 while arr[idx]: idx = idx + 1 - return idx def get_sections(self): - """ Get sections of the module """ + """Get sections of the module""" if self.sect_attrs.has_member("nsections"): num_sects = self.sect_attrs.nsections else: num_sects = self._get_sect_count(self.sect_attrs.grp) - - arr = self._context.object(self.get_symbol_table().name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self.sect_attrs.attrs.vol.offset, - subtype = self._context.symbol_space.get_type(self.get_symbol_table().name + - constants.BANG + 'module_sect_attr'), - count = num_sects) + arr = self._context.object( + self.get_symbol_table_name() + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=self.sect_attrs.attrs.vol.offset, + subtype=self._context.symbol_space.get_type( + self.get_symbol_table_name() + constants.BANG + "module_sect_attr" + ), + count=num_sects, + ) for attr in arr: yield attr + def get_elf_table_name(self): + elf_table_name = intermed.IntermediateSymbolTable.create( + self._context, + "elf_symbol_table", + "linux", + "elf", + native_types=None, + class_types=elf.class_types, + ) + return elf_table_name + def get_symbols(self): - if symbols.symbol_table_is_64bit(self._context, self.get_symbol_table().name): + """Get symbols of the module + + Yields: + A symbol object + """ + + if not hasattr(self, "_elf_table_name"): + self._elf_table_name = self.get_elf_table_name() + if symbols.symbol_table_is_64bit(self._context, self.get_symbol_table_name()): prefix = "Elf64_" else: prefix = "Elf32_" - - elf_table_name = intermed.IntermediateSymbolTable.create(self.context, - self.config_path, - "linux", - "elf", - native_types = None, - class_types = elf.class_types) - syms = self._context.object( - self.get_symbol_table().name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self.section_symtab, - subtype = self._context.symbol_space.get_type(elf_table_name + constants.BANG + prefix + "Sym"), - count = self.num_symtab + 1) + self.get_symbol_table_name() + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=self.section_symtab, + subtype=self._context.symbol_space.get_type( + self._elf_table_name + constants.BANG + prefix + "Sym" + ), + count=self.num_symtab + 1, + ) if self.section_strtab: for sym in syms: - sym.set_cached_strtab(self.section_strtab) yield sym - def get_symbol(self, wanted_sym_name): - """ Get value for a given symbol name """ + def get_symbols_names_and_addresses(self) -> Tuple[str, int]: + """Get names and addresses for each symbol of the module + + Yields: + A tuple for each symbol containing the symbol name and its corresponding value + """ + for sym in self.get_symbols(): - sym_name = sym.get_name() - sym_addr = sym.st_value + sym_arr = self._context.object( + self.get_symbol_table_name() + constants.BANG + "array", + layer_name=self.vol.native_layer_name, + offset=self.section_strtab + sym.st_name, + ) + try: + sym_name = utility.array_to_string( + sym_arr, 512 + ) # 512 is the value of KSYM_NAME_LEN kernel constant + except exceptions.InvalidAddressException: + continue + if sym_name != "": + # Normalize sym.st_value offset, which is an address pointing to the symbol value + mask = self._context.layers[self.vol.layer_name].address_mask + sym_address = sym.st_value & mask + yield (sym_name, sym_address) + + def get_symbol(self, wanted_sym_name): + """Get symbol value for a given symbol name""" + for sym_name, sym_address in self.get_symbols_names_and_addresses(): if wanted_sym_name == sym_name: - return sym_addr + return sym_address + + return None + + def get_symbol_by_address(self, wanted_sym_address): + """Get symbol name for a given symbol address""" + for sym_name, sym_address in self.get_symbols_names_and_addresses(): + if wanted_sym_address == sym_address: + return sym_name + + return None @property def section_symtab(self): @@ -135,7 +188,6 @@ class module(generic.GenericIntelProcess): return self.kallsyms.symtab elif self.has_member("symtab"): return self.symtab - raise AttributeError("module -> symtab: Unable to get symtab") @property @@ -144,8 +196,9 @@ class module(generic.GenericIntelProcess): return int(self.kallsyms.num_symtab) elif self.has_member("num_symtab"): return int(self.num_symtab) - - raise AttributeError("module -> num_symtab: Unable to determine number of symbols") + raise AttributeError( + "module -> num_symtab: Unable to determine number of symbols" + ) @property def section_strtab(self): @@ -155,13 +208,13 @@ class module(generic.GenericIntelProcess): # Older kernels elif self.has_member("strtab"): return self.strtab - raise AttributeError("module -> strtab: Unable to get strtab") class task_struct(generic.GenericIntelProcess): - - def add_process_layer(self, config_prefix: str = None, preferred_name: str = None) -> Optional[str]: + def add_process_layer( + self, config_prefix: str = None, preferred_name: str = None + ) -> Optional[str]: """Constructs a new layer based on the process's DTB. Returns the name of the Layer or None. @@ -172,24 +225,26 @@ class task_struct(generic.GenericIntelProcess): pgd = self.mm.pgd except exceptions.InvalidAddressException: return None - if not isinstance(parent_layer, linear.LinearlyMappedLayer): - raise TypeError("Parent layer is not a translation layer, unable to construct process layer") - + raise TypeError( + "Parent layer is not a translation layer, unable to construct process layer" + ) dtb, layer_name = parent_layer.translate(pgd) if not dtb: return None - if preferred_name is None: preferred_name = self.vol.layer_name + f"_Process{self.pid}" - # Add the constructed layer and return the name - return self._add_process_layer(self._context, dtb, config_prefix, preferred_name) + return self._add_process_layer( + self._context, dtb, config_prefix, preferred_name + ) - def get_process_memory_sections(self, heap_only: bool = False) -> Generator[Tuple[int, int], None, None]: + def get_process_memory_sections( + self, heap_only: bool = False + ) -> Generator[Tuple[int, int], None, None]: """Returns a list of sections based on the memory manager's view of this task's virtual memory.""" - for vma in self.mm.get_mmap_iter(): + for vma in self.mm.get_vma_iter(): start = int(vma.vm_start) end = int(vma.vm_end) @@ -197,8 +252,9 @@ class task_struct(generic.GenericIntelProcess): continue else: # FIXME: Check if this actually needs to be printed out or not - vollog.info(f"adding vma: {start:x} {self.mm.brk:x} | {end:x} {self.mm.start_brk:x}") - + vollog.info( + f"adding vma: {start:x} {self.mm.brk:x} | {end:x} {self.mm.start_brk:x}" + ) yield (start, end - start) @property @@ -239,20 +295,18 @@ class task_struct(generic.GenericIntelProcess): # threads and using the thread_group offset to get the # corresponding task_struct for task in self.thread_group.to_list( - f"{task_symbol_table_name}{constants.BANG}task_struct", - "thread_group" + f"{task_symbol_table_name}{constants.BANG}task_struct", "thread_group" ): yield task -class fs_struct(objects.StructType): +class fs_struct(objects.StructType): def get_root_dentry(self): # < 2.6.26 if self.has_member("rootmnt"): return self.root elif self.root.has_member("dentry"): return self.root.dentry - raise AttributeError("Unable to find the root dentry") def get_root_mnt(self): @@ -261,18 +315,147 @@ class fs_struct(objects.StructType): return self.rootmnt elif self.root.has_member("mnt"): return self.root.mnt - raise AttributeError("Unable to find the root mount") -class mm_struct(objects.StructType): +class maple_tree(objects.StructType): + # include/linux/maple_tree.h + # Mask for Maple Tree Flags + MT_FLAGS_HEIGHT_MASK = 0x7C + MT_FLAGS_HEIGHT_OFFSET = 0x02 + # Shift and mask to extract information from maple tree node pointers + MAPLE_NODE_TYPE_SHIFT = 0x03 + MAPLE_NODE_TYPE_MASK = 0x0F + MAPLE_NODE_POINTER_MASK = 0xFF + + # types of Maple Tree Nodes + MAPLE_DENSE = 0 + MAPLE_LEAF_64 = 1 + MAPLE_RANGE_64 = 2 + MAPLE_ARANGE_64 = 3 + + def get_slot_iter(self): + """Parse the Maple Tree and return every non zero slot.""" + maple_tree_offset = self.vol.offset & ~(self.MAPLE_NODE_POINTER_MASK) + expected_maple_tree_depth = ( + self.ma_flags & self.MT_FLAGS_HEIGHT_MASK + ) >> self.MT_FLAGS_HEIGHT_OFFSET + yield from self._parse_maple_tree_node( + self.ma_root, maple_tree_offset, expected_maple_tree_depth + ) + + def _parse_maple_tree_node( + self, + maple_tree_entry, + parent, + expected_maple_tree_depth, + seen=None, + current_depth=1, + ): + """Recursively parse Maple Tree Nodes and yield all non empty slots""" + + # Create seen set if it does not exist, e.g. on the first call into this recursive function. This + # must be None or an existing set of addresses for MTEs that have already been processed or that + # should otherwise be ignored. If parsing from the root node for example this should be None on the + # first call. If you needed to parse all nodes downwards from part of the tree this should still be + # None. If however you wanted to parse from a node, but ignore some parts of the tree below it then + # this could be populated with the addresses of the nodes you wish to ignore. + + if seen is None: + seen = set() + + # protect against unlikely loop + if maple_tree_entry in seen: + vollog.warning( + f"The mte {hex(maple_tree_entry)} has all ready been seen, no further results will be produced for this node." + ) + return None + else: + seen.add(maple_tree_entry) + + # check if we have exceeded the expected depth of this maple tree. + # e.g. when current_depth is larger than expected_maple_tree_depth there may be an issue. + # it is normal that expected_maple_tree_depth is equal to current_depth. + if expected_maple_tree_depth < current_depth: + vollog.warning( + f"The depth for the maple tree at {hex(self.vol.offset)} is {expected_maple_tree_depth}, however when parsing the nodes " + f"a depth of {current_depth} was reached. This is unexpected and may lead to incorrect results." + ) + + # parse the mte to extract the pointer value, node type, and leaf status + pointer = maple_tree_entry & ~(self.MAPLE_NODE_POINTER_MASK) + node_type = ( + maple_tree_entry >> self.MAPLE_NODE_TYPE_SHIFT + ) & self.MAPLE_NODE_TYPE_MASK + + # create a pointer object for the node parent mte (note this will include flags in the low bits) + symbol_table_name = self.get_symbol_table_name() + node_parent_mte = self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.native_layer_name, + offset=pointer, + ) + + # extract the actual pointer to the parent of this node + node_parent_pointer = node_parent_mte & ~(self.MAPLE_NODE_POINTER_MASK) + + # verify that the node_parent_pointer correctly points to the parent + assert node_parent_pointer == parent + + # create a node object + node = self._context.object( + symbol_table_name + constants.BANG + "maple_node", + layer_name=self.vol.layer_name, + offset=pointer, + ) + + # parse the slots based on the node type + if node_type == self.MAPLE_DENSE: + for slot in node.alloc.slot: + if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0: + yield slot + elif node_type == self.MAPLE_LEAF_64: + for slot in node.mr64.slot: + if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0: + yield slot + elif node_type == self.MAPLE_RANGE_64: + for slot in node.mr64.slot: + if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0: + yield from self._parse_maple_tree_node( + slot, + pointer, + expected_maple_tree_depth, + seen, + current_depth + 1, + ) + elif node_type == self.MAPLE_ARANGE_64: + for slot in node.ma64.slot: + if (slot & ~(self.MAPLE_NODE_TYPE_MASK)) != 0: + yield from self._parse_maple_tree_node( + slot, + pointer, + expected_maple_tree_depth, + seen, + current_depth + 1, + ) + else: + # unkown maple node type + raise AttributeError( + f"Unkown Maple Tree node type {node_type} at offset {hex(pointer)}." + ) + + +class mm_struct(objects.StructType): def get_mmap_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: """Returns an iterator for the mmap list member of an mm_struct.""" + if not self.has_member("mmap"): + raise AttributeError( + "get_mmap_iter called on mm_struct where no mmap member exists." + ) if not self.mmap: - return - + return None yield self.mmap seen = {self.mmap.vol.offset} @@ -283,32 +466,59 @@ class mm_struct(objects.StructType): seen.add(link.vol.offset) link = link.vm_next + def get_maple_tree_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns an iterator for the mm_mt member of an mm_struct.""" + + if not self.has_member("mm_mt"): + raise AttributeError( + "get_maple_tree_iter called on mm_struct where no mm_mt member exists." + ) + symbol_table_name = self.get_symbol_table_name() + for vma_pointer in self.mm_mt.get_slot_iter(): + # convert pointer to vm_area_struct and yield + vma = self._context.object( + symbol_table_name + constants.BANG + "vm_area_struct", + layer_name=self.vol.native_layer_name, + offset=vma_pointer, + ) + yield vma + + def get_vma_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: + """Returns an iterator for the VMAs in an mm_struct. Automatically choosing the mmap or mm_mt as required.""" + + if self.has_member("mmap"): + yield from self.get_mmap_iter() + elif self.has_member("mm_mt"): + yield from self.get_maple_tree_iter() + else: + raise AttributeError("Unable to find mmap or mm_mt in mm_struct") + class super_block(objects.StructType): # include/linux/kdev_t.h MINORBITS = 20 # Superblock flags - SB_RDONLY = 1 # Mount read-only - SB_NOSUID = 2 # Ignore suid and sgid bits - SB_NODEV = 4 # Disallow access to device special files - SB_NOEXEC = 8 # Disallow program execution - SB_SYNCHRONOUS = 16 # Writes are synced at once - SB_MANDLOCK = 64 # Allow mandatory locks on an FS - SB_DIRSYNC = 128 # Directory modifications are synchronous - SB_NOATIME = 1024 # Do not update access times - SB_NODIRATIME = 2048 # Do not update directory access times + SB_RDONLY = 1 # Mount read-only + SB_NOSUID = 2 # Ignore suid and sgid bits + SB_NODEV = 4 # Disallow access to device special files + SB_NOEXEC = 8 # Disallow program execution + SB_SYNCHRONOUS = 16 # Writes are synced at once + SB_MANDLOCK = 64 # Allow mandatory locks on an FS + SB_DIRSYNC = 128 # Directory modifications are synchronous + SB_NOATIME = 1024 # Do not update access times + SB_NODIRATIME = 2048 # Do not update directory access times SB_SILENT = 32768 - SB_POSIXACL = (1 << 16) # VFS does not apply the umask - SB_KERNMOUNT = (1 << 22) # this is a kern_mount call - SB_I_VERSION = (1 << 23) # Update inode I_version field - SB_LAZYTIME = (1 << 25) # Update the on-disk [acm]times lazily + SB_POSIXACL = 1 << 16 # VFS does not apply the umask + SB_KERNMOUNT = 1 << 22 # this is a kern_mount call + SB_I_VERSION = 1 << 23 # Update inode I_version field + SB_LAZYTIME = 1 << 25 # Update the on-disk [acm]times lazily SB_OPTS = { SB_SYNCHRONOUS: "sync", SB_DIRSYNC: "dirsync", SB_MANDLOCK: "mand", - SB_LAZYTIME: "lazytime" + SB_LAZYTIME: "lazytime", } @property @@ -320,10 +530,12 @@ class super_block(objects.StructType): return self.s_dev & ((1 << self.MINORBITS) - 1) def get_flags_access(self) -> str: - return 'ro' if self.s_flags & self.SB_RDONLY else 'rw' + return "ro" if self.s_flags & self.SB_RDONLY else "rw" def get_flags_opts(self) -> Iterable[str]: - sb_opts = [self.SB_OPTS[sb_opt] for sb_opt in self.SB_OPTS if sb_opt & self.s_flags] + sb_opts = [ + self.SB_OPTS[sb_opt] for sb_opt in self.SB_OPTS if sb_opt & self.s_flags + ] return sb_opts def get_type(self): @@ -386,8 +598,7 @@ class vm_area_struct(objects.StructType): if (vm_flags & mask) == mask: retval = retval + char else: - retval = retval + '-' - + retval = retval + "-" return retval # only parse the rwx bits @@ -401,7 +612,6 @@ class vm_area_struct(objects.StructType): def get_page_offset(self) -> int: if self.vm_file == 0: return 0 - return self.vm_pgoff << constants.linux.PAGE_SHIFT def get_name(self, context, task): @@ -409,55 +619,71 @@ class vm_area_struct(objects.StructType): fname = linux.LinuxUtilities.path_for_file(context, task, self.vm_file) elif self.vm_start <= task.mm.start_brk and self.vm_end >= task.mm.brk: fname = "[heap]" - elif self.vm_start <= task.mm.start_stack and self.vm_end >= task.mm.start_stack: + elif self.vm_start <= task.mm.start_stack <= self.vm_end: fname = "[stack]" - elif self.vm_mm.context.has_member("vdso") and self.vm_start == self.vm_mm.context.vdso: + elif ( + self.vm_mm.context.has_member("vdso") + and self.vm_start == self.vm_mm.context.vdso + ): fname = "[vdso]" else: fname = "Anonymous Mapping" - return fname # used by malfind - def is_suspicious(self): + def is_suspicious(self, proclayer=None): ret = False flags_str = self.get_protection() if flags_str == "rwx": ret = True - elif flags_str == "r-x" and self.vm_file.dereference().vol.offset == 0: ret = True - + elif proclayer and "x" in flags_str: + for i in range(self.vm_start, self.vm_end, 1 << constants.linux.PAGE_SHIFT): + try: + if proclayer.is_dirty(i): + vollog.warning( + f"Found malicious (dirty+exec) page at {hex(i)} !" + ) + # We do not attempt to find other dirty+exec pages once we have found one + ret = True + break + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug(f"Unable to translate address {hex(i)} : {excp}") + # Abort as it is likely that other addresses in the same range will also fail + ret = False + break return ret class qstr(objects.StructType): - def name_as_str(self) -> str: if self.has_member("len"): str_length = self.len + 1 # Maximum length should include null terminator else: str_length = 255 - try: ret = objects.utility.pointer_to_string(self.name, str_length) except (exceptions.InvalidAddressException, ValueError): ret = "" - return ret class dentry(objects.StructType): - def path(self) -> str: """Based on __dentry_path Linux kernel function""" reversed_path = [] dentry_seen = set() current_dentry = self - while (not current_dentry.is_root() and - current_dentry.vol.offset not in dentry_seen): + while ( + not current_dentry.is_root() + and current_dentry.vol.offset not in dentry_seen + ): parent = current_dentry.d_parent reversed_path.append(current_dentry.d_name.name_as_str()) dentry_seen.add(current_dentry.vol.offset) @@ -475,7 +701,6 @@ class dentry(objects.StructType): """ if self.vol.offset == old_dentry: return True - return self.d_ancestor(old_dentry) def d_ancestor(self, ancestor_dentry): @@ -487,19 +712,18 @@ class dentry(objects.StructType): dentry_seen = set() current_dentry = self - while (not current_dentry.is_root() and - current_dentry.vol.offset not in dentry_seen): + while ( + not current_dentry.is_root() + and current_dentry.vol.offset not in dentry_seen + ): if current_dentry.d_parent == ancestor_dentry.vol.offset: return current_dentry - dentry_seen.add(current_dentry.vol.offset) current_dentry = current_dentry.d_parent - return None class struct_file(objects.StructType): - def get_dentry(self) -> interfaces.objects.ObjectInterface: if self.has_member("f_dentry"): return self.f_dentry @@ -509,6 +733,7 @@ class struct_file(objects.StructType): raise AttributeError("Unable to find file -> dentry") def get_vfsmnt(self) -> interfaces.objects.ObjectInterface: + """Returns the fs (vfsmount) where this file is mounted""" if self.has_member("f_vfsmnt"): return self.f_vfsmnt elif self.has_member("f_path"): @@ -518,13 +743,14 @@ class struct_file(objects.StructType): class list_head(objects.StructType, collections.abc.Iterable): - - def to_list(self, - symbol_type: str, - member: str, - forward: bool = True, - sentinel: bool = True, - layer: Optional[str] = None) -> Iterator[interfaces.objects.ObjectInterface]: + def to_list( + self, + symbol_type: str, + member: str, + forward: bool = True, + sentinel: bool = True, + layer: Optional[str] = None, + ) -> Iterator[interfaces.objects.ObjectInterface]: """Returns an iterator of the entries in the list. Args: @@ -540,23 +766,26 @@ class list_head(objects.StructType, collections.abc.Iterable): """ layer = layer or self.vol.layer_name - relative_offset = self._context.symbol_space.get_type(symbol_type).relative_child_offset(member) + relative_offset = self._context.symbol_space.get_type( + symbol_type + ).relative_child_offset(member) - direction = 'prev' + direction = "prev" if forward: - direction = 'next' + direction = "next" try: link = getattr(self, direction).dereference() except exceptions.InvalidAddressException: - return - + return None if not sentinel: - yield self._context.object(symbol_type, layer, offset = self.vol.offset - relative_offset) - + yield self._context.object( + symbol_type, layer, offset=self.vol.offset - relative_offset + ) seen = {self.vol.offset} while link.vol.offset not in seen: - - obj = self._context.object(symbol_type, layer, offset = link.vol.offset - relative_offset) + obj = self._context.object( + symbol_type, layer, offset=link.vol.offset - relative_offset + ) yield obj seen.add(link.vol.offset) @@ -570,7 +799,6 @@ class list_head(objects.StructType, collections.abc.Iterable): class files_struct(objects.StructType): - def get_fds(self) -> interfaces.objects.ObjectInterface: if self.has_member("fdt"): return self.fdt.fd.dereference() @@ -589,7 +817,6 @@ class files_struct(objects.StructType): class mount(objects.StructType): - MNT_NOSUID = 0x01 MNT_NODEV = 0x02 MNT_NOEXEC = 0x04 @@ -636,16 +863,79 @@ class mount(objects.StructType): raise AttributeError("Unable to find mount -> mount flags") def get_mnt_parent(self): + """Gets the fs where we are mounted on + + Returns: + A mount pointer + """ return self.mnt_parent def get_mnt_mountpoint(self): + """Gets the dentry of the mountpoint + + Returns: + A dentry pointer + """ + return self.mnt_mountpoint + def get_parent_mount(self): + return self.mnt.get_parent_mount() + + def has_parent(self) -> bool: + """Checks if this mount has a parent + + Returns: + bool: 'True' if this mount has a parent + """ + return self.mnt_parent != self.vol.offset + + def get_vfsmnt_current(self): + """Returns the fs where we are mounted on + + Returns: + A 'vfsmount' + """ + return self.mnt + + def get_vfsmnt_parent(self): + """Gets the parent fs (vfsmount) to where it's mounted on + + Returns: + A 'vfsmount' + """ + + return self.get_mnt_parent().get_vfsmnt_current() + + def get_dentry_current(self): + """Returns the root of the mounted tree + + Returns: + A dentry pointer + """ + vfsmnt = self.get_vfsmnt_current() + dentry = vfsmnt.mnt_root + + return dentry + + def get_dentry_parent(self): + """Returns the parent root of the mounted tree + + Returns: + A dentry pointer + """ + + return self.get_mnt_parent().get_dentry_current() + def get_flags_access(self) -> str: return "ro" if self.get_mnt_flags() & self.MNT_READONLY else "rw" def get_flags_opts(self) -> Iterable[str]: - flags = [self.MNT_FLAGS[mntflag] for mntflag in self.MNT_FLAGS if mntflag & self.get_mnt_flags()] + flags = [ + self.MNT_FLAGS[mntflag] + for mntflag in self.MNT_FLAGS + if mntflag & self.get_mnt_flags() + ] return flags def is_shared(self) -> bool: @@ -660,20 +950,18 @@ class mount(objects.StructType): def get_devname(self) -> str: return utility.pointer_to_string(self.mnt_devname, count=255) - def has_parent(self) -> bool: - return self.vol.offset != self.mnt_parent - def get_dominating_id(self, root) -> int: """Get ID of closest dominating peer group having a representative under the given root.""" mnt_seen = set() current_mnt = self.mnt_master - while (current_mnt and - current_mnt.vol.offset != 0 and - current_mnt.vol.offset not in mnt_seen): + while ( + current_mnt + and current_mnt.vol.offset != 0 + and current_mnt.vol.offset not in mnt_seen + ): peer = current_mnt.get_peer_under_root(self.mnt_ns, root) if peer and peer.vol.offset != 0: return peer.mnt_group_id - mnt_seen.add(current_mnt.vol.offset) current_mnt = current_mnt.mnt_master return 0 @@ -685,14 +973,14 @@ class mount(objects.StructType): mnt_seen = set() current_mnt = self while current_mnt.vol.offset not in mnt_seen: - if current_mnt.mnt_ns == ns and current_mnt.is_path_reachable(current_mnt.mnt.mnt_root, root): + if current_mnt.mnt_ns == ns and current_mnt.is_path_reachable( + current_mnt.mnt.mnt_root, root + ): return current_mnt - mnt_seen.add(current_mnt.vol.offset) current_mnt = current_mnt.next_peer() if current_mnt.vol.offset == self.vol.offset: break - return None def is_path_reachable(self, current_dentry, root): @@ -701,44 +989,155 @@ class mount(objects.StructType): """ mnt_seen = set() current_mnt = self - while (current_mnt.mnt.vol.offset != root.mnt and - current_mnt.has_parent() and - current_mnt.vol.offset not in mnt_seen): - + while ( + current_mnt.mnt.vol.offset != root.mnt + and current_mnt.has_parent() + and current_mnt.vol.offset not in mnt_seen + ): current_dentry = current_mnt.mnt_mountpoint mnt_seen.add(current_mnt.vol.offset) current_mnt = current_mnt.mnt_parent - - return current_mnt.mnt.vol.offset == root.mnt and current_dentry.is_subdir(root.dentry) + return current_mnt.mnt.vol.offset == root.mnt and current_dentry.is_subdir( + root.dentry + ) def next_peer(self): table_name = self.vol.type_name.split(constants.BANG)[0] mount_struct = "{0}{1}mount".format(table_name, constants.BANG) - offset = self._context.symbol_space.get_type(mount_struct).relative_child_offset("mnt_share") + offset = self._context.symbol_space.get_type( + mount_struct + ).relative_child_offset("mnt_share") + + return self._context.object( + mount_struct, + self.vol.layer_name, + offset=self.mnt_share.next.vol.offset - offset, + ) - return self._context.object(mount_struct, self.vol.layer_name, offset=self.mnt_share.next.vol.offset - offset) class vfsmount(objects.StructType): - def is_valid(self): - return self.get_mnt_sb() != 0 and \ - self.get_mnt_root() != 0 and \ - self.get_mnt_parent() != 0 + return ( + self.get_mnt_sb() != 0 + and self.get_mnt_root() != 0 + and self.get_mnt_parent() != 0 + ) + + def _is_kernel_prior_to_struct_mount(self) -> bool: + """Helper to distinguish between kernels prior to version 3.3.8 that + lacked the 'mount' structure and later versions that have it. + + The 'mnt_parent' member was moved from struct 'vfsmount' to struct + 'mount' when the latter was introduced. + + Alternatively, vmlinux.has_type('mount') can be used here but it is faster. + + Returns: + bool: 'True' if the kernel + """ + + return self.has_member("mnt_parent") + + def is_equal(self, vfsmount_ptr) -> bool: + """Helper to make sure it is comparing two pointers to 'vfsmount'. + + Depending on the kernel version, the calling object (self) could be + a 'vfsmount \*' (<3.3.8) or a 'vfsmount' (>=3.3.8). This way we trust + in the framework "auto" dereferencing ability to assure that when we + reach this point 'self' will be a 'vfsmount' already and self.vol.offset + a 'vfsmount \*' and not a 'vfsmount \*\*'. The argument must be a 'vfsmount \*'. + Typically, it's called from do_get_path(). + + Args: + vfsmount_ptr (vfsmount \*): A pointer to a 'vfsmount' + + Raises: + exceptions.VolatilityException: If vfsmount_ptr is not a 'vfsmount \*' + + Returns: + bool: 'True' if the given argument points to the the same 'vfsmount' + as 'self'. + """ + if type(vfsmount_ptr) == objects.Pointer: + return self.vol.offset == vfsmount_ptr + else: + raise exceptions.VolatilityException( + "Unexpected argument type. It has to be a 'vfsmount *'" + ) def _get_real_mnt(self): - table_name = self.vol.type_name.split(constants.BANG)[0] - mount_struct = f"{table_name}{constants.BANG}mount" - offset = self._context.symbol_space.get_type(mount_struct).relative_child_offset("mnt") + """Gets the struct 'mount' containing this 'vfsmount'. - return self._context.object(mount_struct, self.vol.layer_name, offset = self.vol.offset - offset) + It should be only called from kernels >= 3.3.8 when 'struct mount' was introduced. + + Returns: + mount: the struct 'mount' containing this 'vfsmount'. + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + return linux.LinuxUtilities.container_of( + self.vol.offset, "mount", "mnt", vmlinux + ) + + def get_vfsmnt_current(self): + """Returns the current fs where we are mounted on + + Returns: + A vfsmount pointer + """ + return self.get_mnt_parent() + + def get_vfsmnt_parent(self): + """Gets the parent fs (vfsmount) to where it's mounted on + + Returns: + For kernels < 3.3.8: A vfsmount pointer + For kernels >= 3.3.8: A vfsmount object + """ + if self._is_kernel_prior_to_struct_mount(): + return self.get_mnt_parent() + else: + return self._get_real_mnt().get_vfsmnt_parent() + + def get_dentry_current(self): + """Returns the root of the mounted tree + + Returns: + A dentry pointer + """ + if self._is_kernel_prior_to_struct_mount(): + return self.get_mnt_mountpoint() + else: + return self._get_real_mnt().get_dentry_current() + + def get_dentry_parent(self): + """Returns the parent root of the mounted tree + + Returns: + A dentry pointer + """ + if self._is_kernel_prior_to_struct_mount(): + return self.get_mnt_mountpoint() + else: + return self._get_real_mnt().get_mnt_mountpoint() def get_mnt_parent(self): - if self.has_member("mnt_parent"): + """Gets the mnt_parent member. + + Returns: + For kernels < 3.3.8: A vfsmount pointer + For kernels >= 3.3.8: A mount pointer + """ + if self._is_kernel_prior_to_struct_mount(): return self.mnt_parent else: - return self._get_real_mnt().mnt_parent + return self._get_real_mnt().get_mnt_parent() def get_mnt_mountpoint(self): + """Gets the dentry of the mountpoint + + Returns: + A dentry pointer + """ if self.has_member("mnt_mountpoint"): return self.mnt_mountpoint else: @@ -747,23 +1146,57 @@ class vfsmount(objects.StructType): def get_mnt_root(self): return self.mnt_root + def has_parent(self) -> bool: + if self._is_kernel_prior_to_struct_mount(): + return self.mnt_parent != self.vol.offset + else: + return self._get_real_mnt().has_parent() + + def get_mnt_sb(self): + return self.mnt_sb + + def get_flags_access(self) -> str: + return "ro" if self.mnt_flags & mount.MNT_READONLY else "rw" + + def get_flags_opts(self) -> Iterable[str]: + flags = [ + mntflagtxt + for mntflag, mntflagtxt in mount.MNT_FLAGS.items() + if mntflag & self.mnt_flags != 0 + ] + return flags + + def get_mnt_flags(self): + return self.mnt_flags + + def is_shared(self) -> bool: + return self.get_mnt_flags() & mount.MNT_SHARED + + def is_unbindable(self) -> bool: + return self.get_mnt_flags() & mount.MNT_UNBINDABLE + + def is_slave(self) -> bool: + return self.mnt_master and self.mnt_master.vol.offset != 0 + + def get_devname(self) -> str: + return utility.pointer_to_string(self.mnt_devname, count=255) + class kobject(objects.StructType): - def reference_count(self): refcnt = self.kref.refcount - if self.has_member("counter"): + if refcnt.has_member("counter"): ret = refcnt.counter else: ret = refcnt.refs.counter - return ret + class mnt_namespace(objects.StructType): def get_inode(self): if self.has_member("proc_inum"): return self.proc_inum - elif self.ns.has_member("inum"): + elif self.has_member("ns") and self.ns.has_member("inum"): return self.ns.inum else: raise AttributeError("Unable to find mnt_namespace inode") @@ -774,6 +1207,462 @@ class mnt_namespace(objects.StructType): if not self._context.symbol_space.has_type(mnt_type): # Old kernels ~ 2.6 mnt_type = table_name + constants.BANG + "vfsmount" - for mount in self.list.to_list(mnt_type, "mnt_list"): yield mount + + +class net(objects.StructType): + def get_inode(self): + if self.has_member("proc_inum"): + # 3.8.13 <= kernel < 3.19.8 + return self.proc_inum + elif self.has_member("ns") and self.ns.has_member("inum"): + # kernel >= 3.19.8 + return self.ns.inum + else: + # kernel < 3.8.13 + raise AttributeError("Unable to find net_namespace inode") + + +class socket(objects.StructType): + def _get_vol_kernel(self): + symbol_table_arr = self.vol.type_name.split("!", 1) + symbol_table = symbol_table_arr[0] if len(symbol_table_arr) == 2 else None + + module_names = list( + self._context.modules.get_modules_by_symbol_tables(symbol_table) + ) + if not module_names: + raise ValueError(f"No module using the symbol table {symbol_table}") + kernel_module_name = module_names[0] + kernel = self._context.modules[kernel_module_name] + return kernel + + def get_inode(self): + try: + kernel = self._get_vol_kernel() + except ValueError: + return 0 + socket_alloc = linux.LinuxUtilities.container_of( + self.vol.offset, "socket_alloc", "socket", kernel + ) + vfs_inode = socket_alloc.vfs_inode + + return vfs_inode.i_ino + + def get_state(self): + socket_state_idx = self.state + if 0 <= socket_state_idx < len(SOCKET_STATES): + return SOCKET_STATES[socket_state_idx] + + +class sock(objects.StructType): + def get_family(self): + family_idx = self.__sk_common.skc_family + if 0 <= family_idx < len(SOCK_FAMILY): + return SOCK_FAMILY[family_idx] + + def get_type(self): + return SOCK_TYPES.get(self.sk_type, "") + + def get_inode(self): + if not self.sk_socket: + return 0 + return self.sk_socket.get_inode() + + def get_protocol(self): + return None + + def get_state(self): + # Return the generic socket state + if self.has_member("sk"): + return self.sk.sk_socket.get_state() + return self.sk_socket.get_state() + + +class unix_sock(objects.StructType): + def get_name(self): + if not self.addr: + return None + sockaddr_un = self.addr.name.cast("sockaddr_un") + saddr = str(utility.array_to_string(sockaddr_un.sun_path)) + return saddr + + def get_protocol(self): + return None + + def get_state(self): + """Return a string representing the sock state.""" + + # Unix socket states reuse (a subset) of the inet_sock states contants + if self.sk.get_type() == "STREAM": + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(TCP_STATES): + return TCP_STATES[state_idx] + else: + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_inode(self): + return self.sk.get_inode() + + +class inet_sock(objects.StructType): + def get_family(self): + family_idx = self.sk.__sk_common.skc_family + if 0 <= family_idx < len(SOCK_FAMILY): + return SOCK_FAMILY[family_idx] + + def get_protocol(self): + # If INET6 family and a proto is defined, we use that specific IPv6 protocol. + # Otherwise, we use the standard IP protocol. + protocol = IP_PROTOCOLS.get(self.sk.sk_protocol) + if self.get_family() == "AF_INET6": + protocol = IPV6_PROTOCOLS.get(self.sk.sk_protocol, protocol) + return protocol + + def get_state(self): + """Return a string representing the sock state.""" + + if self.sk.get_type() == "STREAM": + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(TCP_STATES): + return TCP_STATES[state_idx] + else: + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_src_port(self): + sport_le = getattr(self, "sport", getattr(self, "inet_sport", None)) + if sport_le is not None: + return socket_module.htons(sport_le) + + def get_dst_port(self): + sk_common = self.sk.__sk_common + if hasattr(sk_common, "skc_portpair"): + dport_le = sk_common.skc_portpair & 0xFFFF + elif hasattr(self, "dport"): + dport_le = self.dport + elif hasattr(self, "inet_dport"): + dport_le = self.inet_dport + elif hasattr(sk_common, "skc_dport"): + dport_le = sk_common.skc_dport + else: + return None + return socket_module.htons(dport_le) + + def get_src_addr(self): + sk_common = self.sk.__sk_common + family = sk_common.skc_family + if family == socket_module.AF_INET: + addr_size = 4 + if hasattr(self, "rcv_saddr"): + saddr = self.rcv_saddr + elif hasattr(self, "inet_rcv_saddr"): + saddr = self.inet_rcv_saddr + else: + saddr = sk_common.skc_rcv_saddr + elif family == socket_module.AF_INET6: + addr_size = 16 + saddr = self.pinet6.saddr + else: + return None + parent_layer = self._context.layers[self.vol.layer_name] + try: + addr_bytes = parent_layer.read(saddr.vol.offset, addr_size) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read socket src address from {saddr.vol.offset:#x}" + ) + return None + return socket_module.inet_ntop(family, addr_bytes) + + def get_dst_addr(self): + sk_common = self.sk.__sk_common + family = sk_common.skc_family + if family == socket_module.AF_INET: + if hasattr(self, "daddr") and self.daddr: + daddr = self.daddr + elif hasattr(self, "inet_daddr") and self.inet_daddr: + daddr = self.inet_daddr + else: + daddr = sk_common.skc_daddr + addr_size = 4 + elif family == socket_module.AF_INET6: + if hasattr(self.pinet6, "daddr"): + daddr = self.pinet6.daddr + else: + daddr = sk_common.skc_v6_daddr + addr_size = 16 + else: + return None + parent_layer = self._context.layers[self.vol.layer_name] + try: + addr_bytes = parent_layer.read(daddr.vol.offset, addr_size) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read socket dst address from {daddr.vol.offset:#x}" + ) + return None + return socket_module.inet_ntop(family, addr_bytes) + + +class netlink_sock(objects.StructType): + def get_protocol(self): + protocol_idx = self.sk.sk_protocol + if 0 <= protocol_idx < len(NETLINK_PROTOCOLS): + return NETLINK_PROTOCOLS[protocol_idx] + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + def get_portid(self): + if self.has_member("pid"): + # kernel < 3.7.10 + return self.pid + if self.has_member("portid"): + # kernel >= 3.7.10 + return self.portid + else: + raise AttributeError("Unable to find a source port id") + + def get_dst_portid(self): + if self.has_member("dst_pid"): + # kernel < 3.7.10 + return self.dst_pid + if self.has_member("dst_portid"): + # kernel >= 3.7.10 + return self.dst_portid + else: + raise AttributeError("Unable to find a destination port id") + + +class vsock_sock(objects.StructType): + def get_protocol(self): + # The protocol should always be 0 for vsocks + return None + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + +class packet_sock(objects.StructType): + def get_protocol(self): + eth_proto = socket_module.htons(self.num) + if eth_proto == 0: + return None + elif eth_proto in ETH_PROTOCOLS: + return ETH_PROTOCOLS[eth_proto] + else: + return f"0x{eth_proto:x}" + + def get_state(self): + # Return the generic socket state + return self.sk.sk_socket.get_state() + + +class bt_sock(objects.StructType): + def get_protocol(self): + type_idx = self.sk.sk_protocol + if 0 <= type_idx < len(BLUETOOTH_PROTOCOLS): + return BLUETOOTH_PROTOCOLS[type_idx] + + def get_state(self): + state_idx = self.sk.__sk_common.skc_state + if 0 <= state_idx < len(BLUETOOTH_STATES): + return BLUETOOTH_STATES[state_idx] + + +class xdp_sock(objects.StructType): + def get_protocol(self): + # The protocol should always be 0 for xdp_sock + return None + + def get_state(self): + # xdp_sock.state is an enum + return self.state.lookup() + + +class bpf_prog(objects.StructType): + def get_type(self): + # The program type was in `bpf_prog_aux::prog_type` from 3.18.140 to + # 4.1.52 before it was moved to `bpf_prog::type` + if self.has_member("type"): + # kernel >= 4.1.52 + return self.type + + if self.has_member("aux") and self.aux: + if self.aux.has_member("prog_type"): + # 3.18.140 <= kernel < 4.1.52 + return self.aux.prog_type + + # kernel < 3.18.140 + raise AttributeError("Unable to find the BPF type") + + +class cred(objects.StructType): + # struct cred was added in kernels 2.6.29 + def _get_cred_int_value(self, member: str) -> int: + """Helper to obtain the right cred member value for the current kernel. + + Args: + member (str): The requested cred member name to obtain its value + + Raises: + AttributeError: When the requested cred member doesn't exist + AttributeError: When the cred implementation is not supported. + + Returns: + int: The cred member value + """ + if not self.has_member(member): + raise AttributeError(f"struct cred doesn't have a '{member}' member") + + cred_val = self.member(member) + if hasattr(cred_val, "val"): + # From kernels 3.5.7 on it is a 'kuid_t' type + value = cred_val.val + elif isinstance(cred_val, objects.Integer): + # From at least 2.6.30 and until 3.5.7 it was a 'uid_t' type which was an 'unsigned int' + value = cred_val + else: + raise AttributeError("Kernel struct cred is not supported") + + return int(value) + + @property + def euid(self): + """Returns the effective user ID + + Returns: + int: the effective user ID value + """ + return self._get_cred_int_value("euid") + + +class kernel_cap_struct(objects.StructType): + # struct kernel_cap_struct exists from 2.1.92 <= kernels < 6.3 + @classmethod + def get_last_cap_value(cls) -> int: + """Returns the latest capability ID supported by the framework. + + Returns: + int: The latest capability ID supported by the framework. + """ + return len(CAPABILITIES) - 1 + + def get_kernel_cap_full(self) -> int: + """Return the maximum value allowed for this kernel for a capability + + Returns: + int: The capability full bitfield mask + """ + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + try: + cap_last_cap = vmlinux.object_from_symbol(symbol_name="cap_last_cap") + except exceptions.SymbolError: + # It should be a kernel < 3.2, let's use our list of capabilities + cap_last_cap = self.get_last_cap_value() + + return (1 << cap_last_cap + 1) - 1 + + @classmethod + def capabilities_to_string(cls, capabilities_bitfield: int) -> List[str]: + """Translates a capability bitfield to a list of capability strings. + + Args: + capabilities_bitfield (int): The capability bitfield value. + + Returns: + List[str]: A list of capability strings. + """ + + capabilities = [] + for bit, name in enumerate(CAPABILITIES): + if capabilities_bitfield & (1 << bit) != 0: + capabilities.append(name) + + return capabilities + + def get_capabilities(self) -> int: + """Returns the capability bitfield value + + Returns: + int: The capability bitfield value. + """ + + if not self.has_member("cap"): + raise exceptions.VolatilityException( + "Unsupported kernel capabilities implementation" + ) + + if isinstance(self.cap, objects.Array): + if len(self.cap) == 1: + # At least in the vanilla kernel, from 2.6.24 to 2.6.25 + # kernel_cap_struct::cap become a two elements array. + # However, in some distros or custom kernel can technically + # be _KERNEL_CAPABILITY_U32S = _LINUX_CAPABILITY_U32S_1 + # Leaving this code here for the sake of ensuring completeness. + cap_value = self.cap[0] + elif len(self.cap) == 2: + # In 2.6.25.x <= kernels < 6.3 kernel_cap_struct::cap is a two + # elements __u32 array that constitutes a 64bit bitfield. + cap_value = (self.cap[1] << 32) | self.cap[0] + else: + raise exceptions.VolatilityException( + "Unsupported kernel capabilities implementation" + ) + else: + # In kernels < 2.6.25.x kernel_cap_struct::cap is a __u32 + cap_value = self.cap + + return cap_value & self.get_kernel_cap_full() + + def enumerate_capabilities(self) -> List[str]: + """Returns the list of capability strings. + + Returns: + List[str]: The list of capability strings. + """ + capabilities_value = self.get_capabilities() + return self.capabilities_to_string(capabilities_value) + + def has_capability(self, capability: str) -> bool: + """Checks if the given capability string is enabled. + + Args: + capability (str): A string representing the capability i.e. dac_read_search + + Raises: + AttributeError: If the given capability is unknown to the framework. + + Returns: + bool: "True" if the given capability is enabled. + """ + if capability not in CAPABILITIES: + raise AttributeError(f"Unknown capability with name '{capability}'") + + cap_value = 1 << CAPABILITIES.index(capability) + return cap_value & self.get_capabilities() != 0 + + +class kernel_cap_t(kernel_cap_struct): + # In kernels 6.3 kernel_cap_struct became the kernel_cap_t typedef + def get_capabilities(self) -> int: + """Returns the capability bitfield value + + Returns: + int: The capability bitfield value. + """ + + if self.has_member("val"): + # In kernels >= 6.3 kernel_cap_t::val is a u64 + cap_value = self.val + else: + raise exceptions.VolatilityException( + "Unsupported kernel capabilities implementation" + ) + + return cap_value & self.get_kernel_cap_full() diff --git a/volatility3/framework/symbols/linux/extensions/bash.py b/volatility3/framework/symbols/linux/extensions/bash.py index 40fa2521d..29ecb4f76 100644 --- a/volatility3/framework/symbols/linux/extensions/bash.py +++ b/volatility3/framework/symbols/linux/extensions/bash.py @@ -9,7 +9,6 @@ from volatility3.framework.renderers import conversion class hist_entry(objects.StructType): - def is_valid(self): try: cmd = self.get_command() diff --git a/volatility3/framework/symbols/linux/extensions/elf.py b/volatility3/framework/symbols/linux/extensions/elf.py index 1277afe93..fe85b194f 100644 --- a/volatility3/framework/symbols/linux/extensions/elf.py +++ b/volatility3/framework/symbols/linux/extensions/elf.py @@ -3,42 +3,64 @@ # from typing import Dict, Tuple +import logging from volatility3.framework import constants -from volatility3.framework import objects, interfaces +from volatility3.framework import objects, interfaces, exceptions + +vollog = logging.getLogger(__name__) class elf(objects.StructType): - ''' + """ Class used to create elf objects. It overrides the typename to `Elf32_` or `Elf64_`, depending on the corresponding value on e_ident - ''' + """ - def __init__(self, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, size: int, - members: Dict[str, Tuple[int, interfaces.objects.Template]]) -> None: - - super().__init__(context = context, - type_name = type_name, - object_info = object_info, - size = size, - members = members) + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + size: int, + members: Dict[str, Tuple[int, interfaces.objects.Template]], + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + size=size, + members=members, + ) layer_name = self.vol.layer_name symbol_table_name = self.get_symbol_table_name() # We read the MAGIC: (0x0 to 0x4) 0x7f 0x45 0x4c 0x46 - magic = self._context.object(symbol_table_name + constants.BANG + "unsigned long", - layer_name = layer_name, - offset = object_info.offset) + try: + magic = self._context.object( + symbol_table_name + constants.BANG + "unsigned long", + layer_name=layer_name, + offset=object_info.offset, + ) + except ( + exceptions.PagedInvalidAddressException, + exceptions.InvalidAddressException, + ) as excp: + vollog.debug( + f"Unable to check magic bytes for ELF file at offset {hex(object_info.offset)} in layer {layer_name}: {excp}" + ) + return None # Check validity - if magic != 0x464c457f: + if magic != 0x464C457F: # e.g. ELF return None # We need to read the EI_CLASS (0x4 offset) - ei_class = self._context.object(symbol_table_name + constants.BANG + "unsigned char", - layer_name = layer_name, - offset = object_info.offset + 0x4) + ei_class = self._context.object( + symbol_table_name + constants.BANG + "unsigned char", + layer_name=layer_name, + offset=object_info.offset + 0x4, + ) if ei_class == 1: self._type_prefix = "Elf32_" @@ -48,19 +70,24 @@ class elf(objects.StructType): raise ValueError(f"Unsupported ei_class value {ei_class}") # Construct the full header - self._hdr = self._context.object(symbol_table_name + constants.BANG + self._type_prefix + "Ehdr", - layer_name = layer_name, - offset = object_info.offset) + self._hdr = self._context.object( + symbol_table_name + constants.BANG + self._type_prefix + "Ehdr", + layer_name=layer_name, + offset=object_info.offset, + ) self._offset = object_info.offset self._cached_symtab = None self._cached_strtab = None def is_valid(self): - ''' + """ Determine whether it is a valid object - ''' - return self._type_prefix is not None and self._hdr is not None + """ + if hasattr(self, "_type_prefix") and hasattr(self, "_hdr"): + return self._type_prefix is not None and self._hdr is not None + else: + return False def __getattr__(self, name): # Just redirect to the corresponding header @@ -71,17 +98,26 @@ class elf(objects.StructType): def __dir__(self): return self._hdr.__dir__() + [ - "get_program_headers", "is_valid", "get_section_headers", "get_symbols", "__dir__" + "get_program_headers", + "is_valid", + "get_section_headers", + "get_symbols", + "__dir__", ] def get_program_headers(self): program_headers = self._context.object( self.get_symbol_table_name() + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self._offset + self.e_phoff, - subtype = self._context.symbol_space.get_type(self.get_symbol_table_name() + constants.BANG + - self._type_prefix + "Phdr"), - count = self.e_phnum) + layer_name=self.vol.layer_name, + offset=self._offset + self.e_phoff, + subtype=self._context.symbol_space.get_type( + self.get_symbol_table_name() + + constants.BANG + + self._type_prefix + + "Phdr" + ), + count=self.e_phnum, + ) for prog_header in program_headers: prog_header.parent_e_type = self.e_type @@ -92,11 +128,16 @@ class elf(objects.StructType): def get_section_headers(self): section_headers = self._context.object( self.get_symbol_table_name() + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self._offset + self.e_shoff, - subtype = self._context.symbol_space.get_type(self.get_symbol_table_name() + constants.BANG + - self._type_prefix + "Shdr"), - count = self.e_shnum) + layer_name=self.vol.layer_name, + offset=self._offset + self.e_shoff, + subtype=self._context.symbol_space.get_type( + self.get_symbol_table_name() + + constants.BANG + + self._type_prefix + + "Shdr" + ), + count=self.e_shnum, + ) return section_headers def _find_symbols(self): @@ -107,7 +148,7 @@ class elf(objects.StructType): for phdr in self.get_program_headers(): try: # Find PT_DYNAMIC segment - if str(phdr.p_type.description) != 'PT_DYNAMIC': + if str(phdr.p_type.description) != "PT_DYNAMIC": continue except ValueError: # If the p_type value is outside the ones declared in the enumeration, an @@ -145,15 +186,20 @@ class elf(objects.StructType): self._find_symbols() if self._cached_symtab is None: - return + return None symtab_arr = self._context.object( self.get_symbol_table_name() + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self._cached_symtab, - subtype = self._context.symbol_space.get_type(self.get_symbol_table_name() + constants.BANG + - self._type_prefix + "Sym"), - count = self._cached_numsyms) + layer_name=self.vol.layer_name, + offset=self._cached_symtab, + subtype=self._context.symbol_space.get_type( + self.get_symbol_table_name() + + constants.BANG + + self._type_prefix + + "Sym" + ), + count=self._cached_numsyms, + ) for sym in symtab_arr: sym.cached_strtab = self._cached_strtab @@ -161,7 +207,7 @@ class elf(objects.StructType): class elf_sym(objects.StructType): - """ An elf symbol entry""" + """An elf symbol entry""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -179,13 +225,13 @@ class elf_sym(objects.StructType): addr = self._cached_strtab + self.st_name # Just get the first 255 characters, it should be enough for a symbol name - name_bytes = self._context.layers[self.vol.layer_name].read(addr, 255, pad = True) + name_bytes = self._context.layers[self.vol.layer_name].read(addr, 255, pad=True) if name_bytes: idx = name_bytes.find(b"\x00") if idx != -1: name_bytes = name_bytes[:idx] - return name_bytes.decode('utf-8', errors = 'ignore') + return name_bytes.decode("utf-8", errors="ignore") else: # If we cannot read the name from the address space, # we return None. @@ -193,7 +239,7 @@ class elf_sym(objects.StructType): class elf_phdr(objects.StructType): - """ An elf program header """ + """An elf program header""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -236,7 +282,7 @@ class elf_phdr(objects.StructType): def dynamic_sections(self): # sanity check try: - if str(self.p_type.description) != 'PT_DYNAMIC': + if str(self.p_type.description) != "PT_DYNAMIC": return None except ValueError: # If the value is outside the ones declared in the enumeration, an @@ -248,17 +294,19 @@ class elf_phdr(objects.StructType): symbol_table_name = self.get_symbol_table_name() - rtsize = self._context.symbol_space.get_type(symbol_table_name + \ - constants.BANG + \ - self._type_prefix + "Dyn").size + rtsize = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + self._type_prefix + "Dyn" + ).size for i in range(256): # use the real size idx = i * rtsize - dyn = self._context.object(symbol_table_name + constants.BANG + self._type_prefix + "Dyn", - layer_name = self.vol.layer_name, - offset = arr_start + idx) + dyn = self._context.object( + symbol_table_name + constants.BANG + self._type_prefix + "Dyn", + layer_name=self.vol.layer_name, + offset=arr_start + idx, + ) yield dyn @@ -266,4 +314,10 @@ class elf_phdr(objects.StructType): break -class_types = {'Elf': elf, 'Elf64_Phdr': elf_phdr, 'Elf32_Phdr': elf_phdr, 'Elf32_Sym': elf_sym, 'Elf64_Sym': elf_sym} +class_types = { + "Elf": elf, + "Elf64_Phdr": elf_phdr, + "Elf32_Phdr": elf_phdr, + "Elf32_Sym": elf_sym, + "Elf64_Sym": elf_sym, +} diff --git a/volatility3/framework/symbols/linux/xen.json b/volatility3/framework/symbols/linux/xen.json new file mode 100644 index 000000000..8e843e728 --- /dev/null +++ b/volatility3/framework/symbols/linux/xen.json @@ -0,0 +1,115 @@ +{ + "symbols": { + }, + "user_types": { + "xen_p2m": { + "fields":{ + "entries": { + "offset": 0, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long long" + } + } + } + }, + "kind": "struct", + "size": 8 + }, + "xen_pfn":{ + "fields":{ + "entries": { + "offset": 0, + "type": { + "count": 1, + "kind": "array", + "subtype": { + "kind": "base", + "name": "unsigned long long" + } + } + } + }, + "kind": "struct", + "size": 16 + }, + "xen_pfn_entry":{ + "fields":{ + "pfn":{ + "offset": 0, + "type": { + "kind": "base", + "name": "unsigned long long" + } + }, + "gmfn":{ + "offset": 8, + "type": { + "kind": "base", + "name": "unsigned long long" + } + } + }, + "kind": "struct", + "size": 16 + + } + }, + "enums": { + }, + "base_types": { + "unsigned char": { + "endian": "little", + "kind": "char", + "signed": false, + "size": 1 + }, + "unsigned short": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 2 + }, + "long": { + "endian": "little", + "kind": "int", + "signed": true, + "size": 4 + }, + "char": { + "endian": "little", + "kind": "char", + "signed": true, + "size": 1 + }, + "unsigned long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 4 + }, + "long long": { + "endian": "little", + "kind": "int", + "signed": true, + "size": 8 + }, + "unsigned long long": { + "endian": "little", + "kind": "int", + "signed": false, + "size": 8 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "ikelos-by-hand", + "datetime": "2023-01-09T00:51:00" + }, + "format": "6.1.0" + } +} diff --git a/volatility3/framework/symbols/mac/__init__.py b/volatility3/framework/symbols/mac/__init__.py index 7d094ea4f..bc98e5bdc 100644 --- a/volatility3/framework/symbols/mac/__init__.py +++ b/volatility3/framework/symbols/mac/__init__.py @@ -14,23 +14,24 @@ class MacKernelIntermedSymbols(intermed.IntermediateSymbolTable): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.set_type_class('proc', extensions.proc) - self.set_type_class('fileglob', extensions.fileglob) - self.set_type_class('vnode', extensions.vnode) - self.set_type_class('vm_map_entry', extensions.vm_map_entry) - self.set_type_class('vm_map_object', extensions.vm_map_object) - self.set_type_class('socket', extensions.socket) - self.set_type_class('inpcb', extensions.inpcb) - self.set_type_class('queue_entry', extensions.queue_entry) - self.set_type_class('ifnet', extensions.ifnet) - self.set_type_class('sockaddr_dl', extensions.sockaddr_dl) - self.set_type_class('sockaddr', extensions.sockaddr) - self.set_type_class('sysctl_oid', extensions.sysctl_oid) - self.set_type_class('kauth_scope', extensions.kauth_scope) + self.set_type_class("proc", extensions.proc) + self.set_type_class("fileglob", extensions.fileglob) + self.set_type_class("vnode", extensions.vnode) + self.set_type_class("vm_map_entry", extensions.vm_map_entry) + self.set_type_class("vm_map_object", extensions.vm_map_object) + self.set_type_class("socket", extensions.socket) + self.set_type_class("inpcb", extensions.inpcb) + self.set_type_class("queue_entry", extensions.queue_entry) + self.set_type_class("ifnet", extensions.ifnet) + self.set_type_class("sockaddr_dl", extensions.sockaddr_dl) + self.set_type_class("sockaddr", extensions.sockaddr) + self.set_type_class("sysctl_oid", extensions.sysctl_oid) + self.set_type_class("kauth_scope", extensions.kauth_scope) class MacUtilities(interfaces.configuration.VersionableInterface): """Class with multiple useful mac functions.""" + """ Version History: 1.1.0 -> added walk_list_head API @@ -41,24 +42,34 @@ class MacUtilities(interfaces.configuration.VersionableInterface): _required_framework_version = (2, 0, 0) @classmethod - def mask_mods_list(cls, context: interfaces.context.ContextInterface, layer_name: str, - mods: Iterator[Any]) -> List[Tuple[interfaces.objects.ObjectInterface, Any, Any]]: + def mask_mods_list( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + mods: Iterator[Any], + ) -> List[Tuple[interfaces.objects.ObjectInterface, Any, Any]]: """ A helper function to mask the starting and end address of kernel modules """ mask = context.layers[layer_name].address_mask - return [(objects.utility.array_to_string(mod.name), mod.address & mask, (mod.address & mask) + mod.size) - for mod in mods] + return [ + ( + objects.utility.array_to_string(mod.name), + mod.address & mask, + (mod.address & mask) + mod.size, + ) + for mod in mods + ] @classmethod def generate_kernel_handler_info( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - kernel, # ikelos - how to type this?? - mods_list: Iterator[Any]): - + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + kernel, # ikelos - how to type this?? + mods_list: Iterator[Any], + ): try: start_addr = kernel.object_from_symbol("vm_kernel_stext") except exceptions.SymbolError: @@ -74,12 +85,18 @@ class MacUtilities(interfaces.configuration.VersionableInterface): start_addr = start_addr & mask end_addr = end_addr & mask - return [("__kernel__", start_addr, end_addr)] + \ - MacUtilities.mask_mods_list(context, layer_name, mods_list) + return [("__kernel__", start_addr, end_addr)] + MacUtilities.mask_mods_list( + context, layer_name, mods_list + ) @classmethod - def lookup_module_address(cls, context: interfaces.context.ContextInterface, handlers: Iterator[Any], - target_address, kernel_module_name: str = None): + def lookup_module_address( + cls, + context: interfaces.context.ContextInterface, + handlers: Iterator[Any], + target_address, + kernel_module_name: str = None, + ): mod_name = "UNKNOWN" symbol_name = "N/A" @@ -92,19 +109,30 @@ class MacUtilities(interfaces.configuration.VersionableInterface): if start <= target_address <= end: mod_name = name if name == "__kernel__": - symbols = list(context.symbol_space.get_symbols_by_location(target_address - module_shift)) + symbols = list( + context.symbol_space.get_symbols_by_location( + target_address - module_shift + ) + ) if len(symbols) > 0: - symbol_name = str(symbols[0].split(constants.BANG)[1]) if constants.BANG in symbols[0] else \ - str(symbols[0]) + symbol_name = ( + str(symbols[0].split(constants.BANG)[1]) + if constants.BANG in symbols[0] + else str(symbols[0]) + ) break return mod_name, symbol_name @classmethod - def files_descriptors_for_process(cls, context: interfaces.context.ContextInterface, symbol_table_name: str, - task: interfaces.objects.ObjectInterface): + def files_descriptors_for_process( + cls, + context: interfaces.context.ContextInterface, + symbol_table_name: str, + task: interfaces.objects.ObjectInterface, + ): """Creates a generator for the file descriptors of a process Args: @@ -136,14 +164,16 @@ class MacUtilities(interfaces.configuration.VersionableInterface): if num_fds > 4096: num_fds = 1024 - file_type = symbol_table_name + constants.BANG + 'fileproc' + file_type = symbol_table_name + constants.BANG + "fileproc" try: table_addr = task.p_fd.fd_ofiles.dereference() except exceptions.InvalidAddressException: - return + return None - fds = objects.utility.array_of_pointers(table_addr, count = num_fds, subtype = file_type, context = context) + fds = objects.utility.array_of_pointers( + table_addr, count=num_fds, subtype=file_type, context=context + ) for fd_num, f in enumerate(fds): if f != 0: @@ -152,7 +182,7 @@ class MacUtilities(interfaces.configuration.VersionableInterface): except exceptions.InvalidAddressException: continue - if ftype == 'VNODE': + if ftype == "VNODE": vnode = f.f_fglob.fg_data.dereference().cast("vnode") path = vnode.full_path() elif ftype: @@ -161,18 +191,20 @@ class MacUtilities(interfaces.configuration.VersionableInterface): yield f, path, fd_num @classmethod - def _walk_iterable(cls, - queue: interfaces.objects.ObjectInterface, - list_head_member: str, - list_next_member: str, - next_member: str, - max_elements: int = 4096) -> Iterable[interfaces.objects.ObjectInterface]: + def _walk_iterable( + cls, + queue: interfaces.objects.ObjectInterface, + list_head_member: str, + list_next_member: str, + next_member: str, + max_elements: int = 4096, + ) -> Iterable[interfaces.objects.ObjectInterface]: seen: Set[int] = set() try: - current = queue.member(attr = list_head_member) + current = queue.member(attr=list_head_member) except exceptions.InvalidAddressException: - return + return None while current: if current.vol.offset in seen: @@ -187,33 +219,42 @@ class MacUtilities(interfaces.configuration.VersionableInterface): yield current try: - current = current.member(attr = next_member).member(attr = list_next_member) + current = current.member(attr=next_member).member(attr=list_next_member) except exceptions.InvalidAddressException: break @classmethod - def walk_tailq(cls, - queue: interfaces.objects.ObjectInterface, - next_member: str, - max_elements: int = 4096) -> Iterable[interfaces.objects.ObjectInterface]: - - for element in cls._walk_iterable(queue, "tqh_first", "tqe_next", next_member, max_elements): + def walk_tailq( + cls, + queue: interfaces.objects.ObjectInterface, + next_member: str, + max_elements: int = 4096, + ) -> Iterable[interfaces.objects.ObjectInterface]: + for element in cls._walk_iterable( + queue, "tqh_first", "tqe_next", next_member, max_elements + ): yield element @classmethod - def walk_list_head(cls, - queue: interfaces.objects.ObjectInterface, - next_member: str, - max_elements: int = 4096) -> Iterable[interfaces.objects.ObjectInterface]: - - for element in cls._walk_iterable(queue, "lh_first", "le_next", next_member, max_elements): + def walk_list_head( + cls, + queue: interfaces.objects.ObjectInterface, + next_member: str, + max_elements: int = 4096, + ) -> Iterable[interfaces.objects.ObjectInterface]: + for element in cls._walk_iterable( + queue, "lh_first", "le_next", next_member, max_elements + ): yield element @classmethod - def walk_slist(cls, - queue: interfaces.objects.ObjectInterface, - next_member: str, - max_elements: int = 4096) -> Iterable[interfaces.objects.ObjectInterface]: - - for element in cls._walk_iterable(queue, "slh_first", "sle_next", next_member, max_elements): + def walk_slist( + cls, + queue: interfaces.objects.ObjectInterface, + next_member: str, + max_elements: int = 4096, + ) -> Iterable[interfaces.objects.ObjectInterface]: + for element in cls._walk_iterable( + queue, "slh_first", "sle_next", next_member, max_elements + ): yield element diff --git a/volatility3/framework/symbols/mac/extensions/__init__.py b/volatility3/framework/symbols/mac/extensions/__init__.py index 94045d2e7..bf0b3d775 100644 --- a/volatility3/framework/symbols/mac/extensions/__init__.py +++ b/volatility3/framework/symbols/mac/extensions/__init__.py @@ -1,25 +1,25 @@ # 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 Generator, Iterable, Optional, Set, Tuple -import logging - -from volatility3.framework import constants, objects, renderers -from volatility3.framework import exceptions, interfaces +from volatility3.framework import constants, exceptions, interfaces, objects from volatility3.framework.objects import utility from volatility3.framework.renderers import conversion from volatility3.framework.symbols import generic vollog = logging.getLogger(__name__) -class proc(generic.GenericIntelProcess): +class proc(generic.GenericIntelProcess): def get_task(self): return self.task.dereference().cast("task") - def add_process_layer(self, config_prefix: str = None, preferred_name: str = None) -> Optional[str]: + def add_process_layer( + self, config_prefix: str = None, preferred_name: str = None + ) -> Optional[str]: """Constructs a new layer based on the process's DTB. Returns the name of the Layer or None. @@ -27,42 +27,53 @@ class proc(generic.GenericIntelProcess): parent_layer = self._context.layers[self.vol.layer_name] if not isinstance(parent_layer, interfaces.layers.TranslationLayerInterface): - raise TypeError("Parent layer is not a translation layer, unable to construct process layer") + raise TypeError( + "Parent layer is not a translation layer, unable to construct process layer" + ) try: dtb = self.get_task().map.pmap.pm_cr3 except exceptions.InvalidAddressException: + # Bail out because we couldn't find the DTB return None if preferred_name is None: preferred_name = self.vol.layer_name + f"_Process{self.p_pid}" # Add the constructed layer and return the name - return self._add_process_layer(self._context, dtb, config_prefix, preferred_name) + return self._add_process_layer( + self._context, dtb, config_prefix, preferred_name + ) def get_map_iter(self) -> Iterable[interfaces.objects.ObjectInterface]: try: task = self.get_task() - except exceptions.InvalidAddressException: - return - - try: current_map = task.map.hdr.links.next except exceptions.InvalidAddressException: - return + return None seen: Set[int] = set() for i in range(task.map.hdr.nentries): - if (not current_map or - current_map.vol.offset in seen or - not self._context.layers[task.vol.native_layer_name].is_valid(current_map.dereference().vol.offset, current_map.dereference().vol.size)): - - vollog.log(constants.LOGLEVEL_VVV, "Breaking process maps iteration due to invalid state.") + if ( + not current_map + or current_map.vol.offset in seen + or not self._context.layers[task.vol.native_layer_name].is_valid( + current_map.dereference().vol.offset, + current_map.dereference().vol.size, + ) + ): + vollog.log( + constants.LOGLEVEL_VVV, + "Breaking process maps iteration due to invalid state.", + ) break # ZP_POISON value used to catch programming errors - if current_map.links.start == 0xdeadbeefdeadbeef or current_map.links.end == 0xdeadbeefdeadbeef: + if ( + current_map.links.start == 0xDEADBEEFDEADBEEF + or current_map.links.end == 0xDEADBEEFDEADBEEF + ): break yield current_map @@ -75,11 +86,12 @@ class proc(generic.GenericIntelProcess): # the fix for linux was to call int() so that we were not returning vol objects. # I call int() on these and the code works nearly 1-1 with the linux one so I am very confused ###### - def get_process_memory_sections(self, - context: interfaces.context.ContextInterface, - config_prefix: str, - rw_no_file: bool = False) -> \ - Generator[Tuple[int, int], None, None]: + def get_process_memory_sections( + self, + context: interfaces.context.ContextInterface, + config_prefix: str, + rw_no_file: bool = False, + ) -> Generator[Tuple[int, int], None, None]: """Returns a list of sections based on the memory manager's view of this task's virtual memory.""" for vma in self.get_map_iter(): @@ -87,7 +99,10 @@ class proc(generic.GenericIntelProcess): end = int(vma.links.end) if rw_no_file: - if vma.get_perms() != "rw" or vma.get_path(context, config_prefix) != "": + if ( + vma.get_perms() != "rw" + or vma.get_path(context, config_prefix) != "" + ): if vma.get_special_path() != "[heap]": continue @@ -95,17 +110,14 @@ class proc(generic.GenericIntelProcess): class fileglob(objects.StructType): - def get_fg_type(self): ret = None if self.has_member("fg_type"): ret = self.fg_type elif self.fg_ops != 0: - try: + with contextlib.suppress(exceptions.InvalidAddressException): ret = self.fg_ops.fo_type - except exceptions.InvalidAddressException: - pass if ret: ret = str(ret.description).replace("DTYPE_", "") @@ -114,7 +126,6 @@ class fileglob(objects.StructType): class vm_map_object(objects.StructType): - def get_map_object(self): if self.has_member("vm_object"): return self.vm_object @@ -125,31 +136,38 @@ class vm_map_object(objects.StructType): class vnode(objects.StructType): - def _do_calc_path(self, ret, vnodeobj, vname): if vnodeobj is None: - return + return None if vname: try: ret.append(utility.pointer_to_string(vname, 255)) except exceptions.InvalidAddressException: - return + return None if int(vnodeobj.v_flag) & 0x000001 != 0 and int(vnodeobj.v_mount) != 0: if int(vnodeobj.v_mount.mnt_vnodecovered) != 0: - self._do_calc_path(ret, vnodeobj.v_mount.mnt_vnodecovered, vnodeobj.v_mount.mnt_vnodecovered.v_name) + self._do_calc_path( + ret, + vnodeobj.v_mount.mnt_vnodecovered, + vnodeobj.v_mount.mnt_vnodecovered.v_name, + ) else: try: parent = vnodeobj.v_parent parent_name = parent.v_name except exceptions.InvalidAddressException: - return + return None self._do_calc_path(ret, parent, parent_name) def full_path(self): - if self.v_flag & 0x000001 != 0 and self.v_mount != 0 and self.v_mount.mnt_flag & 0x00004000 != 0: + if ( + self.v_flag & 0x000001 != 0 + and self.v_mount != 0 + and self.v_mount.mnt_flag & 0x00004000 != 0 + ): ret = b"/" else: elements = [] @@ -169,7 +187,6 @@ class vnode(objects.StructType): class vm_map_entry(objects.StructType): - def is_suspicious(self, context, config_prefix): """Flags memory regions that are mapped rwx or that map an executable not back from a file on disk.""" @@ -189,7 +206,7 @@ class vm_map_entry(objects.StructType): permask = "rwx" perms = "" - for (ctr, i) in enumerate([1, 3, 5]): + for ctr, i in enumerate([1, 3, 5]): if (self.protection & i) == i: perms = perms + permask[ctr] else: @@ -201,7 +218,7 @@ class vm_map_entry(objects.StructType): if self.has_member("alias"): ret = int(self.alias) else: - ret = int(self.vme_offset) & 0xfff + ret = int(self.vme_offset) & 0xFFF return ret @@ -302,9 +319,11 @@ class vm_map_entry(objects.StructType): break if found: - vpager = context.object(config_prefix + constants.BANG + "vnode_pager", - layer_name = vnode_object.vol.native_layer_name, - offset = vnode_object.pager) + vpager = context.object( + config_prefix + constants.BANG + "vnode_pager", + layer_name=vnode_object.vol.native_layer_name, + offset=vnode_object.pager, + ) ret = vpager.vnode_handle else: ret = None @@ -313,7 +332,6 @@ class vm_map_entry(objects.StructType): class socket(objects.StructType): - def get_inpcb(self): try: ret = self.so_pcb.dereference().cast("inpcb") @@ -371,10 +389,20 @@ class socket(objects.StructType): class inpcb(objects.StructType): - def get_tcp_state(self): - tcp_states = ("CLOSED", "LISTEN", "SYN_SENT", "SYN_RECV", "ESTABLISHED", "CLOSE_WAIT", "FIN_WAIT1", "CLOSING", - "LAST_ACK", "FIN_WAIT2", "TIME_WAIT") + tcp_states = ( + "CLOSED", + "LISTEN", + "SYN_SENT", + "SYN_RECV", + "ESTABLISHED", + "CLOSE_WAIT", + "FIN_WAIT1", + "CLOSING", + "LAST_ACK", + "FIN_WAIT2", + "TIME_WAIT", + ) try: tcpcb = self.inp_ppcb.dereference().cast("tcpcb") @@ -408,14 +436,18 @@ class inpcb(objects.StructType): def get_ipv6_info(self): try: - lip = self.inp_dependladdr.inp6_local.member(attr = '__u6_addr').member(attr = '__u6_addr32') + lip = self.inp_dependladdr.inp6_local.member(attr="__u6_addr").member( + attr="__u6_addr32" + ) except exceptions.InvalidAddressException: return None lport = self.inp_lport try: - rip = self.inp_dependfaddr.inp6_foreign.member(attr = '__u6_addr').member(attr = '__u6_addr32') + rip = self.inp_dependfaddr.inp6_foreign.member(attr="__u6_addr").member( + attr="__u6_addr32" + ) except exceptions.InvalidAddressException: return None @@ -425,12 +457,13 @@ class inpcb(objects.StructType): class queue_entry(objects.StructType): - - def walk_list(self, - list_head: interfaces.objects.ObjectInterface, - member_name: str, - type_name: str, - max_size: int = 4096) -> Iterable[interfaces.objects.ObjectInterface]: + def walk_list( + self, + list_head: interfaces.objects.ObjectInterface, + member_name: str, + type_name: str, + max_size: int = 4096, + ) -> Iterable[interfaces.objects.ObjectInterface]: """ Walks a queue in a smear-aware and smear-resistant manner @@ -455,8 +488,8 @@ class queue_entry(objects.StructType): seen = set() - for attr in ['next', 'prev']: - try: + for attr in ["next", "prev"]: + with contextlib.suppress(exceptions.InvalidAddressException): n = getattr(self, attr).dereference().cast(type_name) while n is not None and n.vol.offset != list_head: @@ -469,16 +502,16 @@ class queue_entry(objects.StructType): yielded = yielded + 1 if yielded == max_size: - return + return None - n = getattr(n.member(attr = member_name), attr).dereference().cast(type_name) - - except exceptions.InvalidAddressException: - pass + n = ( + getattr(n.member(attr=member_name), attr) + .dereference() + .cast(type_name) + ) class ifnet(objects.StructType): - def sockaddr_dl(self): if self.has_member("if_lladdr"): try: @@ -487,7 +520,9 @@ class ifnet(objects.StructType): val = None else: try: - val = self.if_addrhead.tqh_first.ifa_addr.dereference().cast("sockaddr_dl") + val = self.if_addrhead.tqh_first.ifa_addr.dereference().cast( + "sockaddr_dl" + ) except exceptions.InvalidAddressException: val = None @@ -496,7 +531,6 @@ class ifnet(objects.StructType): # this is used for MAC addresses class sockaddr_dl(objects.StructType): - def __str__(self): ret = "" @@ -520,7 +554,6 @@ class sockaddr_dl(objects.StructType): class sockaddr(objects.StructType): - def get_address(self): ip = "" @@ -531,7 +564,9 @@ class sockaddr(objects.StructType): elif family == 30: # AF_INET6 addr_in6 = self.cast("sockaddr_in6") - ip = conversion.convert_ipv6(addr_in6.sin6_addr.member(attr = "__u6_addr").member(attr = "__u6_addr32")) + ip = conversion.convert_ipv6( + addr_in6.sin6_addr.member(attr="__u6_addr").member(attr="__u6_addr32") + ) elif family == 18: # AF_LINK addr_dl = self.cast("sockaddr_dl") @@ -541,7 +576,6 @@ class sockaddr(objects.StructType): class sysctl_oid(objects.StructType): - def get_perms(self) -> str: """ Returns the actions allowed on the node @@ -559,7 +593,7 @@ class sysctl_oid(objects.StructType): checks = [0x80000000, 0x40000000, 0x00800000] perms = ["R", "W", "L"] - for (i, c) in enumerate(checks): + for i, c in enumerate(checks): if c & self.oid_kind: ret = ret + perms[i] else: @@ -584,9 +618,15 @@ class sysctl_oid(objects.StructType): Based on sysctl_sysctl_debug_dump_node """ - types = {1: 'CTLTYPE_NODE', 2: 'CTLTYPE_INT', 3: 'CTLTYPE_STRING', 4: 'CTLTYPE_QUAD', 5: 'CTLTYPE_OPAQUE'} + types = { + 1: "CTLTYPE_NODE", + 2: "CTLTYPE_INT", + 3: "CTLTYPE_STRING", + 4: "CTLTYPE_QUAD", + 5: "CTLTYPE_OPAQUE", + } - ctltype = self.oid_kind & 0xf + ctltype = self.oid_kind & 0xF if 0 < ctltype < 6: ret = types[ctltype] @@ -597,7 +637,6 @@ class sysctl_oid(objects.StructType): class kauth_scope(objects.StructType): - def get_listeners(self): for listener in self.ks_listeners: if listener != 0 and listener.kll_callback != 0: diff --git a/volatility3/framework/symbols/metadata.py b/volatility3/framework/symbols/metadata.py index 7cde686ee..61947be69 100644 --- a/volatility3/framework/symbols/metadata.py +++ b/volatility3/framework/symbols/metadata.py @@ -2,7 +2,7 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from typing import Optional, Tuple +from typing import Optional, Tuple, Union from volatility3.framework import interfaces @@ -11,11 +11,13 @@ class WindowsMetadata(interfaces.symbols.MetadataInterface): """Class to handle the metadata from a Windows symbol table.""" @property - def pe_version(self) -> Optional[Tuple]: - build = self._json_data.get('pe', {}).get('build', None) - revision = self._json_data.get('pe', {}).get('revision', None) - minor = self._json_data.get('pe', {}).get('minor', None) - major = self._json_data.get('pe', {}).get('major', None) + def pe_version( + self, + ) -> Optional[Union[Tuple[int, int, int], Tuple[int, int, int, int]]]: + build = self._json_data.get("pe", {}).get("build", None) + revision = self._json_data.get("pe", {}).get("revision", None) + minor = self._json_data.get("pe", {}).get("minor", None) + major = self._json_data.get("pe", {}).get("major", None) if revision is None or minor is None or major is None: return None if build is None: @@ -30,12 +32,12 @@ class WindowsMetadata(interfaces.symbols.MetadataInterface): @property def pdb_guid(self) -> Optional[str]: - return self._json_data.get('pdb', {}).get('GUID', None) + return self._json_data.get("pdb", {}).get("GUID", None) @property def pdb_age(self) -> Optional[int]: - return self._json_data.get('pdb', {}).get('age', None) + return self._json_data.get("pdb", {}).get("age", None) class LinuxMetadata(interfaces.symbols.MetadataInterface): - """Class to handle the etadata from a Linux symbol table.""" + """Class to handle the metadata from a Linux symbol table.""" diff --git a/volatility3/framework/symbols/native.py b/volatility3/framework/symbols/native.py index d9833e26d..7c3e1b312 100644 --- a/volatility3/framework/symbols/native.py +++ b/volatility3/framework/symbols/native.py @@ -21,7 +21,8 @@ class NativeTable(interfaces.symbols.NativeTableInterface): self._overrides[native_type] = native_class # Create this once early, because it may get used a lot self._types = set(self._native_dictionary).union( - {'enum', 'array', 'bitfield', 'void', 'string', 'bytes', 'function'}) + {"enum", "array", "bitfield", "void", "string", "bytes", "function"} + ) def get_type_class(self, name: str) -> Type[interfaces.objects.ObjectInterface]: ntype, _ = self._native_dictionary.get(name, (objects.Integer, None)) @@ -45,62 +46,71 @@ class NativeTable(interfaces.symbols.NativeTableInterface): if constants.BANG in type_name: name_split = type_name.split(constants.BANG) if len(name_split) > 2: - raise ValueError(f"SymbolName cannot contain multiple {constants.BANG} separators") + raise ValueError( + f"SymbolName cannot contain multiple {constants.BANG} separators" + ) table_name, type_name = name_split prefix = table_name + constants.BANG additional: Dict[str, Any] = {} obj: Optional[Type[interfaces.objects.ObjectInterface]] = None - if type_name == 'void' or type_name == 'function': + if type_name == "void" or type_name == "function": obj = objects.Void - elif type_name == 'array': + elif type_name == "array": obj = objects.Array - additional = {"count": 0, "subtype": self.get_type('void')} - elif type_name == 'enum': + additional = {"count": 0, "subtype": self.get_type("void")} + elif type_name == "enum": obj = objects.Enumeration - additional = {"base_type": self.get_type('void'), "choices": {}} - elif type_name == 'bitfield': + additional = {"base_type": self.get_type("void"), "choices": {}} + elif type_name == "bitfield": obj = objects.BitField - additional = {"start_bit": 0, "end_bit": 0, "base_type": self.get_type('void')} - elif type_name == 'string': + additional = { + "start_bit": 0, + "end_bit": 0, + "base_type": self.get_type("void"), + } + elif type_name == "string": obj = objects.String additional = {"max_length": 0} - elif type_name == 'bytes': + elif type_name == "bytes": obj = objects.Bytes additional = {"length": 0} if obj is not None: - return objects.templates.ObjectTemplate(obj, type_name = prefix + type_name, **additional) + return objects.templates.ObjectTemplate( + obj, type_name=prefix + type_name, **additional + ) _native_type, native_format = self._native_dictionary[type_name] - if type_name == 'pointer': - additional = {'subtype': self.get_type('void')} + if type_name == "pointer": + additional = {"subtype": self.get_type("void")} return objects.templates.ObjectTemplate( self.get_type_class(type_name), # pylint: disable=W0142 - type_name = prefix + type_name, - data_format = objects.DataFormatInfo(*native_format), - **additional) + type_name=prefix + type_name, + data_format=objects.DataFormatInfo(*native_format), + **additional, + ) std_ctypes = { - 'int': (objects.Integer, (4, "little", True)), - 'long': (objects.Integer, (4, "little", True)), - 'unsigned long': (objects.Integer, (4, "little", False)), - 'unsigned int': (objects.Integer, (4, "little", False)), - 'char': (objects.Integer, (1, "little", True)), - 'byte': (objects.Bytes, (1, "little", True)), - 'unsigned char': (objects.Integer, (1, "little", False)), - 'unsigned short int': (objects.Integer, (2, "little", False)), - 'unsigned short': (objects.Integer, (2, "little", False)), - 'unsigned be short': (objects.Integer, (2, "big", False)), - 'short': (objects.Integer, (2, "little", True)), - 'long long': (objects.Integer, (8, "little", True)), - 'unsigned long long': (objects.Integer, (8, "little", True)), - 'float': (objects.Float, (4, "little", True)), - 'double': (objects.Float, (8, "little", True)), - 'wchar': (objects.Integer, (2, "little", False)) + "int": (objects.Integer, (4, "little", True)), + "long": (objects.Integer, (4, "little", True)), + "unsigned long": (objects.Integer, (4, "little", False)), + "unsigned int": (objects.Integer, (4, "little", False)), + "char": (objects.Integer, (1, "little", True)), + "byte": (objects.Bytes, (1, "little", True)), + "unsigned char": (objects.Integer, (1, "little", False)), + "unsigned short int": (objects.Integer, (2, "little", False)), + "unsigned short": (objects.Integer, (2, "little", False)), + "unsigned be short": (objects.Integer, (2, "big", False)), + "short": (objects.Integer, (2, "little", True)), + "long long": (objects.Integer, (8, "little", True)), + "unsigned long long": (objects.Integer, (8, "little", True)), + "float": (objects.Float, (4, "little", True)), + "double": (objects.Float, (8, "little", True)), + "wchar": (objects.Integer, (2, "little", False)), } native_types = std_ctypes.copy() -native_types['pointer'] = (objects.Pointer, (4, "little", False)) +native_types["pointer"] = (objects.Pointer, (4, "little", False)) x86NativeTable = NativeTable("native", native_types) -native_types['pointer'] = (objects.Pointer, (8, "little", False)) +native_types["pointer"] = (objects.Pointer, (8, "little", False)) x64NativeTable = NativeTable("native", native_types) diff --git a/volatility3/framework/symbols/windows/__init__.py b/volatility3/framework/symbols/windows/__init__.py index 899b89dc2..abf9f6da3 100755 --- a/volatility3/framework/symbols/windows/__init__.py +++ b/volatility3/framework/symbols/windows/__init__.py @@ -1,64 +1,61 @@ # 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 contextlib from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import extensions -from volatility3.framework.symbols.windows.extensions import registry, pool, pe +from volatility3.framework.symbols.windows.extensions import pe, pool, registry class WindowsKernelIntermedSymbols(intermed.IntermediateSymbolTable): - def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # Set-up windows specific types - self.set_type_class('_ETHREAD', extensions.ETHREAD) - self.set_type_class('_KTHREAD', extensions.KTHREAD) - self.set_type_class('_LIST_ENTRY', extensions.LIST_ENTRY) - self.set_type_class('_EPROCESS', extensions.EPROCESS) - self.set_type_class('_UNICODE_STRING', extensions.UNICODE_STRING) - self.set_type_class('_EX_FAST_REF', extensions.EX_FAST_REF) - self.set_type_class('_TOKEN', extensions.TOKEN) - self.set_type_class('_OBJECT_HEADER', pool.OBJECT_HEADER) - self.set_type_class('_FILE_OBJECT', extensions.FILE_OBJECT) - self.set_type_class('_DEVICE_OBJECT', extensions.DEVICE_OBJECT) - self.set_type_class('_CM_KEY_BODY', registry.CM_KEY_BODY) - self.set_type_class('_CMHIVE', registry.CMHIVE) - self.set_type_class('_CM_KEY_NODE', registry.CM_KEY_NODE) - self.set_type_class('_CM_KEY_VALUE', registry.CM_KEY_VALUE) - self.set_type_class('_HMAP_ENTRY', registry.HMAP_ENTRY) - self.set_type_class('_MMVAD_SHORT', extensions.MMVAD_SHORT) - self.set_type_class('_MMVAD', extensions.MMVAD) - self.set_type_class('_KSYSTEM_TIME', extensions.KSYSTEM_TIME) - self.set_type_class('_KMUTANT', extensions.KMUTANT) - self.set_type_class('_DRIVER_OBJECT', extensions.DRIVER_OBJECT) - self.set_type_class('_OBJECT_SYMBOLIC_LINK', extensions.OBJECT_SYMBOLIC_LINK) - self.set_type_class('_CONTROL_AREA', extensions.CONTROL_AREA) - self.set_type_class('_SHARED_CACHE_MAP', extensions.SHARED_CACHE_MAP) - self.set_type_class('_VACB', extensions.VACB) - self.set_type_class('_POOL_TRACKER_BIG_PAGES', pool.POOL_TRACKER_BIG_PAGES) - self.set_type_class('_IMAGE_DOS_HEADER', pe.IMAGE_DOS_HEADER) - + self.set_type_class("_ETHREAD", extensions.ETHREAD) + self.set_type_class("_KTHREAD", extensions.KTHREAD) + self.set_type_class("_LIST_ENTRY", extensions.LIST_ENTRY) + self.set_type_class("_EPROCESS", extensions.EPROCESS) + self.set_type_class("_UNICODE_STRING", extensions.UNICODE_STRING) + self.set_type_class("_EX_FAST_REF", extensions.EX_FAST_REF) + self.set_type_class("_TOKEN", extensions.TOKEN) + self.set_type_class("_OBJECT_HEADER", pool.OBJECT_HEADER) + self.set_type_class("_FILE_OBJECT", extensions.FILE_OBJECT) + self.set_type_class("_DEVICE_OBJECT", extensions.DEVICE_OBJECT) + self.set_type_class("_CM_KEY_BODY", registry.CM_KEY_BODY) + self.set_type_class("_CMHIVE", registry.CMHIVE) + self.set_type_class("_CM_KEY_NODE", registry.CM_KEY_NODE) + self.set_type_class("_CM_KEY_VALUE", registry.CM_KEY_VALUE) + self.set_type_class("_HMAP_ENTRY", registry.HMAP_ENTRY) + self.set_type_class("_MMVAD_SHORT", extensions.MMVAD_SHORT) + self.set_type_class("_MMVAD", extensions.MMVAD) + self.set_type_class("_KSYSTEM_TIME", extensions.KSYSTEM_TIME) + self.set_type_class("_KMUTANT", extensions.KMUTANT) + self.set_type_class("_DRIVER_OBJECT", extensions.DRIVER_OBJECT) + self.set_type_class("_OBJECT_SYMBOLIC_LINK", extensions.OBJECT_SYMBOLIC_LINK) + self.set_type_class("_CONTROL_AREA", extensions.CONTROL_AREA) + self.set_type_class("_SHARED_CACHE_MAP", extensions.SHARED_CACHE_MAP) + self.set_type_class("_VACB", extensions.VACB) + self.set_type_class("_POOL_TRACKER_BIG_PAGES", pool.POOL_TRACKER_BIG_PAGES) + self.set_type_class("_IMAGE_DOS_HEADER", pe.IMAGE_DOS_HEADER) + # Might not necessarily defined in every version of windows - self.optional_set_type_class('_IMAGE_NT_HEADERS', pe.IMAGE_NT_HEADERS) - self.optional_set_type_class('_IMAGE_NT_HEADERS64', pe.IMAGE_NT_HEADERS) + self.optional_set_type_class("_IMAGE_NT_HEADERS", pe.IMAGE_NT_HEADERS) + self.optional_set_type_class("_IMAGE_NT_HEADERS64", pe.IMAGE_NT_HEADERS) # This doesn't exist in very specific versions of windows - try: + with contextlib.suppress(ValueError): if self.get_type("_POOL_TRACKER_BIG_PAGES").has_member("PoolType"): - self.set_type_class('_POOL_HEADER', pool.POOL_HEADER_VISTA) + self.set_type_class("_POOL_HEADER", pool.POOL_HEADER_VISTA) else: - self.set_type_class('_POOL_HEADER', pool.POOL_HEADER) - except ValueError: - pass + self.set_type_class("_POOL_HEADER", pool.POOL_HEADER) # these don't exist in windows XP - self.optional_set_type_class('_MMADDRESS_NODE', extensions.MMVAD_SHORT) - + self.optional_set_type_class("_MMADDRESS_NODE", extensions.MMVAD_SHORT) + # these were introduced starting in windows 8 - self.optional_set_type_class('_MM_AVL_NODE', extensions.MMVAD_SHORT) - + self.optional_set_type_class("_MM_AVL_NODE", extensions.MMVAD_SHORT) + # these were introduced starting in windows 7 - self.optional_set_type_class('_RTL_BALANCED_NODE', extensions.MMVAD_SHORT) - \ No newline at end of file + self.optional_set_type_class("_RTL_BALANCED_NODE", extensions.MMVAD_SHORT) diff --git a/volatility3/framework/symbols/windows/extensions/__init__.py b/volatility3/framework/symbols/windows/extensions/__init__.py index b5ee272a0..846e5bd90 100755 --- a/volatility3/framework/symbols/windows/extensions/__init__.py +++ b/volatility3/framework/symbols/windows/extensions/__init__.py @@ -3,13 +3,21 @@ # import collections.abc +import contextlib import datetime import functools import logging import math from typing import Generator, Iterable, Iterator, List, Optional, Tuple, Union -from volatility3.framework import constants, exceptions, interfaces, objects, renderers, symbols +from volatility3.framework import ( + constants, + exceptions, + interfaces, + objects, + renderers, + symbols, +) from volatility3.framework.interfaces.objects import ObjectInterface from volatility3.framework.layers import intel from volatility3.framework.renderers import conversion @@ -37,7 +45,7 @@ class MMVAD_SHORT(objects.StructType): by VadRoot. """ - @functools.lru_cache(maxsize = None) + @functools.lru_cache(maxsize=None) def get_tag(self): vad_address = self.vol.offset @@ -50,11 +58,13 @@ class MMVAD_SHORT(objects.StructType): try: # TODO: instantiate a _POOL_HEADER and return PoolTag - bytesobj = self._context.object(symbol_table_name + constants.BANG + "bytes", - layer_name = self.vol.layer_name, - offset = vad_address, - native_layer_name = self.vol.native_layer_name, - length = 4) + bytesobj = self._context.object( + symbol_table_name + constants.BANG + "bytes", + layer_name=self.vol.layer_name, + offset=vad_address, + native_layer_name=self.vol.native_layer_name, + length=4, + ) return bytesobj.decode() except exceptions.InvalidAddressException: @@ -62,14 +72,16 @@ class MMVAD_SHORT(objects.StructType): except UnicodeDecodeError: return None - def traverse(self, visited = None, depth = 0): + def traverse(self, visited=None, depth=0): """Traverse the VAD tree, determining each underlying VAD node type by looking up the pool tag for the structure and then casting into a new object.""" # TODO: this is an arbitrary limit chosen based on past observations if depth > 100: - vollog.log(constants.LOGLEVEL_VVV, "Vad tree is too deep, something went wrong!") + vollog.log( + constants.LOGLEVEL_VVV, "Vad tree is too deep, something went wrong!" + ) raise RuntimeError("Vad tree is too deep") if visited is None: @@ -79,7 +91,7 @@ class MMVAD_SHORT(objects.StructType): if vad_address in visited: vollog.log(constants.LOGLEVEL_VVV, "VAD node already seen!") - return + return None visited.add(vad_address) tag = self.get_tag() @@ -95,25 +107,37 @@ class MMVAD_SHORT(objects.StructType): else: # any node other than the root that doesn't have a recognized tag # is just garbage and we skip the node entirely - vollog.log(constants.LOGLEVEL_VVV, - f"Skipping VAD at {self.vol.offset} depth {depth} with tag {tag}") - return + vollog.log( + constants.LOGLEVEL_VVV, + f"Skipping VAD at {self.vol.offset} depth {depth} with tag {tag}", + ) + return None if target: vad_object = self.cast(target) yield vad_object try: - for vad_node in self.get_left_child().dereference().traverse(visited, depth + 1): + for vad_node in ( + self.get_left_child().dereference().traverse(visited, depth + 1) + ): yield vad_node except exceptions.InvalidAddressException as excp: - vollog.log(constants.LOGLEVEL_VVV, f"Invalid address on LeftChild: {excp.invalid_address:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Invalid address on LeftChild: {excp.invalid_address:#x}", + ) try: - for vad_node in self.get_right_child().dereference().traverse(visited, depth + 1): + for vad_node in ( + self.get_right_child().dereference().traverse(visited, depth + 1) + ): yield vad_node except exceptions.InvalidAddressException as excp: - vollog.log(constants.LOGLEVEL_VVV, f"Invalid address on RightChild: {excp.invalid_address:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Invalid address on RightChild: {excp.invalid_address:#x}", + ) def get_right_child(self): """Get the right child member.""" @@ -178,7 +202,6 @@ class MMVAD_SHORT(objects.StructType): # this is for windows 8 and 10 elif self.has_member("VadNode"): - if self.VadNode.has_member("u1"): return self.VadNode.u1.Parent & ~0x3 @@ -187,7 +210,6 @@ class MMVAD_SHORT(objects.StructType): # also for windows 8 and 10 elif self.has_member("Core"): - if self.Core.VadNode.has_member("u1"): return self.Core.VadNode.u1.Parent & ~0x3 @@ -196,18 +218,16 @@ class MMVAD_SHORT(objects.StructType): raise AttributeError("Unable to find the parent member") - def get_start(self): - """Get the VAD's starting virtual address.""" + def get_start(self) -> int: + """Get the VAD's starting virtual address. This is the first accessible byte in the range.""" if self.has_member("StartingVpn"): - if self.has_member("StartingVpnHigh"): return (self.StartingVpn << 12) | (self.StartingVpnHigh << 44) else: return self.StartingVpn << 12 elif self.has_member("Core"): - if self.Core.has_member("StartingVpnHigh"): return (self.Core.StartingVpn << 12) | (self.Core.StartingVpnHigh << 44) else: @@ -215,11 +235,10 @@ class MMVAD_SHORT(objects.StructType): raise AttributeError("Unable to find the starting VPN member") - def get_end(self): - """Get the VAD's ending virtual address.""" + def get_end(self) -> int: + """Get the VAD's ending virtual address. This is the last accessible byte in the range.""" if self.has_member("EndingVpn"): - if self.has_member("EndingVpnHigh"): return (((self.EndingVpn + 1) << 12) | (self.EndingVpnHigh << 44)) - 1 else: @@ -227,12 +246,18 @@ class MMVAD_SHORT(objects.StructType): elif self.has_member("Core"): if self.Core.has_member("EndingVpnHigh"): - return (((self.Core.EndingVpn + 1) << 12) | (self.Core.EndingVpnHigh << 44)) - 1 + return ( + ((self.Core.EndingVpn + 1) << 12) | (self.Core.EndingVpnHigh << 44) + ) - 1 else: return ((self.Core.EndingVpn + 1) << 12) - 1 raise AttributeError("Unable to find the ending VPN member") + def get_size(self) -> int: + """Get the size of the VAD region. The OS ensures page granularity.""" + return (self.get_end() - self.get_start()) + 1 + def get_commit_charge(self): """Get the VAD's commit charge (number of committed pages)""" @@ -250,19 +275,33 @@ class MMVAD_SHORT(objects.StructType): def get_private_memory(self): """Get the VAD's private memory setting.""" - if self.has_member("u1") and self.u1.has_member("VadFlags1") and self.u1.VadFlags1.has_member("PrivateMemory"): + if ( + self.has_member("u1") + and self.u1.has_member("VadFlags1") + and self.u1.VadFlags1.has_member("PrivateMemory") + ): return self.u1.VadFlags1.PrivateMemory - elif self.has_member("u") and self.u.has_member("VadFlags") and self.u.VadFlags.has_member("PrivateMemory"): + elif ( + self.has_member("u") + and self.u.has_member("VadFlags") + and self.u.VadFlags.has_member("PrivateMemory") + ): return self.u.VadFlags.PrivateMemory elif self.has_member("Core"): - if (self.Core.has_member("u1") and self.Core.u1.has_member("VadFlags1") - and self.Core.u1.VadFlags1.has_member("PrivateMemory")): + if ( + self.Core.has_member("u1") + and self.Core.u1.has_member("VadFlags1") + and self.Core.u1.VadFlags1.has_member("PrivateMemory") + ): return self.Core.u1.VadFlags1.PrivateMemory - elif (self.Core.has_member("u") and self.Core.u.has_member("VadFlags") - and self.Core.u.VadFlags.has_member("PrivateMemory")): + elif ( + self.Core.has_member("u") + and self.Core.u.has_member("VadFlags") + and self.Core.u.VadFlags.has_member("PrivateMemory") + ): return self.Core.u.VadFlags.PrivateMemory raise AttributeError("Unable to find the private memory member") @@ -305,22 +344,22 @@ class MMVAD(MMVAD_SHORT): file_name = renderers.NotApplicableValue() - try: + with contextlib.suppress(exceptions.InvalidAddressException): # this is for xp and 2003 if self.has_member("ControlArea"): filename_obj = self.ControlArea.FilePointer.FileName # this is for vista through windows 7 else: - filename_obj = self.Subsection.ControlArea.FilePointer.dereference().cast( - "_FILE_OBJECT").FileName + filename_obj = ( + self.Subsection.ControlArea.FilePointer.dereference() + .cast("_FILE_OBJECT") + .FileName + ) if filename_obj.Length > 0: file_name = filename_obj.get_string() - except exceptions.InvalidAddressException: - pass - return file_name @@ -332,9 +371,10 @@ class EX_FAST_REF(objects.StructType): """ def dereference(self) -> interfaces.objects.ObjectInterface: - if constants.BANG not in self.vol.type_name: - raise ValueError(f"Invalid symbol table name syntax (no {constants.BANG} found)") + raise ValueError( + f"Invalid symbol table name syntax (no {constants.BANG} found)" + ) # the mask value is different on 32 and 64 bits symbol_table_name = self.vol.type_name.split(constants.BANG)[0] @@ -343,10 +383,12 @@ class EX_FAST_REF(objects.StructType): else: max_fast_ref = 15 - return self._context.object(symbol_table_name + constants.BANG + "pointer", - layer_name = self.vol.layer_name, - offset = self.Object & ~max_fast_ref, - native_layer_name = self.vol.native_layer_name) + return self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.layer_name, + offset=self.Object & ~max_fast_ref, + native_layer_name=self.vol.native_layer_name, + ) class DEVICE_OBJECT(objects.StructType, pool.ExecutiveObject): @@ -364,6 +406,7 @@ class DEVICE_OBJECT(objects.StructType, pool.ExecutiveObject): yield device device = device.AttachedDevice.dereference() + class DRIVER_OBJECT(objects.StructType, pool.ExecutiveObject): """A class for kernel driver objects.""" @@ -374,7 +417,7 @@ class DRIVER_OBJECT(objects.StructType, pool.ExecutiveObject): def get_devices(self) -> Generator[ObjectInterface, None, None]: """Enumerate the driver's device objects""" - device = self.DeviceObject.dereference() + device = self.DeviceObject.dereference() while device: yield device device = device.NextDevice.dereference() @@ -404,32 +447,36 @@ class FILE_OBJECT(objects.StructType, pool.ExecutiveObject): def is_valid(self) -> bool: """Determine if the object is valid.""" - return self.FileName.Length > 0 and self._context.layers[self.FileName.Buffer.vol.native_layer_name].is_valid( - self.FileName.Buffer) + return self.FileName.Length > 0 and self._context.layers[ + self.FileName.Buffer.vol.native_layer_name + ].is_valid(self.FileName.Buffer) def file_name_with_device(self) -> Union[str, interfaces.renderers.BaseAbsentValue]: - name: Union[str, interfaces.renderers.BaseAbsentValue] = renderers.UnreadableValue() + name: Union[str, interfaces.renderers.BaseAbsentValue] = ( + renderers.UnreadableValue() + ) # this pointer needs to be checked against native_layer_name because the object may # be instantiated from a primary (virtual) layer or a memory (physical) layer. if self._context.layers[self.vol.native_layer_name].is_valid(self.DeviceObject): - try: + with contextlib.suppress(ValueError): name = f"\\Device\\{self.DeviceObject.get_device_name()}" - except ValueError: - pass - try: + with contextlib.suppress(TypeError, exceptions.InvalidAddressException): name += self.FileName.String - except (TypeError, exceptions.InvalidAddressException): - pass return name def access_string(self): ## Make a nicely formatted ACL string - return (('R' if self.ReadAccess else '-') + ('W' if self.WriteAccess else '-') + - ('D' if self.DeleteAccess else '-') + ('r' if self.SharedRead else '-') + - ('w' if self.SharedWrite else '-') + ('d' if self.SharedDelete else '-')) + return ( + ("R" if self.ReadAccess else "-") + + ("W" if self.WriteAccess else "-") + + ("D" if self.DeleteAccess else "-") + + ("r" if self.SharedRead else "-") + + ("w" if self.SharedWrite else "-") + + ("d" if self.SharedDelete else "-") + ) class KMUTANT(objects.StructType, pool.ExecutiveObject): @@ -448,30 +495,42 @@ class KMUTANT(objects.StructType, pool.ExecutiveObject): class ETHREAD(objects.StructType): """A class for executive thread objects.""" - def owning_process(self, kernel_layer: str = None) -> interfaces.objects.ObjectInterface: + def owning_process(self) -> interfaces.objects.ObjectInterface: """Return the EPROCESS that owns this thread.""" - return self.ThreadsProcess.dereference(kernel_layer) + + # For Windows XPs + if self.has_member("ThreadsProcess"): + return self.ThreadsProcess.dereference().cast("_EPROCESS") + # For Windows Vista and later versions + elif self.has_member("Tcb") and self.Tcb.has_member("Process"): + return self.Tcb.Process.dereference().cast("_EPROCESS") + else: + raise AttributeError("Unable to find the owning process of ethread") def get_cross_thread_flags(self) -> str: dictCrossThreadFlags = { - 'PS_CROSS_THREAD_FLAGS_TERMINATED': 0, - 'PS_CROSS_THREAD_FLAGS_DEADTHREAD': 1, - 'PS_CROSS_THREAD_FLAGS_HIDEFROMDBG': 2, - 'PS_CROSS_THREAD_FLAGS_IMPERSONATING': 3, - 'PS_CROSS_THREAD_FLAGS_SYSTEM': 4, - 'PS_CROSS_THREAD_FLAGS_HARD_ERRORS_DISABLED': 5, - 'PS_CROSS_THREAD_FLAGS_BREAK_ON_TERMINATION': 6, - 'PS_CROSS_THREAD_FLAGS_SKIP_CREATION_MSG': 7, - 'PS_CROSS_THREAD_FLAGS_SKIP_TERMINATION_MSG': 8 + "PS_CROSS_THREAD_FLAGS_TERMINATED": 0, + "PS_CROSS_THREAD_FLAGS_DEADTHREAD": 1, + "PS_CROSS_THREAD_FLAGS_HIDEFROMDBG": 2, + "PS_CROSS_THREAD_FLAGS_IMPERSONATING": 3, + "PS_CROSS_THREAD_FLAGS_SYSTEM": 4, + "PS_CROSS_THREAD_FLAGS_HARD_ERRORS_DISABLED": 5, + "PS_CROSS_THREAD_FLAGS_BREAK_ON_TERMINATION": 6, + "PS_CROSS_THREAD_FLAGS_SKIP_CREATION_MSG": 7, + "PS_CROSS_THREAD_FLAGS_SKIP_TERMINATION_MSG": 8, } flags = self.CrossThreadFlags - stringCrossThreadFlags = '' + stringCrossThreadFlags = "" for flag in dictCrossThreadFlags: if flags & 2 ** dictCrossThreadFlags[flag]: - stringCrossThreadFlags += f'{flag} ' + stringCrossThreadFlags += f"{flag} " - return stringCrossThreadFlags[:-1] if stringCrossThreadFlags else stringCrossThreadFlags + return ( + stringCrossThreadFlags[:-1] + if stringCrossThreadFlags + else stringCrossThreadFlags + ) class UNICODE_STRING(objects.StructType): @@ -484,10 +543,14 @@ class UNICODE_STRING(objects.StructType): # We manually construct an object rather than casting a dereferenced pointer in case # the buffer length is 0 and the pointer is a NULL pointer - return self._context.object(self.vol.type_name.split(constants.BANG)[0] + constants.BANG + 'string', - layer_name = self.Buffer.vol.layer_name, - offset = self.Buffer, - max_length = self.Length, errors = 'replace', encoding = 'utf16') + return self._context.object( + self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string", + layer_name=self.Buffer.vol.native_layer_name, + offset=self.Buffer, + max_length=self.Length, + errors="replace", + encoding="utf16", + ) String = property(get_string) @@ -529,7 +592,7 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return False # check for all 0s besides the PCID entries - if dtb & ~0xfff == 0: + if dtb & ~0xFFF == 0: return False ## TODO: we can also add the thread Flink and Blink tests if necessary @@ -546,7 +609,9 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): if not isinstance(parent_layer, intel.Intel): # We can't get bits_per_register unless we're an intel space (since that's not defined at the higher layer) - raise TypeError("Parent layer is not a translation layer, unable to construct process layer") + raise TypeError( + "Parent layer is not a translation layer, unable to construct process layer" + ) # Presumably for 64-bit systems, the DTB is defined as an array, rather than an unsigned long long dtb: int = 0 @@ -560,12 +625,16 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): preferred_name = self.vol.layer_name + f"_Process{self.UniqueProcessId}" # Add the constructed layer and return the name - return self._add_process_layer(self._context, dtb, config_prefix, preferred_name) + return self._add_process_layer( + self._context, dtb, config_prefix, preferred_name + ) def get_peb(self) -> interfaces.objects.ObjectInterface: """Constructs a PEB object""" if constants.BANG not in self.vol.type_name: - raise ValueError(f"Invalid symbol table name syntax (no {constants.BANG} found)") + raise ValueError( + f"Invalid symbol table name syntax (no {constants.BANG} found)" + ) # add_process_layer can raise InvalidAddressException. # if that happens, we let the exception propagate upwards @@ -573,13 +642,16 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): proc_layer = self._context.layers[proc_layer_name] if not proc_layer.is_valid(self.Peb): - raise exceptions.InvalidAddressException(proc_layer_name, self.Peb, - f"Invalid Peb address at {self.Peb:0x}") + raise exceptions.InvalidAddressException( + proc_layer_name, self.Peb, f"Invalid Peb address at {self.Peb:0x}" + ) sym_table = self.get_symbol_table_name() - peb = self._context.object(f"{sym_table}{constants.BANG}_PEB", - layer_name = proc_layer_name, - offset = self.Peb) + peb = self._context.object( + f"{sym_table}{constants.BANG}_PEB", + layer_name=proc_layer_name, + offset=self.Peb, + ) return peb def load_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: @@ -588,11 +660,12 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): try: peb = self.get_peb() for entry in peb.Ldr.InLoadOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InLoadOrderLinks"): + f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", + "InLoadOrderLinks", + ): yield entry except exceptions.InvalidAddressException: - return + return None def init_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: """Generator for DLLs in the order that they were initialized""" @@ -600,11 +673,12 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): try: peb = self.get_peb() for entry in peb.Ldr.InInitializationOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InInitializationOrderLinks"): + f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", + "InInitializationOrderLinks", + ): yield entry except exceptions.InvalidAddressException: - return + return None def mem_order_modules(self) -> Iterable[interfaces.objects.ObjectInterface]: """Generator for DLLs in the order that they appear in memory""" @@ -612,11 +686,12 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): try: peb = self.get_peb() for entry in peb.Ldr.InMemoryOrderModuleList.to_list( - f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", - "InMemoryOrderLinks"): + f"{self.get_symbol_table_name()}{constants.BANG}_LDR_DATA_TABLE_ENTRY", + "InMemoryOrderLinks", + ): yield entry except exceptions.InvalidAddressException: - return + return None def get_handle_count(self): try: @@ -625,8 +700,10 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return self.ObjectTable.HandleCount except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _EPROCESS.ObjectTable.HandleCount at {self.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _EPROCESS.ObjectTable.HandleCount at {self.vol.offset:#x}", + ) return renderers.UnreadableValue() @@ -637,19 +714,27 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return renderers.NotApplicableValue() symbol_table_name = self.get_symbol_table_name() - kvo = self._context.layers[self.vol.native_layer_name].config['kernel_virtual_offset'] - ntkrnlmp = self._context.module(symbol_table_name, - layer_name = self.vol.native_layer_name, - offset = kvo, - native_layer_name = self.vol.native_layer_name) - session = ntkrnlmp.object(object_type = "_MM_SESSION_SPACE", offset = self.Session, absolute = True) + kvo = self._context.layers[self.vol.native_layer_name].config[ + "kernel_virtual_offset" + ] + ntkrnlmp = self._context.module( + symbol_table_name, + layer_name=self.vol.native_layer_name, + offset=kvo, + native_layer_name=self.vol.native_layer_name, + ) + session = ntkrnlmp.object( + object_type="_MM_SESSION_SPACE", offset=self.Session, absolute=True + ) if session.has_member("SessionId"): return session.SessionId except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVV, - f"Cannot access _EPROCESS.Session.SessionId at {self.vol.offset:#x}") + vollog.log( + constants.LOGLEVEL_VVV, + f"Cannot access _EPROCESS.Session.SessionId at {self.vol.offset:#x}", + ) return renderers.UnreadableValue() @@ -680,7 +765,6 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): return False def get_vad_root(self): - # windows 8 and 2012 (_MM_AVL_TABLE) if self.VadRoot.has_member("BalancedRoot"): return self.VadRoot.BalancedRoot @@ -709,68 +793,81 @@ class EPROCESS(generic.GenericIntelProcess, pool.ExecutiveObject): block_size = self.get_peb().ProcessParameters.EnvironmentSize except AttributeError: # Windows XP block_size = self.get_peb().ProcessParameters.Length - envars = context.layers[process_space].read(block, block_size).decode("utf-16-le", - errors = 'replace').split('\x00')[:-1] + envars = ( + context.layers[process_space] + .read(block, block_size) + .decode("utf-16-le", errors="replace") + .split("\x00")[:-1] + ) except exceptions.InvalidAddressException: - return renderers.UnreadableValue() + return # Generation finished for envar in envars: - split_index = envar.find('=') + split_index = envar.find("=") env = envar[:split_index] - var = envar[split_index + 1:] + var = envar[split_index + 1 :] # Exclude parse problem with some types of env if env and var: yield env, var + return # Generation finished class LIST_ENTRY(objects.StructType, collections.abc.Iterable): """A class for double-linked lists on Windows.""" - def to_list(self, - symbol_type: str, - member: str, - forward: bool = True, - sentinel: bool = True, - layer: Optional[str] = None) -> Iterator[interfaces.objects.ObjectInterface]: + def to_list( + self, + symbol_type: str, + member: str, + forward: bool = True, + sentinel: bool = True, + layer: Optional[str] = None, + ) -> Iterator[interfaces.objects.ObjectInterface]: """Returns an iterator of the entries in the list.""" layer = layer or self.vol.layer_name - relative_offset = self._context.symbol_space.get_type(symbol_type).relative_child_offset(member) + relative_offset = self._context.symbol_space.get_type( + symbol_type + ).relative_child_offset(member) - direction = 'Blink' + direction = "Blink" if forward: - direction = 'Flink' + direction = "Flink" trans_layer = self._context.layers[layer] try: is_valid = trans_layer.is_valid(self.vol.offset) if not is_valid: - return + return None link = getattr(self, direction).dereference() except exceptions.InvalidAddressException: - return + return None if not sentinel: - yield self._context.object(symbol_type, - layer, - offset = self.vol.offset - relative_offset, - native_layer_name = layer or self.vol.native_layer_name) + yield self._context.object( + symbol_type, + layer, + offset=self.vol.offset - relative_offset, + native_layer_name=layer or self.vol.native_layer_name, + ) seen = {self.vol.offset} while link.vol.offset not in seen: obj_offset = link.vol.offset - relative_offset if not trans_layer.is_valid(obj_offset): - return + return None - obj = self._context.object(symbol_type, - layer, - offset = obj_offset, - native_layer_name = layer or self.vol.native_layer_name) + obj = self._context.object( + symbol_type, + layer, + offset=obj_offset, + native_layer_name=layer or self.vol.native_layer_name, + ) yield obj seen.add(link.vol.offset) @@ -778,7 +875,7 @@ class LIST_ENTRY(objects.StructType, collections.abc.Iterable): try: link = getattr(link, direction).dereference() except exceptions.InvalidAddressException: - return + return None def __iter__(self) -> Iterator[interfaces.objects.ObjectInterface]: return self.to_list(self.vol.parent.vol.type_name, self.vol.member_name) @@ -794,47 +891,63 @@ class TOKEN(objects.StructType): layer_name = self.vol.layer_name kvo = self._context.layers[layer_name].config["kernel_virtual_offset"] symbol_table = self.get_symbol_table_name() - ntkrnlmp = self._context.module(symbol_table, layer_name = layer_name, offset = kvo) - UserAndGroups = ntkrnlmp.object(object_type = "array", - offset = self.UserAndGroups.dereference().vol.get("offset") - kvo, - subtype = ntkrnlmp.get_type("_SID_AND_ATTRIBUTES"), - count = self.UserAndGroupCount) + ntkrnlmp = self._context.module( + symbol_table, layer_name=layer_name, offset=kvo + ) + UserAndGroups = ntkrnlmp.object( + object_type="array", + offset=self.UserAndGroups.dereference().vol.get("offset") - kvo, + subtype=ntkrnlmp.get_type("_SID_AND_ATTRIBUTES"), + count=self.UserAndGroupCount, + ) for sid_and_attr in UserAndGroups: try: sid = sid_and_attr.Sid.dereference().cast("_SID") # catch invalid pointers (UserAndGroupCount is too high) if sid is None: - return + return None # this mimics the windows API IsValidSid if sid.Revision & 0xF != 1 or sid.SubAuthorityCount > 15: - return + return None id_auth = "" for i in sid.IdentifierAuthority.Value: id_auth = i - SubAuthority = ntkrnlmp.object(object_type = "array", - offset = sid.SubAuthority.vol.offset - kvo, - subtype = ntkrnlmp.get_type("unsigned long"), - count = int(sid.SubAuthorityCount)) - yield "S-" + "-".join(str(i) for i in (sid.Revision, id_auth) + tuple(SubAuthority)) + SubAuthority = ntkrnlmp.object( + object_type="array", + offset=sid.SubAuthority.vol.offset - kvo, + subtype=ntkrnlmp.get_type("unsigned long"), + count=int(sid.SubAuthorityCount), + ) + yield "S-" + "-".join( + str(i) for i in (sid.Revision, id_auth) + tuple(SubAuthority) + ) except exceptions.InvalidAddressException: - vollog.log(constants.LOGLEVEL_VVVV, "InvalidAddressException while parsing for token sid") + vollog.log( + constants.LOGLEVEL_VVVV, + "InvalidAddressException while parsing for token sid", + ) def privileges(self): """Return a list of privileges for the current token object.""" try: for priv_index in range(64): - yield (priv_index, bool(self.Privileges.Present & (2 ** priv_index)), - bool(self.Privileges.Enabled & (2 ** priv_index)), - bool(self.Privileges.EnabledByDefault & (2 ** priv_index))) + yield ( + priv_index, + bool(self.Privileges.Present & (2**priv_index)), + bool(self.Privileges.Enabled & (2**priv_index)), + bool(self.Privileges.EnabledByDefault & (2**priv_index)), + ) except AttributeError: # Windows XP if self.PrivilegeCount < 1024: # This is a pointer to an array of _LUID_AND_ATTRIBUTES for luid in self.Privileges.dereference().cast( - "array", - count = self.PrivilegeCount, - subtype = self._context.symbol_space[self.get_symbol_table_name()].get_type( - "_LUID_AND_ATTRIBUTES")): + "array", + count=self.PrivilegeCount, + subtype=self._context.symbol_space[ + self.get_symbol_table_name() + ].get_type("_LUID_AND_ATTRIBUTES"), + ): # The Attributes member is a flag enabled = luid.Attributes & 2 != 0 default = luid.Attributes & 1 != 0 @@ -848,58 +961,58 @@ class KTHREAD(objects.StructType): def get_state(self) -> str: dictState = { - 0: 'Initialized', - 1: 'Ready', - 2: 'Running', - 3: 'Standby', - 4: 'Terminated', - 5: 'Waiting', - 6: 'Transition', - 7: 'DeferredReady', - 8: 'GateWait' + 0: "Initialized", + 1: "Ready", + 2: "Running", + 3: "Standby", + 4: "Terminated", + 5: "Waiting", + 6: "Transition", + 7: "DeferredReady", + 8: "GateWait", } return dictState.get(self.State, renderers.NotApplicableValue()) def get_wait_reason(self) -> str: dictWaitReason = { - 0: 'Executive', - 1: 'FreePage', - 2: 'PageIn', - 3: 'PoolAllocation', - 4: 'DelayExecution', - 5: 'Suspended', - 6: 'UserRequest', - 7: 'WrExecutive', - 8: 'WrFreePage', - 9: 'WrPageIn', - 10: 'WrPoolAllocation', - 11: 'WrDelayExecution', - 12: 'WrSuspended', - 13: 'WrUserRequest', - 14: 'WrEventPair', - 15: 'WrQueue', - 16: 'WrLpcReceive', - 17: 'WrLpcReply', - 18: 'WrVirtualMemory', - 19: 'WrPageOut', - 20: 'WrRendezvous', - 21: 'Spare2', - 22: 'Spare3', - 23: 'Spare4', - 24: 'Spare5', - 25: 'Spare6', - 26: 'WrKernel', - 27: 'WrResource', - 28: 'WrPushLock', - 29: 'WrMutex', - 30: 'WrQuantumEnd', - 31: 'WrDispatchInt', - 32: 'WrPreempted', - 33: 'WrYieldExecution', - 34: 'WrFastMutex', - 35: 'WrGuardedMutex', - 36: 'WrRundown', - 37: 'MaximumWaitReason' + 0: "Executive", + 1: "FreePage", + 2: "PageIn", + 3: "PoolAllocation", + 4: "DelayExecution", + 5: "Suspended", + 6: "UserRequest", + 7: "WrExecutive", + 8: "WrFreePage", + 9: "WrPageIn", + 10: "WrPoolAllocation", + 11: "WrDelayExecution", + 12: "WrSuspended", + 13: "WrUserRequest", + 14: "WrEventPair", + 15: "WrQueue", + 16: "WrLpcReceive", + 17: "WrLpcReply", + 18: "WrVirtualMemory", + 19: "WrPageOut", + 20: "WrRendezvous", + 21: "Spare2", + 22: "Spare3", + 23: "Spare4", + 24: "Spare5", + 25: "Spare6", + 26: "WrKernel", + 27: "WrResource", + 28: "WrPushLock", + 29: "WrMutex", + 30: "WrQuantumEnd", + 31: "WrDispatchInt", + 32: "WrPreempted", + 33: "WrYieldExecution", + 34: "WrFastMutex", + 35: "WrGuardedMutex", + 36: "WrRundown", + 37: "MaximumWaitReason", } return dictWaitReason.get(self.WaitReason, renderers.NotApplicableValue()) @@ -918,7 +1031,9 @@ class CONTROL_AREA(objects.StructType): return False # The SizeOfSegment should match the total PTEs multiplied by a default page size - if self.Segment.SizeOfSegment != (self.Segment.TotalNumberOfPtes * self.PAGE_SIZE): + if self.Segment.SizeOfSegment != ( + self.Segment.TotalNumberOfPtes * self.PAGE_SIZE + ): return False # The first SubsectionBase should not be page aligned @@ -934,18 +1049,22 @@ class CONTROL_AREA(objects.StructType): def get_subsection(self) -> interfaces.objects.ObjectInterface: """Get the Subsection object, which is found immediately after the _CONTROL_AREA.""" - return self._context.object(self.get_symbol_table_name() + constants.BANG + "_SUBSECTION", - layer_name = self.vol.layer_name, - offset = self.vol.offset + self.vol.size, - native_layer_name = self.vol.native_layer_name) + return self._context.object( + self.get_symbol_table_name() + constants.BANG + "_SUBSECTION", + layer_name=self.vol.layer_name, + offset=self.vol.offset + self.vol.size, + native_layer_name=self.vol.native_layer_name, + ) def get_pte(self, offset: int) -> interfaces.objects.ObjectInterface: """Get a PTE object at the requested offset""" - return self._context.object(self.get_symbol_table_name() + constants.BANG + "_MMPTE", - layer_name = self.vol.layer_name, - offset = offset, - native_layer_name = self.vol.native_layer_name) + return self._context.object( + self.get_symbol_table_name() + constants.BANG + "_MMPTE", + layer_name=self.vol.layer_name, + offset=offset, + native_layer_name=self.vol.native_layer_name, + ) def get_available_pages(self) -> Iterable[Tuple[int, int, int]]: """Get the available pages that correspond to a cached file. @@ -953,7 +1072,9 @@ class CONTROL_AREA(objects.StructType): The tuples generated are (physical_offset, file_offset, page_size). """ symbol_table_name = self.get_symbol_table_name() - mmpte_type = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + "_MMPTE") + mmpte_type = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "_MMPTE" + ) mmpte_size = mmpte_type.size subsection = self.get_subsection() is_64bit = symbols.symbol_table_is_64bit(self._context, symbol_table_name) @@ -994,8 +1115,9 @@ class CONTROL_AREA(objects.StructType): elif mmpte.u.Soft.Prototype == 1: if not is_64bit and not is_pae: - subsection_offset = ((mmpte.u.Subsect.SubsectionAddressHigh << 7) | - (mmpte.u.Subsect.SubsectionAddressLow << 3)) + subsection_offset = ( + mmpte.u.Subsect.SubsectionAddressHigh << 7 + ) | (mmpte.u.Subsect.SubsectionAddressLow << 3) # If the entry is not a valid physical address then see if it is in transition. elif mmpte.u.Trans.Transition == 1: @@ -1041,17 +1163,21 @@ class SHARED_CACHE_MAP(objects.StructType): if self.FileSize.QuadPart <= 0 or self.ValidDataLength.QuadPart <= 0: return False - if self.SectionSize.QuadPart < 0 or ((self.FileSize.QuadPart < self.ValidDataLength.QuadPart) and - (self.ValidDataLength.QuadPart != 0x7fffffffffffffff)): + if self.SectionSize.QuadPart < 0 or ( + (self.FileSize.QuadPart < self.ValidDataLength.QuadPart) + and (self.ValidDataLength.QuadPart != 0x7FFFFFFFFFFFFFFF) + ): return False return True - def process_index_array(self, - array_pointer: interfaces.objects.ObjectInterface, - level: int, - limit: int, - vacb_list: Optional[List] = None) -> List: + def process_index_array( + self, + array_pointer: interfaces.objects.ObjectInterface, + level: int, + limit: int, + vacb_list: Optional[List] = None, + ) -> List: """Recursively process the sparse multilevel VACB index array. :param array_pointer: The address of a possible index array @@ -1067,14 +1193,18 @@ class SHARED_CACHE_MAP(objects.StructType): return [] symbol_table_name = self.get_symbol_table_name() - pointer_type = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + "pointer") + pointer_type = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ) # Create an array of 128 entries for the VACB index array - vacb_array = self._context.object(object_type = symbol_table_name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = array_pointer, - count = self.VACB_ARRAY, - subtype = pointer_type) + vacb_array = self._context.object( + object_type=symbol_table_name + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=array_pointer, + count=self.VACB_ARRAY, + subtype=pointer_type, + ) # Iterate through the entries for counter in range(0, self.VACB_ARRAY): @@ -1082,16 +1212,26 @@ class SHARED_CACHE_MAP(objects.StructType): if not vacb_array[counter]: continue - vacb_obj = vacb_array[counter].dereference().cast(symbol_table_name + constants.BANG + "_VACB") + vacb_obj = ( + vacb_array[counter] + .dereference() + .cast(symbol_table_name + constants.BANG + "_VACB") + ) if vacb_obj.SharedCacheMap == self.vol.offset: self.save_vacb(vacb_obj, vacb_list) else: # Process the next level of the multi-level array - vacb_list = self.process_index_array(vacb_array[counter], level + 1, limit, vacb_list) + vacb_list = self.process_index_array( + vacb_array[counter], level + 1, limit, vacb_list + ) return vacb_list def save_vacb(self, vacb_obj: interfaces.objects.ObjectInterface, vacb_list: List): - data = (int(vacb_obj.BaseAddress), int(vacb_obj.get_file_offset()), self.VACB_BLOCK) + data = ( + int(vacb_obj.BaseAddress), + int(vacb_obj.get_file_offset()), + self.VACB_BLOCK, + ) vacb_list.append(data) def get_available_pages(self) -> List: @@ -1114,12 +1254,10 @@ class SHARED_CACHE_MAP(objects.StructType): iterval = 0 while (iterval < full_blocks) and (full_blocks <= 4): vacb_obj = self.InitialVacbs[iterval] - try: + with contextlib.suppress(exceptions.InvalidAddressException): # Make sure that the SharedCacheMap member of the VACB points back to the parent object. if vacb_obj.SharedCacheMap == self.vol.offset: self.save_vacb(vacb_obj, vacb_list) - except exceptions.InvalidAddressException: - pass iterval += 1 # We also have to account for the spill over data that is not found in the full blocks. @@ -1146,15 +1284,19 @@ class SHARED_CACHE_MAP(objects.StructType): # If the file is less than 32 MB than it can be found in a single level VACB index array. symbol_table_name = self.get_symbol_table_name() - pointer_type = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + "pointer") + pointer_type = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "pointer" + ) size_of_pointer = pointer_type.size if not section_size > self.VACB_SIZE_OF_FIRST_LEVEL: array_head = vacb_obj for counter in range(0, full_blocks): - vacb_entry = self._context.object(symbol_table_name + constants.BANG + "pointer", - layer_name = self.vol.layer_name, - offset = array_head + (counter * size_of_pointer)) + vacb_entry = self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.layer_name, + offset=array_head + (counter * size_of_pointer), + ) # If we find a zero entry, then we proceed to the next one. If the entry is zero, # then the view is not mapped and we skip. We do not pad because we use the @@ -1162,19 +1304,25 @@ class SHARED_CACHE_MAP(objects.StructType): if not vacb_entry: continue - vacb = vacb_entry.dereference().cast(symbol_table_name + constants.BANG + "_VACB") + vacb = vacb_entry.dereference().cast( + symbol_table_name + constants.BANG + "_VACB" + ) if vacb.SharedCacheMap == self.vol.offset: self.save_vacb(vacb, vacb_list) if left_over > 0: - vacb_entry = self._context.object(symbol_table_name + constants.BANG + "pointer", - layer_name = self.vol.layer_name, - offset = array_head + ((counter + 1) * size_of_pointer)) + vacb_entry = self._context.object( + symbol_table_name + constants.BANG + "pointer", + layer_name=self.vol.layer_name, + offset=array_head + ((counter + 1) * size_of_pointer), + ) if not vacb_entry: return vacb_list - vacb = vacb_entry.dereference().cast(symbol_table_name + constants.BANG + "_VACB") + vacb = vacb_entry.dereference().cast( + symbol_table_name + constants.BANG + "_VACB" + ) if vacb.SharedCacheMap == self.vol.offset: self.save_vacb(vacb, vacb_list) @@ -1191,13 +1339,14 @@ class SHARED_CACHE_MAP(objects.StructType): limit_depth = level_depth if section_size > self.VACB_SIZE_OF_FIRST_LEVEL: - # Create an array of 128 entries for the VACB index array. - vacb_array = self._context.object(object_type = symbol_table_name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = vacb_obj, - count = self.VACB_ARRAY, - subtype = pointer_type) + vacb_array = self._context.object( + object_type=symbol_table_name + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=vacb_obj, + count=self.VACB_ARRAY, + subtype=pointer_type, + ) # Walk the array and if any entry points to the shared cache map object then we extract it. # Otherwise, if it is non-zero, then traverse to the next level. @@ -1205,13 +1354,19 @@ class SHARED_CACHE_MAP(objects.StructType): if not vacb_array[counter]: continue - vacb = vacb_array[counter].dereference().cast(symbol_table_name + constants.BANG + "_VACB") + vacb = ( + vacb_array[counter] + .dereference() + .cast(symbol_table_name + constants.BANG + "_VACB") + ) if vacb.SharedCacheMap == self.vol.offset: self.save_vacb(vacb, vacb_list) else: # Process the next level of the multi-level array. We set the limit_depth to be # the depth of the tree as determined from the size and we initialize the # current level to 2. - vacb_list = self.process_index_array(vacb_array[counter], 2, limit_depth, vacb_list) + vacb_list = self.process_index_array( + vacb_array[counter], 2, limit_depth, vacb_list + ) return vacb_list diff --git a/volatility3/framework/symbols/windows/extensions/crash.py b/volatility3/framework/symbols/windows/extensions/crash.py index 8d8200aeb..599367a0c 100644 --- a/volatility3/framework/symbols/windows/extensions/crash.py +++ b/volatility3/framework/symbols/windows/extensions/crash.py @@ -7,21 +7,30 @@ from volatility3.framework import objects class SUMMARY_DUMP(objects.StructType): - - def get_buffer(self, sub_type: str, count: int) -> interfaces.objects.ObjectInterface: + def get_buffer( + self, sub_type: str, count: int + ) -> interfaces.objects.ObjectInterface: symbol_table_name = self.get_symbol_table_name() - subtype = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + sub_type) - return self._context.object(object_type = symbol_table_name + constants.BANG + "array", - layer_name = self.vol.layer_name, - offset = self.BufferChar.vol.offset, - count = count, - subtype = subtype) + subtype = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + sub_type + ) + return self._context.object( + object_type=symbol_table_name + constants.BANG + "array", + layer_name=self.vol.layer_name, + offset=self.BufferChar.vol.offset, + count=count, + subtype=subtype, + ) def get_buffer_char(self) -> interfaces.objects.ObjectInterface: - return self.get_buffer(sub_type = "unsigned char", count = (self.BitmapSize + 7) // 8) + return self.get_buffer( + sub_type="unsigned char", count=(self.BitmapSize + 7) // 8 + ) def get_buffer_long(self) -> interfaces.objects.ObjectInterface: - return self.get_buffer(sub_type = "unsigned long", count = (self.BitmapSize + 31) // 32) + return self.get_buffer( + sub_type="unsigned long", count=(self.BitmapSize + 31) // 32 + ) -class_types = {'_SUMMARY_DUMP': SUMMARY_DUMP} +class_types = {"_SUMMARY_DUMP": SUMMARY_DUMP} diff --git a/volatility3/framework/symbols/windows/extensions/kdbg.py b/volatility3/framework/symbols/windows/extensions/kdbg.py index 437edf823..b576f4ef2 100644 --- a/volatility3/framework/symbols/windows/extensions/kdbg.py +++ b/volatility3/framework/symbols/windows/extensions/kdbg.py @@ -7,18 +7,19 @@ from volatility3.framework import objects class KDDEBUGGER_DATA64(objects.StructType): - def get_build_lab(self): """Returns the NT build lab string from the KDBG.""" layer_name = self.vol.layer_name symbol_table_name = self.get_symbol_table_name() - return self._context.object(symbol_table_name + constants.BANG + "string", - layer_name = layer_name, - offset = self.NtBuildLab, - max_length = 32, - errors = "replace") + return self._context.object( + symbol_table_name + constants.BANG + "string", + layer_name=layer_name, + offset=self.NtBuildLab, + max_length=32, + errors="replace", + ) def get_csdversion(self): """Returns the CSDVersion as an integer (i.e. Service Pack number)""" @@ -26,11 +27,13 @@ class KDDEBUGGER_DATA64(objects.StructType): layer_name = self.vol.layer_name symbol_table_name = self.get_symbol_table_name() - csdresult = self._context.object(symbol_table_name + constants.BANG + "unsigned long", - layer_name = layer_name, - offset = self.CmNtCSDVersion) + csdresult = self._context.object( + symbol_table_name + constants.BANG + "unsigned long", + layer_name=layer_name, + offset=self.CmNtCSDVersion, + ) - return (csdresult >> 8) & 0xffffffff + return (csdresult >> 8) & 0xFFFFFFFF -class_types = {'_KDDEBUGGER_DATA64': KDDEBUGGER_DATA64} +class_types = {"_KDDEBUGGER_DATA64": KDDEBUGGER_DATA64} diff --git a/volatility3/framework/symbols/windows/extensions/mbr.py b/volatility3/framework/symbols/windows/extensions/mbr.py index fc7996c52..afdc73a17 100644 --- a/volatility3/framework/symbols/windows/extensions/mbr.py +++ b/volatility3/framework/symbols/windows/extensions/mbr.py @@ -4,30 +4,34 @@ from volatility3.framework import objects -class PARTITION_TABLE(objects.StructType): +class PARTITION_TABLE(objects.StructType): def get_disk_signature(self) -> str: """Get Disk Signature (GUID).""" return "{0:02x}-{1:02x}-{2:02x}-{3:02x}".format( - self.DiskSignature[0], - self.DiskSignature[1], - self.DiskSignature[2], - self.DiskSignature[3] + self.DiskSignature[0], + self.DiskSignature[1], + self.DiskSignature[2], + self.DiskSignature[3], ) + class PARTITION_ENTRY(objects.StructType): - def get_bootable_flag(self) -> int: """Get Bootable Flag.""" return self.BootableFlag - + def is_bootable(self) -> bool: """Check Bootable Partition.""" return False if not (self.get_bootable_flag() == 0x80) else True def get_partition_type(self) -> str: """Get Partition Type.""" - return self.PartitionType.lookup() if self.PartitionType.is_valid_choice else "Not Defined PartitionType" + return ( + self.PartitionType.lookup() + if self.PartitionType.is_valid_choice + else "Not Defined PartitionType" + ) def get_starting_chs(self) -> int: """Get Starting CHS (Cylinder Header Sector) Address.""" @@ -47,16 +51,18 @@ class PARTITION_ENTRY(objects.StructType): def get_starting_cylinder(self) -> int: """Get Starting Cylinder.""" - return (self.StartingCHS[1] - self.get_starting_sector()) * 4 + self.StartingCHS[2] + return ( + self.StartingCHS[1] - self.get_starting_sector() + ) * 4 + self.StartingCHS[2] def get_ending_cylinder(self) -> int: """Get Ending Cylinder.""" return (self.EndingCHS[1] - self.get_ending_sector()) * 4 + self.EndingCHS[2] - + def get_starting_lba(self) -> int: """Get Starting LBA (Logical Block Addressing).""" return self.StartingLBA - + def get_size_in_sectors(self) -> int: """Get Size in Sectors.""" return self.SizeInSectors diff --git a/volatility3/framework/symbols/windows/extensions/mft.py b/volatility3/framework/symbols/windows/extensions/mft.py index ba79b7c8b..14c1f08d6 100644 --- a/volatility3/framework/symbols/windows/extensions/mft.py +++ b/volatility3/framework/symbols/windows/extensions/mft.py @@ -2,14 +2,14 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -from volatility3.framework import objects +from volatility3.framework import objects, constants, exceptions class MFTEntry(objects.StructType): """This represents the base MFT Record""" def get_signature(self) -> str: - signature = self.Signature.cast('string', max_length = 4, encoding = 'latin-1') + signature = self.Signature.cast("string", max_length=4, encoding="latin-1") return signature @@ -17,5 +17,40 @@ class MFTFileName(objects.StructType): """This represents an MFT $FILE_NAME Attribute""" def get_full_name(self) -> str: - output = self.Name.cast("string", encoding = "utf16", max_length = self.NameLength * 2, errors = "replace") + output = self.Name.cast( + "string", encoding="utf16", max_length=self.NameLength * 2, errors="replace" + ) return output + + +class MFTAttribute(objects.StructType): + """This represents an MFT ATTRIBUTE""" + + def get_resident_filename(self) -> str: + # To get the resident name, we jump to relative name offset and read name length * 2 bytes of data + try: + name = self._context.object( + self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "string", + layer_name=self.vol.layer_name, + offset=self.vol.offset + self.Attr_Header.NameOffset, + max_length=self.Attr_Header.NameLength * 2, + errors="replace", + encoding="utf16", + ) + return name + except exceptions.InvalidAddressException: + return None + + def get_resident_filecontent(self) -> bytes: + # To get the resident content, we jump to relative content offset and read name length * 2 bytes of data + try: + bytesobj = self._context.object( + self.vol.type_name.split(constants.BANG)[0] + constants.BANG + "bytes", + layer_name=self.vol.layer_name, + offset=self.vol.offset + self.Attr_Header.ContentOffset, + native_layer_name=self.vol.native_layer_name, + length=self.Attr_Header.ContentLength, + ) + return bytesobj + except exceptions.InvalidAddressException: + return None diff --git a/volatility3/framework/symbols/windows/extensions/network.py b/volatility3/framework/symbols/windows/extensions/network.py index 6e13ad45b..9b7573c2e 100644 --- a/volatility3/framework/symbols/windows/extensions/network.py +++ b/volatility3/framework/symbols/windows/extensions/network.py @@ -19,7 +19,9 @@ def inet_ntop(address_family: int, packed_ip: Union[List[int], Array]) -> str: try: return socket.inet_ntop(address_family, bytes(packed_ip)) except AttributeError: - raise RuntimeError("This version of python does not have socket.inet_ntop, please upgrade") + raise RuntimeError( + "This version of python does not have socket.inet_ntop, please upgrade" + ) raise socket.error("[Errno 97] Address family not supported by protocol") @@ -54,15 +56,21 @@ class _TCP_LISTENER(objects.StructType): MIN_CREATETIME_YEAR = 1950 MAX_CREATETIME_YEAR = 2200 - def __init__(self, context: interfaces.context.ContextInterface, type_name: str, - object_info: interfaces.objects.ObjectInformation, size: int, - members: Dict[str, Tuple[int, interfaces.objects.Template]]) -> None: - - super().__init__(context = context, - type_name = type_name, - object_info = object_info, - size = size, - members = members) + def __init__( + self, + context: interfaces.context.ContextInterface, + type_name: str, + object_info: interfaces.objects.ObjectInformation, + size: int, + members: Dict[str, Tuple[int, interfaces.objects.Template]], + ) -> None: + super().__init__( + context=context, + type_name=type_name, + object_info=object_info, + size=size, + members=members, + ) def get_address_family(self): try: @@ -73,7 +81,7 @@ class _TCP_LISTENER(objects.StructType): def get_owner(self): try: - return self.member('Owner').dereference() + return self.member("Owner").dereference() except exceptions.InvalidAddressException: return None @@ -88,9 +96,11 @@ class _TCP_LISTENER(objects.StructType): def get_owner_procname(self): if self.get_owner().is_valid(): if self.get_owner().has_valid_member("ImageFileName"): - return self.get_owner().ImageFileName.cast("string", - max_length = self.get_owner().ImageFileName.vol.count, - errors = "replace") + return self.get_owner().ImageFileName.cast( + "string", + max_length=self.get_owner().ImageFileName.vol.count, + errors="replace", + ) return None @@ -156,15 +166,19 @@ class _TCP_LISTENER(objects.StructType): yield "v6", inaddr6_any, inaddr6_any def is_valid(self): - try: if not self.get_address_family() in (AF_INET, AF_INET6): - vollog.debug("netw obj 0x{:x} invalid due to invalid address_family {}".format( - self.vol.offset, self.get_address_family())) + vollog.debug( + "netw obj 0x{:x} invalid due to invalid address_family {}".format( + self.vol.offset, self.get_address_family() + ) + ) return False except exceptions.InvalidAddressException: - vollog.debug(f"netw obj 0x{self.vol.offset:x} invalid due to invalid address access") + vollog.debug( + f"netw obj 0x{self.vol.offset:x} invalid due to invalid address access" + ) return False return True @@ -173,7 +187,6 @@ class _TCP_ENDPOINT(_TCP_LISTENER): """Class for objects found in TcpE pools""" def _ipv4_or_ipv6(self, inaddr): - if self.get_address_family() == AF_INET: return inet_ntop(socket.AF_INET, inaddr.addr4) else: @@ -198,23 +211,33 @@ class _TCP_ENDPOINT(_TCP_LISTENER): return None def is_valid(self): - if self.State not in self.State.choices.values(): - vollog.debug(f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid tcp state {self.State}") + vollog.debug( + f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid tcp state {self.State}" + ) return False try: if self.get_address_family() not in (AF_INET, AF_INET6): - vollog.debug(f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid address_family {self.get_address_family()}") + vollog.debug( + f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid address_family {self.get_address_family()}" + ) return False - if not self.get_local_address() and (not self.get_owner() or self.get_owner().UniqueProcessId == 0 - or self.get_owner().UniqueProcessId > 65535): - vollog.debug(f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid owner data") + if not self.get_local_address() and ( + not self.get_owner() + or self.get_owner().UniqueProcessId == 0 + or self.get_owner().UniqueProcessId > 65535 + ): + vollog.debug( + f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid owner data" + ) return False except exceptions.InvalidAddressException: - vollog.debug(f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid address access") + vollog.debug( + f"{type(self)} 0x{self.vol.offset:x} invalid due to invalid address access" + ) return False return True @@ -225,30 +248,28 @@ class _UDP_ENDPOINT(_TCP_LISTENER): class _LOCAL_ADDRESS(objects.StructType): - @property def inaddr(self): return self.pData.dereference().dereference() class _LOCAL_ADDRESS_WIN10_UDP(objects.StructType): - @property def inaddr(self): return self.pData.dereference() win10_x64_class_types = { - '_TCP_ENDPOINT': _TCP_ENDPOINT, - '_TCP_LISTENER': _TCP_LISTENER, - '_UDP_ENDPOINT': _UDP_ENDPOINT, - '_LOCAL_ADDRESS': _LOCAL_ADDRESS, - '_LOCAL_ADDRESS_WIN10_UDP': _LOCAL_ADDRESS_WIN10_UDP + "_TCP_ENDPOINT": _TCP_ENDPOINT, + "_TCP_LISTENER": _TCP_LISTENER, + "_UDP_ENDPOINT": _UDP_ENDPOINT, + "_LOCAL_ADDRESS": _LOCAL_ADDRESS, + "_LOCAL_ADDRESS_WIN10_UDP": _LOCAL_ADDRESS_WIN10_UDP, } class_types = { - '_TCP_ENDPOINT': _TCP_ENDPOINT, - '_TCP_LISTENER': _TCP_LISTENER, - '_UDP_ENDPOINT': _UDP_ENDPOINT, - '_LOCAL_ADDRESS': _LOCAL_ADDRESS + "_TCP_ENDPOINT": _TCP_ENDPOINT, + "_TCP_LISTENER": _TCP_LISTENER, + "_UDP_ENDPOINT": _UDP_ENDPOINT, + "_LOCAL_ADDRESS": _LOCAL_ADDRESS, } diff --git a/volatility3/framework/symbols/windows/extensions/pe.py b/volatility3/framework/symbols/windows/extensions/pe.py index 2f271da5d..3f34fc3dd 100644 --- a/volatility3/framework/symbols/windows/extensions/pe.py +++ b/volatility3/framework/symbols/windows/extensions/pe.py @@ -12,7 +12,6 @@ vollog = logging.getLogger(__name__) class IMAGE_DOS_HEADER(objects.StructType): - def get_nt_header(self) -> interfaces.objects.ObjectInterface: """Carve out the NT header from this DOS header. This reflects on the PE file's Machine type to create a 32- or 64-bit NT header structure. @@ -21,18 +20,24 @@ class IMAGE_DOS_HEADER(objects.StructType): <_IMAGE_NT_HEADERS> or <_IMAGE_NT_HEADERS64> instance """ - if self.e_magic != 0x5a4d: - raise ValueError(f"e_magic {self.e_magic:04X} is not a valid DOS signature.") + if self.e_magic != 0x5A4D: + raise ValueError( + f"e_magic {self.e_magic:04X} is not a valid DOS signature." + ) layer_name = self.vol.layer_name symbol_table_name = self.get_symbol_table_name() - nt_header = self._context.object(symbol_table_name + constants.BANG + "_IMAGE_NT_HEADERS", - layer_name = layer_name, - offset = self.vol.offset + self.e_lfanew) + nt_header = self._context.object( + symbol_table_name + constants.BANG + "_IMAGE_NT_HEADERS", + layer_name=layer_name, + offset=self.vol.offset + self.e_lfanew, + ) if nt_header.Signature != 0x4550: - raise ValueError(f"NT header signature {nt_header.Signature:04X} is not a valid") + raise ValueError( + f"NT header signature {nt_header.Signature:04X} is not a valid" + ) # this checks if we need a PE32+ header if nt_header.FileHeader.Machine == 34404: @@ -40,8 +45,13 @@ class IMAGE_DOS_HEADER(objects.StructType): return nt_header - def replace_header_field(self, sect: interfaces.objects.ObjectInterface, header: bytes, - item: interfaces.objects.ObjectInterface, value: int) -> bytes: + def replace_header_field( + self, + sect: interfaces.objects.ObjectInterface, + header: bytes, + item: interfaces.objects.ObjectInterface, + value: int, + ) -> bytes: """Replaces a member in an _IMAGE_SECTION_HEADER structure. Args: @@ -57,10 +67,12 @@ class IMAGE_DOS_HEADER(objects.StructType): member_size = self._context.symbol_space.get_type(item.vol.type_name).size start = item.vol.offset - sect.vol.offset newval = objects.convert_value_to_data(value, int, item.vol.data_format) - result = header[:start] + newval + header[start + member_size:] + result = header[:start] + newval + header[start + member_size :] return result - def fix_image_base(self, raw_data: bytes, nt_header: interfaces.objects.ObjectInterface) -> bytes: + def fix_image_base( + self, raw_data: bytes, nt_header: interfaces.objects.ObjectInterface + ) -> bytes: """Fix the _OPTIONAL_HEADER.ImageBase value (which is either an unsigned long for 32-bit PE's or unsigned long long for 64-bit PE's) to match the address where the PE file was carved out of memory. @@ -73,17 +85,26 @@ class IMAGE_DOS_HEADER(objects.StructType): patched with the correct address """ - image_base_offset = nt_header.OptionalHeader.ImageBase.vol.offset - self.vol.offset + image_base_offset = ( + nt_header.OptionalHeader.ImageBase.vol.offset - self.vol.offset + ) image_base_type = nt_header.OptionalHeader.ImageBase.vol.type_name member_size = self._context.symbol_space.get_type(image_base_type).size try: - newval = objects.convert_value_to_data(self.vol.offset, int, - nt_header.OptionalHeader.ImageBase.vol.data_format) - new_pe = raw_data[:image_base_offset] + newval + raw_data[image_base_offset + member_size:] + newval = objects.convert_value_to_data( + self.vol.offset, int, nt_header.OptionalHeader.ImageBase.vol.data_format + ) + new_pe = ( + raw_data[:image_base_offset] + + newval + + raw_data[image_base_offset + member_size :] + ) except OverflowError: - vollog.warning("Volatility was unable to fix the image base for the PE file at base address {:#x}. " \ - "This will cause issues with many static analysis tools if you do not inform the " \ - "tool of the in-memory load address.".format(self.vol.offset)) + vollog.warning( + "Volatility was unable to fix the image base for the PE file at base address {:#x}. " + "This will cause issues with many static analysis tools if you do not inform the " + "tool of the in-memory load address.".format(self.vol.offset) + ) new_pe = raw_data return new_pe @@ -104,8 +125,9 @@ class IMAGE_DOS_HEADER(objects.StructType): section_alignment = nt_header.OptionalHeader.SectionAlignment - sect_header_size = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + - "_IMAGE_SECTION_HEADER").size + sect_header_size = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "_IMAGE_SECTION_HEADER" + ).size size_of_image = nt_header.OptionalHeader.SizeOfImage @@ -115,34 +137,50 @@ class IMAGE_DOS_HEADER(objects.StructType): read_layer = self._context.layers[layer_name] - raw_data = read_layer.read(self.vol.offset, nt_header.OptionalHeader.SizeOfImage, pad = True) + raw_data = read_layer.read( + self.vol.offset, nt_header.OptionalHeader.SizeOfImage, pad=True + ) # fix the PE image base before yielding the initial view of the data fixed_data = self.fix_image_base(raw_data, nt_header) yield 0, fixed_data - start_addr = nt_header.FileHeader.SizeOfOptionalHeader + \ - (nt_header.OptionalHeader.vol.offset - self.vol.offset) + start_addr = nt_header.FileHeader.SizeOfOptionalHeader + ( + nt_header.OptionalHeader.vol.offset - self.vol.offset + ) counter = 0 for sect in nt_header.get_sections(): - if sect.VirtualAddress > size_of_image: - raise ValueError(f"Section VirtualAddress is too large: {sect.VirtualAddress}") + raise ValueError( + f"Section VirtualAddress is too large: {sect.VirtualAddress}" + ) if sect.Misc.VirtualSize > size_of_image: - raise ValueError(f"Section VirtualSize is too large: {sect.Misc.VirtualSize}") + raise ValueError( + f"Section VirtualSize is too large: {sect.Misc.VirtualSize}" + ) if sect.SizeOfRawData > size_of_image: - raise ValueError(f"Section SizeOfRawData is too large: {sect.SizeOfRawData}") + raise ValueError( + f"Section SizeOfRawData is too large: {sect.SizeOfRawData}" + ) if sect is not None: # It doesn't matter if this is too big, because it'll get overwritten by the later layers - sect_size = conversion.round(sect.Misc.VirtualSize, section_alignment, up = True) + sect_size = conversion.round( + sect.Misc.VirtualSize, section_alignment, up=True + ) sectheader = read_layer.read(sect.vol.offset, sect_header_size) - sectheader = self.replace_header_field(sect, sectheader, sect.PointerToRawData, sect.VirtualAddress) - sectheader = self.replace_header_field(sect, sectheader, sect.SizeOfRawData, sect_size) - sectheader = self.replace_header_field(sect, sectheader, sect.Misc.VirtualSize, sect_size) + sectheader = self.replace_header_field( + sect, sectheader, sect.PointerToRawData, sect.VirtualAddress + ) + sectheader = self.replace_header_field( + sect, sectheader, sect.SizeOfRawData, sect_size + ) + sectheader = self.replace_header_field( + sect, sectheader, sect.Misc.VirtualSize, sect_size + ) offset = start_addr + (counter * sect_header_size) yield offset, sectheader @@ -150,7 +188,6 @@ class IMAGE_DOS_HEADER(objects.StructType): class IMAGE_NT_HEADERS(objects.StructType): - def get_sections(self) -> Generator[interfaces.objects.ObjectInterface, None, None]: """Iterate through the section headers for this PE file. @@ -160,20 +197,25 @@ class IMAGE_NT_HEADERS(objects.StructType): layer_name = self.vol.layer_name symbol_table_name = self.get_symbol_table_name() - sect_header_size = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + - "_IMAGE_SECTION_HEADER").size - start_addr = self.FileHeader.SizeOfOptionalHeader + self.OptionalHeader.vol.offset + sect_header_size = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "_IMAGE_SECTION_HEADER" + ).size + start_addr = ( + self.FileHeader.SizeOfOptionalHeader + self.OptionalHeader.vol.offset + ) for i in range(self.FileHeader.NumberOfSections): sect_addr = start_addr + (i * sect_header_size) - yield self._context.object(symbol_table_name + constants.BANG + "_IMAGE_SECTION_HEADER", - offset = sect_addr, - layer_name = layer_name) + yield self._context.object( + symbol_table_name + constants.BANG + "_IMAGE_SECTION_HEADER", + offset=sect_addr, + layer_name=layer_name, + ) class_types = { - '_IMAGE_DOS_HEADER': IMAGE_DOS_HEADER, + "_IMAGE_DOS_HEADER": IMAGE_DOS_HEADER, # the 32- and 64-bit extensions behave the same way, but the underlying structure is different - '_IMAGE_NT_HEADERS': IMAGE_NT_HEADERS, - '_IMAGE_NT_HEADERS64': IMAGE_NT_HEADERS + "_IMAGE_NT_HEADERS": IMAGE_NT_HEADERS, + "_IMAGE_NT_HEADERS64": IMAGE_NT_HEADERS, } diff --git a/volatility3/framework/symbols/windows/extensions/pool.py b/volatility3/framework/symbols/windows/extensions/pool.py index 368765497..b761ddad8 100644 --- a/volatility3/framework/symbols/windows/extensions/pool.py +++ b/volatility3/framework/symbols/windows/extensions/pool.py @@ -1,12 +1,21 @@ +import contextlib import functools import logging import struct -from typing import Optional, Tuple, List, Dict, Union +from typing import Dict, List, Optional, Tuple, Union -from volatility3.framework import objects, interfaces, constants, symbols, exceptions, renderers -from volatility3.framework.renderers import conversion from volatility3.plugins.windows.poolscanner import PoolConstraint +from volatility3.framework import ( + constants, + exceptions, + interfaces, + objects, + renderers, + symbols, +) +from volatility3.framework.renderers import conversion + vollog = logging.getLogger(__name__) @@ -17,15 +26,17 @@ class POOL_HEADER(objects.StructType): scan for. """ - def get_object(self, - constraint: PoolConstraint, - use_top_down: bool, - kernel_symbol_table: Optional[str] = None, - native_layer_name: Optional[str] = None) -> Optional[interfaces.objects.ObjectInterface]: + def get_object( + self, + constraint: PoolConstraint, + use_top_down: bool, + kernel_symbol_table: Optional[str] = None, + native_layer_name: Optional[str] = None, + ) -> Optional[interfaces.objects.ObjectInterface]: """Carve an object or data structure from a kernel pool allocation Args: - constraint: a PoolConstraint object used to get the pool allocation header object + constraint: a PoolConstraint object used to get the pool allocation header object use_top_down: for delineating how a windows version finds the size of the object body kernel_symbol_table: in case objects of a different symbol table are scanned for native_layer_name: the name of the layer where the data originally lived @@ -44,21 +55,25 @@ class POOL_HEADER(objects.StructType): # when checking for symbols from a table other than nt_symbols grab _OBJECT_HEADER from the kernel # because symbol_table_name will be different from kernel_symbol_table. if kernel_symbol_table: - object_header_type = self._context.symbol_space.get_type(kernel_symbol_table + constants.BANG + - "_OBJECT_HEADER") + object_header_type = self._context.symbol_space.get_type( + kernel_symbol_table + constants.BANG + "_OBJECT_HEADER" + ) else: # otherwise symbol_table_name *is* the kernel symbol table, so just use that. - object_header_type = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + - "_OBJECT_HEADER") + object_header_type = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "_OBJECT_HEADER" + ) pool_header_size = self.vol.size # if there is no object type, then just instantiate a structure if not executive: - mem_object = self._context.object(symbol_table_name + constants.BANG + type_name, - layer_name = self.vol.layer_name, - offset = self.vol.offset + pool_header_size, - native_layer_name = native_layer_name) + mem_object = self._context.object( + symbol_table_name + constants.BANG + type_name, + layer_name=self.vol.layer_name, + offset=self.vol.offset + pool_header_size, + native_layer_name=native_layer_name, + ) yield mem_object # otherwise we have an executive object in the pool @@ -70,36 +85,53 @@ class POOL_HEADER(objects.StructType): # use the top down approach for windows 8 and later if use_top_down: - body_offset = object_header_type.relative_child_offset('Body') - infomask_offset = object_header_type.relative_child_offset('InfoMask') - pointercount_offset = object_header_type.relative_child_offset('PointerCount') - pointercount_size = object_header_type.members['PointerCount'][1].size - optional_headers, lengths_of_optional_headers = self._calculate_optional_header_lengths( - self._context, symbol_table_name) - padding_available = None if 'PADDING_INFO' not in optional_headers else optional_headers.index( - 'PADDING_INFO') + body_offset = object_header_type.relative_child_offset("Body") + infomask_offset = object_header_type.relative_child_offset("InfoMask") + pointercount_offset = object_header_type.relative_child_offset( + "PointerCount" + ) + pointercount_size = object_header_type.members["PointerCount"][1].size + ( + optional_headers, + lengths_of_optional_headers, + ) = self._calculate_optional_header_lengths( + self._context, symbol_table_name + ) + padding_available = ( + None + if "PADDING_INFO" not in optional_headers + else optional_headers.index("PADDING_INFO") + ) max_optional_headers_length = sum(lengths_of_optional_headers) # define the starting and ending bounds for the scan start_offset = self.vol.offset + pool_header_size - addr_limit = min(max_optional_headers_length, self.BlockSize * alignment) + addr_limit = min( + max_optional_headers_length, self.BlockSize * alignment + ) # A single read is better than lots of little one-byte reads. # We're ok padding this, because the byte we'd check would be 0 which would only be valid if there # were no optional headers in the first place (ie, if we read too much for headers that don't exist, # but the bit we could read were valid) - infomask_data = self._context.layers[self.vol.layer_name].read(start_offset, - addr_limit + infomask_offset, - pad = True) + infomask_data = self._context.layers[self.vol.layer_name].read( + start_offset, addr_limit + infomask_offset, pad=True + ) # Addr stores the offset to the potential start of the OBJECT_HEADER from just after the POOL_HEADER # It will always be aligned to a particular alignment for addr in range(0, addr_limit, alignment): infomask_value = infomask_data[addr + infomask_offset] pointercount_value = int.from_bytes( - infomask_data[addr + pointercount_offset:addr + pointercount_offset + pointercount_size], - byteorder = 'little', - signed = True) + infomask_data[ + addr + + pointercount_offset : addr + + pointercount_offset + + pointercount_size + ], + byteorder="little", + signed=True, + ) if not 0x1000000 > pointercount_value >= 0: continue @@ -128,9 +160,18 @@ class POOL_HEADER(objects.StructType): # --------------- if addr - optional_headers_length < 0: continue - padding_length, = struct.unpack( - "= padding_length > addr: continue - try: - mem_object = self._context.object(symbol_table_name + constants.BANG + type_name, - layer_name = self.vol.layer_name, - offset = addr + body_offset + start_offset, - native_layer_name = native_layer_name) + with contextlib.suppress( + TypeError, exceptions.InvalidAddressException + ): + mem_object = self._context.object( + symbol_table_name + constants.BANG + type_name, + layer_name=self.vol.layer_name, + offset=addr + body_offset + start_offset, + native_layer_name=native_layer_name, + ) if mem_object.is_valid(): yield mem_object - except (TypeError, exceptions.InvalidAddressException): - pass - # use the bottom up approach for windows 7 and earlier else: - type_size = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + type_name).size + type_size = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + type_name + ).size if constraint.additional_structures: for additional_structure in constraint.additional_structures: - type_size += self._context.symbol_space.get_type(symbol_table_name + constants.BANG + additional_structure).size + type_size += self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + additional_structure + ).size - rounded_size = conversion.round(type_size, alignment, up = True) + rounded_size = conversion.round(type_size, alignment, up=True) - mem_object = self._context.object(symbol_table_name + constants.BANG + type_name, - layer_name = self.vol.layer_name, - offset = self.vol.offset + self.BlockSize * alignment - rounded_size, - native_layer_name = native_layer_name) + mem_object = self._context.object( + symbol_table_name + constants.BANG + type_name, + layer_name=self.vol.layer_name, + offset=self.vol.offset + self.BlockSize * alignment - rounded_size, + native_layer_name=native_layer_name, + ) - try: + with contextlib.suppress(TypeError, exceptions.InvalidAddressException): if mem_object.is_valid(): yield mem_object - except (TypeError, exceptions.InvalidAddressException): - pass @classmethod @functools.lru_cache() - def _calculate_optional_header_lengths(cls, context: interfaces.context.ContextInterface, - symbol_table_name: str) -> Tuple[List[str], List[int]]: + def _calculate_optional_header_lengths( + cls, context: interfaces.context.ContextInterface, symbol_table_name: str + ) -> Tuple[List[str], List[int]]: headers = [] sizes = [] for header in [ - 'CREATOR_INFO', 'NAME_INFO', 'HANDLE_INFO', 'QUOTA_INFO', 'PROCESS_INFO', 'AUDIT_INFO', 'EXTENDED_INFO', - 'HANDLE_REVOCATION_INFO', 'PADDING_INFO' + "CREATOR_INFO", + "NAME_INFO", + "HANDLE_INFO", + "QUOTA_INFO", + "PROCESS_INFO", + "AUDIT_INFO", + "EXTENDED_INFO", + "HANDLE_REVOCATION_INFO", + "PADDING_INFO", ]: - try: - type_name = f"{symbol_table_name}{constants.BANG}_OBJECT_HEADER_{header}" + with contextlib.suppress(AttributeError, exceptions.SymbolError): + type_name = ( + f"{symbol_table_name}{constants.BANG}_OBJECT_HEADER_{header}" + ) header_type = context.symbol_space.get_type(type_name) headers.append(header) sizes.append(header_type.size) - except (AttributeError, exceptions.SymbolError): # Some of these may not exist, for example: # if build < 9200: PADDING_INFO else: AUDIT_INFO # if build == 10586: HANDLE_REVOCATION_INFO else EXTENDED_INFO # based on what's present and what's not, this list should be the right order and the right length - pass return headers, sizes def is_free_pool(self): @@ -226,7 +280,9 @@ class POOL_TRACKER_BIG_PAGES(objects.StructType): # Enumeration._generate_inverse_choices() raises ValueError because multiple enum names map to the same # value in the kernel _POOL_TYPE so create a custom mapping here and take the first match symbol_table_name = self.vol.type_name.split(constants.BANG)[0] - pool_type_enum = self._context.symbol_space.get_enumeration(symbol_table_name + constants.BANG + "_POOL_TYPE") + pool_type_enum = self._context.symbol_space.get_enumeration( + symbol_table_name + constants.BANG + "_POOL_TYPE" + ) for k, v in pool_type_enum.choices.items(): if v not in self.pool_type_lookup: self.pool_type_lookup[v] = k @@ -240,16 +296,20 @@ class POOL_TRACKER_BIG_PAGES(objects.StructType): def get_key(self) -> str: """Returns the Key value as a 4 character string""" - tag_bytes = objects.convert_value_to_data(self.Key, int, objects.DataFormatInfo(4, "little", False)) - return "".join([chr(x) if 32 < x < 127 else '' for x in tag_bytes]) + tag_bytes = objects.convert_value_to_data( + self.Key, int, objects.DataFormatInfo(4, "little", False) + ) + return "".join([chr(x) if 32 < x < 127 else "" for x in tag_bytes]) def get_pool_type(self) -> Union[str, interfaces.renderers.BaseAbsentValue]: """Returns the enum name for the PoolType value on applicable systems""" # Not applicable until Vista - if hasattr(self, 'PoolType'): + if hasattr(self, "PoolType"): if not self.pool_type_lookup: self._generate_pool_type_lookup() - return self.pool_type_lookup.get(self.PoolType, f"Unknown choice {self.PoolType}") + return self.pool_type_lookup.get( + self.PoolType, f"Unknown choice {self.PoolType}" + ) else: return renderers.NotApplicableValue() @@ -266,16 +326,21 @@ class ExecutiveObject(interfaces.objects.ObjectInterface): """This is used as a "mixin" that provides all kernel executive objects with a means of finding their own object header.""" - def get_object_header(self) -> 'OBJECT_HEADER': + def get_object_header(self) -> "OBJECT_HEADER": if constants.BANG not in self.vol.type_name: - raise ValueError(f"Invalid symbol table name syntax (no {constants.BANG} found)") + raise ValueError( + f"Invalid symbol table name syntax (no {constants.BANG} found)" + ) symbol_table_name = self.vol.type_name.split(constants.BANG)[0] - body_offset = self._context.symbol_space.get_type(symbol_table_name + constants.BANG + - "_OBJECT_HEADER").relative_child_offset("Body") - return self._context.object(symbol_table_name + constants.BANG + "_OBJECT_HEADER", - layer_name = self.vol.layer_name, - offset = self.vol.offset - body_offset, - native_layer_name = self.vol.native_layer_name) + body_offset = self._context.symbol_space.get_type( + symbol_table_name + constants.BANG + "_OBJECT_HEADER" + ).relative_child_offset("Body") + return self._context.object( + symbol_table_name + constants.BANG + "_OBJECT_HEADER", + layer_name=self.vol.layer_name, + offset=self.vol.offset - body_offset, + native_layer_name=self.vol.native_layer_name, + ) class OBJECT_HEADER(objects.StructType): @@ -296,7 +361,9 @@ class OBJECT_HEADER(objects.StructType): return True - def get_object_type(self, type_map: Dict[int, str], cookie: int = None) -> Optional[str]: + def get_object_type( + self, type_map: Dict[int, str], cookie: int = None + ) -> Optional[str]: """Across all Windows versions, the _OBJECT_HEADER embeds details on the type of object (i.e. process, file) but the way its embedded differs between versions. @@ -304,12 +371,12 @@ class OBJECT_HEADER(objects.StructType): This API abstracts away those details. """ - if self.vol.get('object_header_object_type', None) is not None: + if self.vol.get("object_header_object_type", None) is not None: return self.vol.object_header_object_type try: # vista and earlier have a Type member - self._vol['object_header_object_type'] = self.Type.Name.String + self._vol["object_header_object_type"] = self.Type.Name.String except AttributeError: # windows 7 and later have a TypeIndex, but windows 10 # further encodes the index value with nt1!ObHeaderCookie @@ -318,42 +385,61 @@ class OBJECT_HEADER(objects.StructType): except (AttributeError, TypeError): type_index = self.TypeIndex - self._vol['object_header_object_type'] = type_map.get(type_index) + self._vol["object_header_object_type"] = type_map.get(type_index) return self.vol.object_header_object_type @property def NameInfo(self) -> interfaces.objects.ObjectInterface: if constants.BANG not in self.vol.type_name: - raise ValueError(f"Invalid symbol table name syntax (no {constants.BANG} found)") + raise ValueError( + f"Invalid symbol table name syntax (no {constants.BANG} found)" + ) symbol_table_name = self.vol.type_name.split(constants.BANG)[0] + if symbol_table_name in self._context.modules: + ntkrnlmp = self._context.modules[symbol_table_name] + else: + layer = self._context.layers[self.vol.native_layer_name] + kvo = layer.config.get("kernel_virtual_offset", None) + + if kvo is None: + raise AttributeError( + f"Could not find kernel_virtual_offset for layer: {self.vol.layer_name}" + ) + + # We know this symbol table name can't exist because we checked for it earlier + ntkrnlmp = self._context.module( + symbol_table_name, layer_name=self.vol.layer_name, offset=kvo + ) + try: header_offset = self.NameInfoOffset except AttributeError: # http://codemachine.com/article_objectheader.html (Windows 7 and later) name_info_bit = 0x2 - layer = self._context.layers[self.vol.native_layer_name] - kvo = layer.config.get("kernel_virtual_offset", None) - - if kvo is None: - raise AttributeError(f"Could not find kernel_virtual_offset for layer: {self.vol.layer_name}") - - ntkrnlmp = self._context.module(symbol_table_name, layer_name = self.vol.layer_name, offset = kvo) address = ntkrnlmp.get_symbol("ObpInfoMaskToOffset").address calculated_index = self.InfoMask & (name_info_bit | (name_info_bit - 1)) - header_offset = self._context.object(symbol_table_name + constants.BANG + "unsigned char", - layer_name = self.vol.native_layer_name, - offset = kvo + address + calculated_index) + header_offset = ntkrnlmp.object( + "unsigned char", + layer_name=self.vol.native_layer_name, + offset=address + calculated_index, + ) if header_offset == 0: - raise ValueError("Could not find _OBJECT_HEADER_NAME_INFO for object at {} of layer {}".format( - self.vol.offset, self.vol.layer_name)) + raise ValueError( + "Could not find _OBJECT_HEADER_NAME_INFO for object at {} of layer {}".format( + self.vol.offset, self.vol.layer_name + ) + ) - header = self._context.object(symbol_table_name + constants.BANG + "_OBJECT_HEADER_NAME_INFO", - layer_name = self.vol.layer_name, - offset = self.vol.offset - header_offset, - native_layer_name = self.vol.native_layer_name) + header = ntkrnlmp.object( + "_OBJECT_HEADER_NAME_INFO", + layer_name=self.vol.layer_name, + offset=self.vol.offset - header_offset, + native_layer_name=self.vol.native_layer_name, + absolute=True, + ) return header diff --git a/volatility3/framework/symbols/windows/extensions/registry.py b/volatility3/framework/symbols/windows/extensions/registry.py index 47ff24506..51be0841c 100644 --- a/volatility3/framework/symbols/windows/extensions/registry.py +++ b/volatility3/framework/symbols/windows/extensions/registry.py @@ -1,18 +1,22 @@ # 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 enum import logging import struct from typing import Iterable, Optional, Union from volatility3.framework import constants, exceptions, interfaces, objects -from volatility3.framework.layers.registry import RegistryFormatException, RegistryHive, RegistryInvalidIndex +from volatility3.framework.layers.registry import ( + RegistryFormatException, + RegistryHive, + RegistryInvalidIndex, +) vollog = logging.getLogger(__name__) -BIG_DATA_MAXLEN = 0x3fd8 +BIG_DATA_MAXLEN = 0x3FD8 class RegValueTypes(enum.Enum): @@ -49,20 +53,20 @@ class RegKeyFlags(enum.IntEnum): class HMAP_ENTRY(objects.StructType): - def get_block_offset(self) -> int: try: - return (self.PermanentBinAddress ^ (self.PermanentBinAddress & 0xf)) + self.BlockOffset + return ( + self.PermanentBinAddress ^ (self.PermanentBinAddress & 0xF) + ) + self.BlockOffset except AttributeError: return self.BlockAddress class CMHIVE(objects.StructType): - def is_valid(self) -> bool: """Determine if the object is valid.""" try: - return self.Hive.Signature == 0xbee0bee0 + return self.Hive.Signature == 0xBEE0BEE0 except exceptions.InvalidAddressException: return False @@ -75,12 +79,12 @@ class CMHIVE(objects.StructType): """ for attr in ["FileFullPath", "FileUserName", "HiveRootPath"]: - try: + with contextlib.suppress( + AttributeError, exceptions.InvalidAddressException + ): name = getattr(self, attr) if name.Length > 0: return name.get_string() - except (AttributeError, exceptions.InvalidAddressException): - pass return None @@ -96,7 +100,10 @@ class CM_KEY_BODY(objects.StructType): checking for Flags that contain KEY_HIVE_ENTRY.""" # _CM_KEY_BODY.Trans introduced in Win10 14393 - if hasattr(self, "Trans") and RegKeyFlags.KEY_HIVE_ENTRY & kcb_flags == RegKeyFlags.KEY_HIVE_ENTRY: + if ( + hasattr(self, "Trans") + and RegKeyFlags.KEY_HIVE_ENTRY & kcb_flags == RegKeyFlags.KEY_HIVE_ENTRY + ): return True return False @@ -114,10 +121,13 @@ class CM_KEY_BODY(objects.StructType): break output.append( - kcb.NameBlock.Name.cast("string", - encoding = "utf8", - max_length = kcb.NameBlock.NameLength, - errors = "replace")) + kcb.NameBlock.Name.cast( + "string", + encoding="utf8", + max_length=kcb.NameBlock.NameLength, + errors="replace", + ) + ) kcb = kcb.ParentKcb return "\\".join(reversed(output)) @@ -127,7 +137,9 @@ class CM_KEY_NODE(objects.StructType): def get_volatile(self) -> bool: if not isinstance(self._context.layers[self.vol.layer_name], RegistryHive): - raise ValueError("Cannot determine volatility of registry key without an offset in a RegistryHive layer") + raise ValueError( + "Cannot determine volatility of registry key without an offset in a RegistryHive layer" + ) return bool(self.vol.offset & 0x80000000) def get_subkeys(self) -> Iterable[interfaces.objects.ObjectInterface]: @@ -141,40 +153,50 @@ class CM_KEY_NODE(objects.StructType): yield from self._get_subkeys_recursive(hive, subkey_node) def _get_subkeys_recursive( - self, hive: RegistryHive, - node: interfaces.objects.ObjectInterface) -> Iterable[interfaces.objects.ObjectInterface]: + self, hive: RegistryHive, node: interfaces.objects.ObjectInterface + ) -> Iterable[interfaces.objects.ObjectInterface]: """Recursively descend a node returning subkeys.""" # The keylist appears to include 4 bytes of key name after each value # We can either double the list and only use the even items, or # We could change the array type to a struct with both parts try: - signature = node.cast('string', max_length = 2, encoding = 'latin-1') + signature = node.cast("string", max_length=2, encoding="latin-1") except (exceptions.InvalidAddressException, RegistryFormatException): - return + return None listjump = None - if signature == 'ri': + if signature == "ri": listjump = 1 - elif signature == 'lh' or signature == 'lf': + elif signature == "lh" or signature == "lf": listjump = 2 elif node.vol.type_name.endswith(constants.BANG + "_CM_KEY_NODE"): yield node else: - vollog.debug("Unexpected node type encountered when traversing subkeys: {}, signature: {}".format( - node.vol.type_name, signature)) + vollog.debug( + "Unexpected node type encountered when traversing subkeys: {}, signature: {}".format( + node.vol.type_name, signature + ) + ) if listjump: node.List.count = node.Count * listjump for subnode_offset in node.List[::listjump]: - if (subnode_offset & 0x7fffffff) > hive.maximum_address: - vollog.log(constants.LOGLEVEL_VVV, - f"Node found with address outside the valid Hive size: {hex(subnode_offset)}") + if (subnode_offset & 0x7FFFFFFF) > hive.maximum_address: + vollog.log( + constants.LOGLEVEL_VVV, + f"Node found with address outside the valid Hive size: {hex(subnode_offset)}", + ) else: try: subnode = hive.get_node(subnode_offset) - except (exceptions.InvalidAddressException, RegistryFormatException): - vollog.log(constants.LOGLEVEL_VVV, - f"Failed to get node at {hex(subnode_offset)}, skipping") + except ( + exceptions.InvalidAddressException, + RegistryFormatException, + ): + vollog.log( + constants.LOGLEVEL_VVV, + f"Failed to get node at {hex(subnode_offset)}, skipping", + ) continue yield from self._get_subkeys_recursive(hive, subnode) @@ -194,17 +216,17 @@ class CM_KEY_NODE(objects.StructType): except (RegistryInvalidIndex, RegistryFormatException) as excp: vollog.debug(f"Invalid address {excp}") continue - if node.vol.type_name.endswith(constants.BANG + '_CM_KEY_VALUE'): + if node.vol.type_name.endswith(constants.BANG + "_CM_KEY_VALUE"): yield node except (exceptions.InvalidAddressException, RegistryFormatException) as excp: vollog.debug(f"Invalid address in get_values iteration: {excp}") - return + return None def get_name(self) -> interfaces.objects.ObjectInterface: """Gets the name for the current key node""" namelength = self.NameLength self.Name.count = namelength - return self.Name.cast("string", max_length = namelength, encoding = "latin-1") + return self.Name.cast("string", max_length=namelength, encoding="latin-1") def get_key_path(self) -> str: reg = self._context.layers[self.vol.layer_name] @@ -214,8 +236,8 @@ class CM_KEY_NODE(objects.StructType): # if self.vol.offset == reg.get_node(reg.root_cell_offset).vol.offset: if self.vol.offset == reg.root_cell_offset + 4: # return the last part of the hive name for the root entry - return reg.get_name().split('\\')[-1] - return reg.get_node(self.Parent).get_key_path() + '\\' + self.get_name() + return reg.get_name().split("\\")[-1] + return reg.get_node(self.Parent).get_key_path() + "\\" + self.get_name() class CM_KEY_VALUE(objects.StructType): @@ -225,7 +247,7 @@ class CM_KEY_VALUE(objects.StructType): """Gets the name for the current key value""" namelength = self.NameLength self.Name.count = namelength - return self.Name.cast("string", max_length = namelength, encoding = "latin-1") + return self.Name.cast("string", max_length=namelength, encoding="latin-1") def decode_data(self) -> Union[int, bytes]: """Properly decodes the data associated with the value node""" @@ -240,9 +262,11 @@ class CM_KEY_VALUE(objects.StructType): # If the high-bit is set if datalen & 0x80000000: # Remove the high bit - datalen = datalen & 0x7fffffff - if (0 > datalen or datalen > 4): - raise ValueError(f"Unable to read inline registry value with excessive length: {datalen}") + datalen = datalen & 0x7FFFFFFF + if 0 > datalen or datalen > 4: + raise ValueError( + f"Unable to read inline registry value with excessive length: {datalen}" + ) else: data = layer.read(self.Data.vol.offset, datalen) elif layer.hive.Version == 5 and datalen > 0x4000: @@ -252,10 +276,17 @@ class CM_KEY_VALUE(objects.StructType): for i in range(big_data.Count): # The value 4 should actually be unsigned-int.size, but since it's a file format that shouldn't change # the direct value 4 can be used instead - block_offset = layer.get_cell(big_data.List + (i * 4)).cast("unsigned int") - if isinstance(block_offset, int) and block_offset < layer.maximum_address: + block_offset = layer.get_cell(big_data.List + (i * 4)).cast( + "unsigned int" + ) + if ( + isinstance(block_offset, int) + and block_offset < layer.maximum_address + ): amount = min(BIG_DATA_MAXLEN, datalen) - data += layer.read(offset = layer.get_cell(block_offset).vol.offset, length = amount) + data += layer.read( + offset=layer.get_cell(block_offset).vol.offset, length=amount + ) datalen -= amount else: # Suspect Data actually points to a Cell, @@ -265,27 +296,38 @@ class CM_KEY_VALUE(objects.StructType): self_type = RegValueTypes(self.Type) if self_type == RegValueTypes.REG_DWORD: if len(data) != struct.calcsize("L"): - raise ValueError(f"Size of data does not match the type of registry value {self.get_name()}") - res, = struct.unpack(">L", data) + raise ValueError( + f"Size of data does not match the type of registry value {self.get_name()}" + ) + (res,) = struct.unpack(">L", data) return res if self_type == RegValueTypes.REG_QWORD: if len(data) != struct.calcsize(" Union[int, interfaces.renderers.BaseAbsentValue]: """Return the pid of the process, if any.""" - if self.State.description != "SERVICE_RUNNING" or "PROCESS" not in self.get_type(): + if ( + self.State.description != "SERVICE_RUNNING" + or "PROCESS" not in self.get_type() + ): return renderers.NotApplicableValue() try: @@ -44,35 +47,31 @@ class SERVICE_RECORD(objects.StructType): # or kernel driver, the binary path is stored differently try: if "PROCESS" in self.get_type(): - return self.ServiceProcess.BinaryPath.dereference().cast("string", - encoding = "utf-16", - errors = "replace", - max_length = 512) + return self.ServiceProcess.BinaryPath.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) else: - return self.DriverName.dereference().cast("string", - encoding = "utf-16", - errors = "replace", - max_length = 512) + return self.DriverName.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) except exceptions.InvalidAddressException: return renderers.UnreadableValue() def get_name(self) -> Union[str, interfaces.renderers.BaseAbsentValue]: """Returns the service name.""" try: - return self.ServiceName.dereference().cast("string", - encoding = "utf-16", - errors = "replace", - max_length = 512) + return self.ServiceName.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) except exceptions.InvalidAddressException: return renderers.UnreadableValue() def get_display(self) -> Union[str, interfaces.renderers.BaseAbsentValue]: """Returns the service display.""" try: - return self.DisplayName.dereference().cast("string", - encoding = "utf-16", - errors = "replace", - max_length = 512) + return self.DisplayName.dereference().cast( + "string", encoding="utf-16", errors="replace", max_length=512 + ) except exceptions.InvalidAddressException: return renderers.UnreadableValue() @@ -80,16 +79,16 @@ class SERVICE_RECORD(objects.StructType): """Returns the binary types.""" SERVICE_TYPE_FLAGS = { - 'SERVICE_KERNEL_DRIVER': 1, - 'SERVICE_FILE_SYSTEM_DRIVER': 2, - 'SERVICE_ADAPTOR': 4, - 'SERVICE_RECOGNIZER_DRIVER': 8, - 'SERVICE_WIN32_OWN_PROCESS': 16, - 'SERVICE_WIN32_SHARE_PROCESS': 32, - 'SERVICE_INTERACTIVE_PROCESS': 256 + "SERVICE_KERNEL_DRIVER": 1, + "SERVICE_FILE_SYSTEM_DRIVER": 2, + "SERVICE_ADAPTOR": 4, + "SERVICE_RECOGNIZER_DRIVER": 8, + "SERVICE_WIN32_OWN_PROCESS": 16, + "SERVICE_WIN32_SHARE_PROCESS": 32, + "SERVICE_INTERACTIVE_PROCESS": 256, } - type_flags = Flags(choices = SERVICE_TYPE_FLAGS) + type_flags = Flags(choices=SERVICE_TYPE_FLAGS) return "|".join(type_flags(self.Type)) def traverse(self): @@ -111,7 +110,7 @@ class SERVICE_RECORD(objects.StructType): yield rec rec = rec.ServiceList.Blink.dereference() except exceptions.InvalidAddressException: - return + return None class SERVICE_HEADER(objects.StructType): @@ -125,4 +124,4 @@ class SERVICE_HEADER(objects.StructType): return False -class_types = {'_SERVICE_RECORD': SERVICE_RECORD, '_SERVICE_HEADER': SERVICE_HEADER} +class_types = {"_SERVICE_RECORD": SERVICE_RECORD, "_SERVICE_HEADER": SERVICE_HEADER} diff --git a/volatility3/framework/symbols/windows/mft.json b/volatility3/framework/symbols/windows/mft.json index e5de8f3fa..d4f2aef7a 100644 --- a/volatility3/framework/symbols/windows/mft.json +++ b/volatility3/framework/symbols/windows/mft.json @@ -230,21 +230,21 @@ "offset": 0, "type": { "kind": "struct", - "name": "mft!ATTR_HEADER" + "name": "ATTR_HEADER" } }, "Resident_Header": { "offset": 16, "type": { "kind": "struct", - "name": "mft!RESIDENT_HEADER" + "name": "RESIDENT_HEADER" } }, "Attr_Data": { "offset": 24, "type": { "kind": "struct", - "name": "mft!ATTR_HEADER" + "name": "ATTR_HEADER" } } }, @@ -300,10 +300,24 @@ "kind": "base", "name": "unsigned short" } + }, + "ContentLength": { + "offset": 16, + "type": { + "kind": "base", + "name": "unsigned int" + } + }, + "ContentOffset": { + "offset": 20, + "type": { + "kind": "base", + "name": "unsigned short" + } } }, "kind": "struct", - "size": 16 + "size": 24 },"RESIDENT_HEADER": { "fields": { "AttrSize": { diff --git a/volatility3/framework/symbols/windows/pdbconv.py b/volatility3/framework/symbols/windows/pdbconv.py index da8254ffd..82ec31ccb 100644 --- a/volatility3/framework/symbols/windows/pdbconv.py +++ b/volatility3/framework/symbols/windows/pdbconv.py @@ -19,220 +19,79 @@ from volatility3.framework.layers import physical, msf, resources vollog = logging.getLogger(__name__) primitives = { - 0x03: ("void", { - "endian": "little", - "kind": "void", - "signed": True, - "size": 0 - }), - 0x08: ("HRESULT", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 4 - }), - 0x10: ("char", { - "endian": "little", - "kind": "char", - "signed": True, - "size": 1 - }), - 0x20: ("unsigned char", { - "endian": "little", - "kind": "char", - "signed": False, - "size": 1 - }), - 0x68: ("int8", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 1 - }), - 0x69: ("uint8", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 1 - }), - 0x70: ("char", { - "endian": "little", - "kind": "char", - "signed": True, - "size": 1 - }), - 0x71: ("wchar", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 2 - }), + 0x03: ("void", {"endian": "little", "kind": "void", "signed": True, "size": 0}), + 0x08: ("HRESULT", {"endian": "little", "kind": "int", "signed": False, "size": 4}), + 0x10: ("char", {"endian": "little", "kind": "char", "signed": True, "size": 1}), + 0x20: ( + "unsigned char", + {"endian": "little", "kind": "char", "signed": False, "size": 1}, + ), + 0x68: ("int8", {"endian": "little", "kind": "int", "signed": True, "size": 1}), + 0x69: ("uint8", {"endian": "little", "kind": "int", "signed": False, "size": 1}), + 0x70: ("char", {"endian": "little", "kind": "char", "signed": True, "size": 1}), + 0x71: ("wchar", {"endian": "little", "kind": "int", "signed": True, "size": 2}), # 0x7a: ("rchar16", {}), # 0x7b: ("rchar32", {}), - 0x11: ("short", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 2 - }), - 0x21: ("unsigned short", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 2 - }), - 0x72: ("short", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 2 - }), - 0x73: ("unsigned short", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 2 - }), - 0x12: ("long", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 4 - }), - 0x22: ("unsigned long", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 4 - }), - 0x74: ("int", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 4 - }), - 0x75: ("unsigned int", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 4 - }), - 0x13: ("long long", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 8 - }), - 0x23: ("unsigned long long", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 8 - }), - 0x76: ("long long", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 8 - }), - 0x77: ("unsigned long long", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 8 - }), - 0x14: ("int128", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 16 - }), - 0x24: ("uint128", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 16 - }), - 0x78: ("int128", { - "endian": "little", - "kind": "int", - "signed": True, - "size": 16 - }), - 0x79: ("uint128", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 16 - }), - 0x46: ("f16", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 2 - }), - 0x40: ("f32", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 4 - }), - 0x45: ("f32pp", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 4 - }), - 0x44: ("f48", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 6 - }), - 0x41: ("double", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 8 - }), - 0x42: ("f80", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 10 - }), - 0x43: ("f128", { - "endian": "little", - "kind": "float", - "signed": True, - "size": 16 - }) + 0x11: ("short", {"endian": "little", "kind": "int", "signed": True, "size": 2}), + 0x21: ( + "unsigned short", + {"endian": "little", "kind": "int", "signed": False, "size": 2}, + ), + 0x72: ("short", {"endian": "little", "kind": "int", "signed": True, "size": 2}), + 0x73: ( + "unsigned short", + {"endian": "little", "kind": "int", "signed": False, "size": 2}, + ), + 0x12: ("long", {"endian": "little", "kind": "int", "signed": True, "size": 4}), + 0x22: ( + "unsigned long", + {"endian": "little", "kind": "int", "signed": False, "size": 4}, + ), + 0x74: ("int", {"endian": "little", "kind": "int", "signed": True, "size": 4}), + 0x75: ( + "unsigned int", + {"endian": "little", "kind": "int", "signed": False, "size": 4}, + ), + 0x13: ("long long", {"endian": "little", "kind": "int", "signed": True, "size": 8}), + 0x23: ( + "unsigned long long", + {"endian": "little", "kind": "int", "signed": False, "size": 8}, + ), + 0x76: ("long long", {"endian": "little", "kind": "int", "signed": True, "size": 8}), + 0x77: ( + "unsigned long long", + {"endian": "little", "kind": "int", "signed": False, "size": 8}, + ), + 0x14: ("int128", {"endian": "little", "kind": "int", "signed": True, "size": 16}), + 0x24: ("uint128", {"endian": "little", "kind": "int", "signed": False, "size": 16}), + 0x78: ("int128", {"endian": "little", "kind": "int", "signed": True, "size": 16}), + 0x79: ("uint128", {"endian": "little", "kind": "int", "signed": False, "size": 16}), + 0x46: ("f16", {"endian": "little", "kind": "float", "signed": True, "size": 2}), + 0x40: ("f32", {"endian": "little", "kind": "float", "signed": True, "size": 4}), + 0x45: ("f32pp", {"endian": "little", "kind": "float", "signed": True, "size": 4}), + 0x44: ("f48", {"endian": "little", "kind": "float", "signed": True, "size": 6}), + 0x41: ("double", {"endian": "little", "kind": "float", "signed": True, "size": 8}), + 0x42: ("f80", {"endian": "little", "kind": "float", "signed": True, "size": 10}), + 0x43: ("f128", {"endian": "little", "kind": "float", "signed": True, "size": 16}), } indirections = { - 0x100: ("pointer16", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 2 - }), - 0x400: ("pointer32", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 4 - }), - 0x600: ("pointer64", { - "endian": "little", - "kind": "int", - "signed": False, - "size": 8 - }) + 0x100: ( + "pointer16", + {"endian": "little", "kind": "int", "signed": False, "size": 2}, + ), + 0x400: ( + "pointer32", + {"endian": "little", "kind": "int", "signed": False, "size": 4}, + ), + 0x600: ( + "pointer64", + {"endian": "little", "kind": "int", "signed": False, "size": 8}, + ), } class ForwardArrayCount: - def __init__(self, size, element_type): self.element_type = element_type self.size = size @@ -259,19 +118,25 @@ class PdbReader: sized data following. """ - def __init__(self, - context: interfaces.context.ContextInterface, - location: str, - database_name: Optional[str] = None, - progress_callback: constants.ProgressCallback = None) -> None: + def __init__( + self, + context: interfaces.context.ContextInterface, + location: str, + database_name: Optional[str] = None, + progress_callback: constants.ProgressCallback = None, + ) -> None: self._layer_name, self._context = self.load_pdb_layer(context, location) self._dbiheader: Optional[interfaces.objects.ObjectInterface] = None if not progress_callback: progress_callback = lambda x, y: None self._progress_callback = progress_callback self.types: List[ - Tuple[interfaces.objects.ObjectInterface, Optional[str], interfaces.objects.ObjectInterface]] = [ - ] + Tuple[ + interfaces.objects.ObjectInterface, + Optional[str], + interfaces.objects.ObjectInterface, + ] + ] = [] self.bases: Dict[str, Any] = {} self.user_types: Dict[str, Any] = {} self.enumerations: Dict[str, Any] = {} @@ -290,29 +155,42 @@ class PdbReader: return self._layer_name @classmethod - def load_pdb_layer(cls, context: interfaces.context.ContextInterface, - location: str) -> Tuple[str, interfaces.context.ContextInterface]: + def load_pdb_layer( + cls, context: interfaces.context.ContextInterface, location: str + ) -> Tuple[str, interfaces.context.ContextInterface]: """Loads a PDB file into a layer within the context and returns the name of the new layer. Note: the context may be changed by this method """ physical_layer_name = context.layers.free_layer_name("FileLayer") - physical_config_path = interfaces.configuration.path_join("pdbreader", physical_layer_name) + physical_config_path = interfaces.configuration.path_join( + "pdbreader", physical_layer_name + ) # Create the file layer # This must be specific to get us started, setup the config and run new_context = context.clone() - new_context.config[interfaces.configuration.path_join(physical_config_path, "location")] = location + new_context.config[ + interfaces.configuration.path_join(physical_config_path, "location") + ] = location - physical_layer = physical.FileLayer(new_context, physical_config_path, physical_layer_name) + physical_layer = physical.FileLayer( + new_context, physical_config_path, physical_layer_name + ) new_context.add_layer(physical_layer) # Add on the MSF format layer msf_layer_name = context.layers.free_layer_name("MSFLayer") - msf_config_path = interfaces.configuration.path_join("pdbreader", msf_layer_name) - new_context.config[interfaces.configuration.path_join(msf_config_path, "base_layer")] = physical_layer_name - msf_layer = msf.PdbMultiStreamFormat(new_context, msf_config_path, msf_layer_name) + msf_config_path = interfaces.configuration.path_join( + "pdbreader", msf_layer_name + ) + new_context.config[ + interfaces.configuration.path_join(msf_config_path, "base_layer") + ] = physical_layer_name + msf_layer = msf.PdbMultiStreamFormat( + new_context, msf_config_path, msf_layer_name + ) new_context.add_layer(msf_layer) msf_layer.read_streams() @@ -330,7 +208,7 @@ class PdbReader: def read_necessary_streams(self): """Read streams to populate the various internal components for a PDB table.""" - if not self.metadata['windows'].get('pdb', None): + if not self.metadata["windows"].get("pdb", None): self.read_pdb_info_stream() if not self.user_types: self.read_tpi_stream() @@ -358,28 +236,37 @@ class PdbReader: type_references = self._read_info_stream(4, "IPI", ipi_list) for name in type_references.keys(): # This doesn't break, because we want to use the last string/pdbname in the list - if name.endswith('.pdb'): - self._database_name = name.split('\\')[-1] + if name.endswith(".pdb"): + self._database_name = name.split("\\")[-1] except ValueError: return None def _read_info_stream(self, stream_number, stream_name, info_list): vollog.debug(f"Reading {stream_name}") - info_layer = self._context.layers.get(self._layer_name + "_stream" + str(stream_number), None) + info_layer = self._context.layers.get( + self._layer_name + "_stream" + str(stream_number), None + ) if not info_layer: raise ValueError(f"No {stream_name} stream available") - module = self._context.module(module_name = info_layer.pdb_symbol_table, - layer_name = info_layer.name, - offset = 0) - header = module.object(object_type = "TPI_HEADER", offset = 0) + module = self._context.module( + module_name=info_layer.pdb_symbol_table, + layer_name=info_layer.name, + offset=0, + ) + header = module.object(object_type="TPI_HEADER", offset=0) # Check the header if not (56 <= header.header_size < 1024): raise ValueError(f"{stream_name} Stream Header size outside normal bounds") if header.index_min < 4096: - raise ValueError(f"Minimum {stream_name} index is 4096, found: {header.index_min}") + raise ValueError( + f"Minimum {stream_name} index is 4096, found: {header.index_min}" + ) if header.index_max < header.index_min: - raise ValueError("Maximum {} index is smaller than minimum TPI index, found: {} < {} ".format( - stream_name, header.index_max, header.index_min)) + raise ValueError( + "Maximum {} index is smaller than minimum TPI index, found: {} < {} ".format( + stream_name, header.index_max, header.index_min + ) + ) # Reset the state info_references: Dict[str, int] = {} offset = header.header_size @@ -388,16 +275,18 @@ class PdbReader: length_len = module.get_type(length_type).size info_index = 1 while info_layer.maximum_address - offset > 0: - self._progress_callback(offset * 100 / info_layer.maximum_address, "Reading TPI layer") - length = module.object(object_type = length_type, offset = offset) + self._progress_callback( + offset * 100 / info_layer.maximum_address, "Reading TPI layer" + ) + length = module.object(object_type=length_type, offset=offset) if not isinstance(length, int): raise TypeError("Non-integer length provided") offset += length_len output, consumed = self.consume_type(module, offset, length) leaf_type, name, value = output - for tag_type in ['unnamed', 'anonymous']: - if name == f'<{tag_type}-tag>' or name == f'__{tag_type}': - name = f'__{tag_type}_' + hex(len(info_list) + 0x1000)[2:] + for tag_type in ["unnamed", "anonymous"]: + if name == f"<{tag_type}-tag>" or name == f"__{tag_type}": + name = f"__{tag_type}_" + hex(len(info_list) + 0x1000)[2:] if name: info_references[name] = len(info_list) info_list.append((leaf_type, name, value)) @@ -414,47 +303,80 @@ class PdbReader: dbi_layer = self._context.layers.get(self._layer_name + "_stream3", None) if not dbi_layer: raise ValueError("No DBI stream available") - module = self._context.module(module_name = dbi_layer.pdb_symbol_table, layer_name = dbi_layer.name, offset = 0) - self._dbiheader = module.object(object_type = "DBI_HEADER", offset = 0) + module = self._context.module( + module_name=dbi_layer.pdb_symbol_table, layer_name=dbi_layer.name, offset=0 + ) + self._dbiheader = module.object(object_type="DBI_HEADER", offset=0) if not self._dbiheader: raise ValueError("DBI Header could not be read") # Skip past sections we don't care about to get to the DBG header - dbg_hdr_offset = (self._dbiheader.vol.size + self._dbiheader.module_size + self._dbiheader.secconSize + - self._dbiheader.secmapSize + self._dbiheader.filinfSize + self._dbiheader.tsmapSize + - self._dbiheader.ecinfoSize) - self._dbidbgheader = module.object(object_type = "DBI_DBG_HEADER", offset = dbg_hdr_offset) + dbg_hdr_offset = ( + self._dbiheader.vol.size + + self._dbiheader.module_size + + self._dbiheader.secconSize + + self._dbiheader.secmapSize + + self._dbiheader.filinfSize + + self._dbiheader.tsmapSize + + self._dbiheader.ecinfoSize + ) + self._dbidbgheader = module.object( + object_type="DBI_DBG_HEADER", offset=dbg_hdr_offset + ) self._sections = [] self._omap_mapping = [] if self._dbidbgheader.snSectionHdrOrig != -1: - section_orig_layer_name = self._layer_name + "_stream" + str(self._dbidbgheader.snSectionHdrOrig) - consumed, length = 0, self.context.layers[section_orig_layer_name].maximum_address + section_orig_layer_name = ( + self._layer_name + "_stream" + str(self._dbidbgheader.snSectionHdrOrig) + ) + consumed, length = ( + 0, + self.context.layers[section_orig_layer_name].maximum_address, + ) while consumed < length: - section = self.context.object(dbi_layer.pdb_symbol_table + constants.BANG + "IMAGE_SECTION_HEADER", - offset = consumed, - layer_name = section_orig_layer_name) + section = self.context.object( + dbi_layer.pdb_symbol_table + + constants.BANG + + "IMAGE_SECTION_HEADER", + offset=consumed, + layer_name=section_orig_layer_name, + ) self._sections.append(section) consumed += section.vol.size if self._dbidbgheader.snOmapFromSrc != -1: - omap_layer_name = self._layer_name + "_stream" + str(self._dbidbgheader.snOmapFromSrc) + omap_layer_name = ( + self._layer_name + "_stream" + str(self._dbidbgheader.snOmapFromSrc) + ) length = self.context.layers[omap_layer_name].maximum_address data = self.context.layers[omap_layer_name].read(0, length) # For speed we don't use the framework to read this (usually sizeable) data for i in range(0, length, 8): self._omap_mapping.append( - (int.from_bytes(data[i:i + 4], - byteorder = 'little'), int.from_bytes(data[i + 4:i + 8], byteorder = 'little'))) + ( + int.from_bytes(data[i : i + 4], byteorder="little"), + int.from_bytes(data[i + 4 : i + 8], byteorder="little"), + ) + ) elif self._dbidbgheader.snSectionHdr != -1: - section_layer_name = self._layer_name + "_stream" + str(self._dbidbgheader.snSectionHdr) - consumed, length = 0, self.context.layers[section_layer_name].maximum_address + section_layer_name = ( + self._layer_name + "_stream" + str(self._dbidbgheader.snSectionHdr) + ) + consumed, length = ( + 0, + self.context.layers[section_layer_name].maximum_address, + ) while consumed < length: - section = self.context.object(dbi_layer.pdb_symbol_table + constants.BANG + "IMAGE_SECTION_HEADER", - offset = consumed, - layer_name = section_layer_name) + section = self.context.object( + dbi_layer.pdb_symbol_table + + constants.BANG + + "IMAGE_SECTION_HEADER", + offset=consumed, + layer_name=section_layer_name, + ) self._sections.append(section) consumed += section.vol.size @@ -467,31 +389,45 @@ class PdbReader: vollog.debug("Reading Symbols") - symrec_layer = self._context.layers.get(self._layer_name + "_stream" + str(self._dbiheader.symrecStream), None) + symrec_layer = self._context.layers.get( + self._layer_name + "_stream" + str(self._dbiheader.symrecStream), None + ) if not symrec_layer: raise ValueError("No SymRec stream available") - module = self._context.module(module_name = symrec_layer.pdb_symbol_table, - layer_name = symrec_layer.name, - offset = 0) + module = self._context.module( + module_name=symrec_layer.pdb_symbol_table, + layer_name=symrec_layer.name, + offset=0, + ) offset = 0 max_address = symrec_layer.maximum_address while offset < max_address: self._progress_callback(offset * 100 / max_address, "Reading Symbol layer") - sym = module.object(object_type = "GLOBAL_SYMBOL", offset = offset) - leaf_type = module.object(object_type = "unsigned short", offset = sym.leaf_type.vol.offset) + sym = module.object(object_type="GLOBAL_SYMBOL", offset=offset) + leaf_type = module.object( + object_type="unsigned short", offset=sym.leaf_type.vol.offset + ) name = None address = None if sym.segment < len(self._sections): if leaf_type == 0x1009: # v2 symbol (pascal-string) - name = self.parse_string(sym.name, True, sym.length - sym.vol.size + 2) - address = self._sections[sym.segment - 1].VirtualAddress + sym.offset - elif leaf_type == 0x110e or leaf_type == 0x1127: + name = self.parse_string( + sym.name, True, sym.length - sym.vol.size + 2 + ) + address = ( + self._sections[sym.segment - 1].VirtualAddress + sym.offset + ) + elif leaf_type == 0x110E or leaf_type == 0x1127: # v3 symbol (c-string) - name = self.parse_string(sym.name, False, sym.length - sym.vol.size + 2) - address = self._sections[sym.segment - 1].VirtualAddress + sym.offset + name = self.parse_string( + sym.name, False, sym.length - sym.vol.size + 2 + ) + address = ( + self._sections[sym.segment - 1].VirtualAddress + sym.offset + ) else: vollog.debug(f"Only v2 and v3 symbols are supported: {leaf_type:x}") if name: @@ -514,16 +450,18 @@ class PdbReader: pdb_info_layer = self._context.layers.get(self._layer_name + "_stream1", None) if not pdb_info_layer: raise ValueError("No PDB Info Stream available") - module = self._context.module(module_name = pdb_info_layer.pdb_symbol_table, - layer_name = pdb_info_layer.name, - offset = 0) - pdb_info = module.object(object_type = "PDB_INFORMATION", offset = 0) + module = self._context.module( + module_name=pdb_info_layer.pdb_symbol_table, + layer_name=pdb_info_layer.name, + offset=0, + ) + pdb_info = module.object(object_type="PDB_INFORMATION", offset=0) - self.metadata['windows']['pdb'] = { + self.metadata["windows"]["pdb"] = { "GUID": self.convert_bytes_to_guid(pdb_info.GUID), - "age": pdb_info.age, - "database": self._database_name or 'unknown.pdb', - "machine_type": self._dbiheader.machine + "age": self._dbiheader.age, + "database": self._database_name or "unknown.pdb", + "machine_type": self._dbiheader.machine, } def convert_bytes_to_guid(self, original: bytes) -> str: @@ -570,7 +508,7 @@ class PdbReader: self.metadata["producer"] = { "datetime": datetime.datetime.now().isoformat(), "name": "volatility3", - "version": constants.PACKAGE_VERSION + "version": constants.PACKAGE_VERSION, } return { @@ -584,45 +522,67 @@ class PdbReader: def get_type_from_index(self, index: int) -> Union[List[Any], Dict[str, Any]]: """Takes a type index and returns appropriate dictionary.""" if index < 0x1000: - base_name, base = primitives[index & 0xff] + base_name, base = primitives[index & 0xFF] self.bases[base_name] = base - result: Union[List[Dict[str, Any]], Dict[str, Any]] = {"kind": "base", "name": base_name} - indirection = (index & 0xf00) + result: Union[List[Dict[str, Any]], Dict[str, Any]] = { + "kind": "base", + "name": base_name, + } + indirection = index & 0xF00 if indirection: pointer_name, pointer_base = indirections[indirection] - if self.bases.get('pointer', None) and self.bases['pointer'] == pointer_base: + if ( + self.bases.get("pointer", None) + and self.bases["pointer"] == pointer_base + ): result = {"kind": "pointer", "subtype": result} else: self.bases[pointer_name] = pointer_base - result = {"kind": "pointer", "base": pointer_name, "subtype": result} + result = { + "kind": "pointer", + "base": pointer_name, + "subtype": result, + } return result else: leaf_type, name, value = self.types[index - 0x1000] result = {"kind": "struct", "name": name} if leaf_type in [leaf_type.LF_MODIFIER]: result = self.get_type_from_index(value.subtype_index) - elif leaf_type in [leaf_type.LF_ARRAY, leaf_type.LF_ARRAY_ST, leaf_type.LF_STRIDED_ARRAY]: + elif leaf_type in [ + leaf_type.LF_ARRAY, + leaf_type.LF_ARRAY_ST, + leaf_type.LF_STRIDED_ARRAY, + ]: result = { "count": ForwardArrayCount(value.size, value.element_type), "kind": "array", - "subtype": self.get_type_from_index(value.element_type) + "subtype": self.get_type_from_index(value.element_type), } elif leaf_type in [leaf_type.LF_BITFIELD]: result = { "kind": "bitfield", "type": self.get_type_from_index(value.underlying_type), "bit_length": value.length, - "bit_position": value.position + "bit_position": value.position, } elif leaf_type in [leaf_type.LF_POINTER]: # Since we use the base['pointer'] to set the size for pointers, update it and check we don't get conflicts size = self.get_size_from_index(index) if self.bases.get("pointer", None) is None: - self.bases['pointer'] = {"endian": "little", "kind": "int", "signed": False, "size": size} + self.bases["pointer"] = { + "endian": "little", + "kind": "int", + "signed": False, + "size": size, + } else: - if size != self.bases['pointer']['size']: + if size != self.bases["pointer"]["size"]: raise ValueError("Native pointers with different sizes!") - result = {"kind": "pointer", "subtype": self.get_type_from_index(value.subtype_index)} + result = { + "kind": "pointer", + "subtype": self.get_type_from_index(value.subtype_index), + } elif leaf_type in [leaf_type.LF_PROCEDURE]: return {"kind": "function"} elif leaf_type in [leaf_type.LF_UNION]: @@ -639,24 +599,38 @@ class PdbReader: """Returns the size of the structure based on the type index provided.""" result = -1 - name: Optional[str] = '' + name: Optional[str] = "" if index < 0x1000: - if (index & 0xf00): - _, base = indirections[index & 0xf00] + if index & 0xF00: + _, base = indirections[index & 0xF00] else: - _, base = primitives[index & 0xff] - result = base['size'] + _, base = primitives[index & 0xFF] + result = base["size"] else: leaf_type, name, value = self.types[index - 0x1000] if leaf_type in [ - leaf_type.LF_UNION, leaf_type.LF_CLASS, leaf_type.LF_CLASS_ST, leaf_type.LF_STRUCTURE, - leaf_type.LF_STRUCTURE_ST, leaf_type.LF_INTERFACE, leaf_type.LF_CLASS_VS19, leaf_type.LF_STRUCTURE_VS19 + leaf_type.LF_UNION, + leaf_type.LF_CLASS, + leaf_type.LF_CLASS_ST, + leaf_type.LF_STRUCTURE, + leaf_type.LF_STRUCTURE_ST, + leaf_type.LF_INTERFACE, + leaf_type.LF_CLASS_VS19, + leaf_type.LF_STRUCTURE_VS19, ]: if not value.properties.forward_reference: result = value.size - elif leaf_type in [leaf_type.LF_ARRAY, leaf_type.LF_ARRAY_ST, leaf_type.LF_STRIDED_ARRAY]: + elif leaf_type in [ + leaf_type.LF_ARRAY, + leaf_type.LF_ARRAY_ST, + leaf_type.LF_STRIDED_ARRAY, + ]: result = value.size - elif leaf_type in [leaf_type.LF_MODIFIER, leaf_type.LF_ENUM, leaf_type.LF_ARGLIST]: + elif leaf_type in [ + leaf_type.LF_MODIFIER, + leaf_type.LF_ENUM, + leaf_type.LF_ARGLIST, + ]: result = self.get_size_from_index(value.subtype_index) elif leaf_type in [leaf_type.LF_MEMBER]: result = self.get_size_from_index(value.field_type) @@ -665,16 +639,18 @@ class PdbReader: elif leaf_type in [leaf_type.LF_POINTER]: result = value.size if not result: - if value.pointer_type == 0x0a: + if value.pointer_type == 0x0A: return 4 - elif value.pointer_type == 0x0c: + elif value.pointer_type == 0x0C: return 8 else: raise ValueError("Pointer size could not be determined") elif leaf_type in [leaf_type.LF_PROCEDURE]: raise ValueError("LF_PROCEDURE size could not be identified") else: - raise ValueError(f"Unable to determine size of leaf_type {leaf_type.lookup()}") + raise ValueError( + f"Unable to determine size of leaf_type {leaf_type.lookup()}" + ) if result <= 0: raise ValueError(f"Invalid size identified: {index} ({name})") return result @@ -694,14 +670,19 @@ class PdbReader: self._progress_callback(index * 100 / max_len, "Processing types") leaf_type, name, value = self.types[index] if leaf_type in [ - leaf_type.LF_CLASS, leaf_type.LF_CLASS_ST, leaf_type.LF_STRUCTURE, leaf_type.LF_STRUCTURE_ST, - leaf_type.LF_INTERFACE, leaf_type.LF_CLASS_VS19, leaf_type.LF_STRUCTURE_VS19 + leaf_type.LF_CLASS, + leaf_type.LF_CLASS_ST, + leaf_type.LF_STRUCTURE, + leaf_type.LF_STRUCTURE_ST, + leaf_type.LF_INTERFACE, + leaf_type.LF_CLASS_VS19, + leaf_type.LF_STRUCTURE_VS19, ]: if not value.properties.forward_reference and name: self.user_types[name] = { "kind": "struct", "size": value.size, - "fields": self.convert_fields(value.fields - 0x1000) + "fields": self.convert_fields(value.fields - 0x1000), } elif leaf_type in [leaf_type.LF_UNION]: if not value.properties.forward_reference and name: @@ -709,7 +690,7 @@ class PdbReader: self.user_types[name] = { "kind": "union", "size": value.size, - "fields": self.convert_fields(value.fields - 0x1000) + "fields": self.convert_fields(value.fields - 0x1000), } elif leaf_type in [leaf_type.LF_ENUM]: if not value.properties.forward_reference and name: @@ -720,89 +701,117 @@ class PdbReader: if not isinstance(constants, list): raise ValueError("Enumeration fields type not a list") self.enumerations[name] = { - 'base': base['name'], - 'size': self.get_size_from_index(value.subtype_index), - 'constants': dict([(name, enum.value) for _, name, enum in constants]) + "base": base["name"], + "size": self.get_size_from_index(value.subtype_index), + "constants": dict( + [(name, enum.value) for _, name, enum in constants] + ), } # Re-run through for ForwardSizeReferences - self.user_types = self.replace_forward_references(self.user_types, type_references) + self.user_types = self.replace_forward_references( + self.user_types, type_references + ) type_handlers = { # Leaf_type: ('Structure', has_name, value_attribute) - 'LF_CLASS': ('LF_STRUCTURE', True, 'size'), - 'LF_CLASS_ST': ('LF_STRUCTURE', True, 'size'), - 'LF_STRUCTURE': ('LF_STRUCTURE', True, 'size'), - 'LF_STRUCTURE_ST': ('LF_STRUCTURE', True, 'size'), - 'LF_INTERFACE': ('LF_STRUCTURE', True, 'size'), - 'LF_CLASS_VS19': ('LF_STRUCTURE_VS19', True, 'size'), - 'LF_STRUCTURE_VS19': ('LF_STRUCTURE_VS19', True, 'size'), - 'LF_MEMBER': ('LF_MEMBER', True, 'offset'), - 'LF_MEMBER_ST': ('LF_MEMBER', True, 'offset'), - 'LF_ARRAY': ('LF_ARRAY', True, 'size'), - 'LF_ARRAY_ST': ('LF_ARRAY', True, 'size'), - 'LF_STRIDED_ARRAY': ('LF_ARRAY', True, 'size'), - 'LF_ENUMERATE': ('LF_ENUMERATE', True, 'value'), - 'LF_ARGLIST': ('LF_ENUM', True, None), - 'LF_ENUM': ('LF_ENUM', True, None), - 'LF_UNION': ('LF_UNION', True, None), - 'LF_STRING_ID': ('LF_STRING_ID', True, None), - 'LF_FUNC_ID': ('LF_FUNC_ID', True, None), - 'LF_MODIFIER': ('LF_MODIFIER', False, None), - 'LF_POINTER': ('LF_POINTER', False, None), - 'LF_PROCEDURE': ('LF_PROCEDURE', False, None), - 'LF_FIELDLIST': ('LF_FIELDLIST', False, None), - 'LF_BITFIELD': ('LF_BITFIELD', False, None), - 'LF_UDT_SRC_LINE': ('LF_UDT_SRC_LINE', False, None), - 'LF_UDT_MOD_SRC_LINE': ('LF_UDT_MOD_SRC_LINE', False, None), - 'LF_BUILDINFO': ('LF_BUILDINFO', False, None) + "LF_CLASS": ("LF_STRUCTURE", True, "size"), + "LF_CLASS_ST": ("LF_STRUCTURE", True, "size"), + "LF_STRUCTURE": ("LF_STRUCTURE", True, "size"), + "LF_STRUCTURE_ST": ("LF_STRUCTURE", True, "size"), + "LF_INTERFACE": ("LF_STRUCTURE", True, "size"), + "LF_CLASS_VS19": ("LF_STRUCTURE_VS19", True, "size"), + "LF_STRUCTURE_VS19": ("LF_STRUCTURE_VS19", True, "size"), + "LF_MEMBER": ("LF_MEMBER", True, "offset"), + "LF_MEMBER_ST": ("LF_MEMBER", True, "offset"), + "LF_ARRAY": ("LF_ARRAY", True, "size"), + "LF_ARRAY_ST": ("LF_ARRAY", True, "size"), + "LF_STRIDED_ARRAY": ("LF_ARRAY", True, "size"), + "LF_ENUMERATE": ("LF_ENUMERATE", True, "value"), + "LF_ARGLIST": ("LF_ENUM", True, None), + "LF_ENUM": ("LF_ENUM", True, None), + "LF_UNION": ("LF_UNION", True, None), + "LF_STRING_ID": ("LF_STRING_ID", True, None), + "LF_FUNC_ID": ("LF_FUNC_ID", True, None), + "LF_MODIFIER": ("LF_MODIFIER", False, None), + "LF_POINTER": ("LF_POINTER", False, None), + "LF_PROCEDURE": ("LF_PROCEDURE", False, None), + "LF_FIELDLIST": ("LF_FIELDLIST", False, None), + "LF_BITFIELD": ("LF_BITFIELD", False, None), + "LF_UDT_SRC_LINE": ("LF_UDT_SRC_LINE", False, None), + "LF_UDT_MOD_SRC_LINE": ("LF_UDT_MOD_SRC_LINE", False, None), + "LF_BUILDINFO": ("LF_BUILDINFO", False, None), } def consume_type( - self, module: interfaces.context.ModuleInterface, offset: int, length: int - ) -> Tuple[Tuple[Optional[interfaces.objects.ObjectInterface], Optional[str], Union[ - None, List, interfaces.objects.ObjectInterface]], int]: + self, module: interfaces.context.ModuleInterface, offset: int, length: int + ) -> Tuple[ + Tuple[ + Optional[interfaces.objects.ObjectInterface], + Optional[str], + Union[None, List, interfaces.objects.ObjectInterface], + ], + int, + ]: """Returns a (leaf_type, name, object) Tuple for a type, and the number of bytes consumed.""" - leaf_type = self.context.object(module.get_enumeration("LEAF_TYPE"), - layer_name = module._layer_name, - offset = offset) + leaf_type = self.context.object( + module.get_enumeration("LEAF_TYPE"), + layer_name=module._layer_name, + offset=offset, + ) consumed = leaf_type.vol.base_type.size remaining = length - consumed - type_handler, has_name, value_attribute = self.type_handlers.get(leaf_type.lookup(), - ('LF_UNKNOWN', False, None)) + type_handler, has_name, value_attribute = self.type_handlers.get( + leaf_type.lookup(), ("LF_UNKNOWN", False, None) + ) - if type_handler in ['LF_FIELDLIST']: + if type_handler in ["LF_FIELDLIST"]: sub_length = remaining sub_offset = offset + consumed fields = [] while length > consumed: - subfield, sub_consumed = self.consume_type(module, sub_offset, sub_length) - sub_consumed += self.consume_padding(module.layer_name, sub_offset + sub_consumed) + subfield, sub_consumed = self.consume_type( + module, sub_offset, sub_length + ) + sub_consumed += self.consume_padding( + module.layer_name, sub_offset + sub_consumed + ) sub_length -= sub_consumed sub_offset += sub_consumed consumed += sub_consumed fields.append(subfield) result = leaf_type, None, fields - elif type_handler in ['LF_BUILDINFO']: - parsed_obj = module.object(object_type = type_handler, offset = offset + consumed) + elif type_handler in ["LF_BUILDINFO"]: + parsed_obj = module.object( + object_type=type_handler, offset=offset + consumed + ) parsed_obj.arguments.count = parsed_obj.count consumed += parsed_obj.arguments.vol.size result = leaf_type, None, parsed_obj elif type_handler in self.type_handlers: - parsed_obj = module.object(object_type = type_handler, offset = offset + consumed) + parsed_obj = module.object( + object_type=type_handler, offset=offset + consumed + ) current_consumed = remaining if has_name: name_offset = parsed_obj.name.vol.offset - parsed_obj.vol.offset if value_attribute: - name, value, excess = self.determine_extended_value(leaf_type, getattr(parsed_obj, value_attribute), - module, remaining - name_offset) + name, value, excess = self.determine_extended_value( + leaf_type, + getattr(parsed_obj, value_attribute), + module, + remaining - name_offset, + ) setattr(parsed_obj, value_attribute, value) current_consumed = parsed_obj.vol.size + len(name) + 1 + excess else: - name = self.parse_string(parsed_obj.name, leaf_type < leaf_type.LF_ST_MAX, - size = remaining - name_offset) + name = self.parse_string( + parsed_obj.name, + leaf_type < leaf_type.LF_ST_MAX, + size=remaining - name_offset, + ) parsed_obj.name = name else: name = None @@ -816,9 +825,9 @@ class PdbReader: def consume_padding(self, layer_name: str, offset: int) -> int: """Returns the amount of padding used between fields.""" val = self.context.layers[layer_name].read(offset, 1) - if not ((val[0] & 0xf0) == 0xf0): + if not ((val[0] & 0xF0) == 0xF0): return 0 - return (int(val[0]) & 0x0f) + return int(val[0]) & 0x0F def convert_fields(self, fields: int) -> Dict[Optional[str], Dict[str, Any]]: """Converts a field list into a list of fields.""" @@ -829,7 +838,10 @@ class PdbReader: return result for field in fields_struct: _, name, member = field - result[name] = {"offset": member.offset, "type": self.get_type_from_index(member.field_type)} + result[name] = { + "offset": member.offset, + "type": self.get_type_from_index(member.field_type), + } return result def replace_forward_references(self, types, type_references): @@ -852,9 +864,13 @@ class PdbReader: if element_type > 0x1000: _, name, toplevel_type = self.types[element_type - 0x1000] # If there's no name, the original size is probably fine as long as we're not indirect (LF_MODIFIER) - if not name and isinstance( - toplevel_type, - interfaces.objects.ObjectInterface) and toplevel_type.vol.type_name.endswith('LF_MODIFIER'): + if ( + not name + and isinstance( + toplevel_type, interfaces.objects.ObjectInterface + ) + and toplevel_type.vol.type_name.endswith("LF_MODIFIER") + ): # We have check they don't point to a forward reference, so we go round again with the subtype element_type = toplevel_type.subtype_index loop = True @@ -867,81 +883,99 @@ class PdbReader: # COMMON CODE @staticmethod - def parse_string(structure: interfaces.objects.ObjectInterface, - parse_as_pascal: bool = False, - size: int = 0) -> str: + def parse_string( + structure: interfaces.objects.ObjectInterface, + parse_as_pascal: bool = False, + size: int = 0, + ) -> str: """Consumes either a c-string or a pascal string depending on the leaf_type.""" if not parse_as_pascal: - name = structure.cast("string", max_length = size, encoding = "latin-1") + name = structure.cast("string", max_length=size, encoding="latin-1") else: name = structure.cast("pascal_string") - name = name.string.cast("string", max_length = name.length, encoding = "latin-1") + name = name.string.cast( + "string", max_length=name.length, encoding="latin-1" + ) return str(name) - def determine_extended_value(self, leaf_type: interfaces.objects.ObjectInterface, - value: interfaces.objects.ObjectInterface, module: interfaces.context.ModuleInterface, - length: int) -> Tuple[str, interfaces.objects.ObjectInterface, int]: + def determine_extended_value( + self, + leaf_type: interfaces.objects.ObjectInterface, + value: interfaces.objects.ObjectInterface, + module: interfaces.context.ModuleInterface, + length: int, + ) -> Tuple[str, interfaces.objects.ObjectInterface, int]: """Reads a value and potentially consumes more data to construct the value.""" excess = 0 if value >= leaf_type.LF_CHAR: - sub_leaf_type = self.context.object(self.context.symbol_space.get_enumeration(leaf_type.vol.type_name), - layer_name = leaf_type.vol.layer_name, - offset = value.vol.offset) + sub_leaf_type = self.context.object( + self.context.symbol_space.get_enumeration(leaf_type.vol.type_name), + layer_name=leaf_type.vol.layer_name, + offset=value.vol.offset, + ) # Set the offset at just after the previous size type offset = value.vol.offset + value.vol.data_format.length if sub_leaf_type in [leaf_type.LF_CHAR]: - value = module.object(object_type = 'char', offset = offset) + value = module.object(object_type="char", offset=offset) elif sub_leaf_type in [leaf_type.LF_SHORT]: - value = module.object(object_type = 'short', offset = offset) + value = module.object(object_type="short", offset=offset) elif sub_leaf_type in [leaf_type.LF_USHORT]: - value = module.object(object_type = 'unsigned short', offset = offset) + value = module.object(object_type="unsigned short", offset=offset) elif sub_leaf_type in [leaf_type.LF_LONG]: - value = module.object(object_type = 'long', offset = offset) + value = module.object(object_type="long", offset=offset) elif sub_leaf_type in [leaf_type.LF_ULONG]: - value = module.object(object_type = 'unsigned long', offset = offset) + value = module.object(object_type="unsigned long", offset=offset) else: raise TypeError("Unexpected extended value type") excess = value.vol.data_format.length # Updated the consume/offset counters - name = module.object(object_type = "string", offset = value.vol.offset + value.vol.data_format.length) - name_str = self.parse_string(name, leaf_type < leaf_type.LF_ST_MAX, size = length - excess) + name = module.object( + object_type="string", offset=value.vol.offset + value.vol.data_format.length + ) + name_str = self.parse_string( + name, leaf_type < leaf_type.LF_ST_MAX, size=length - excess + ) return name_str, value, excess class PdbRetreiver: - - def retreive_pdb(self, - guid: str, - file_name: str, - progress_callback: constants.ProgressCallback = None) -> Optional[str]: + def retreive_pdb( + self, + guid: str, + file_name: str, + progress_callback: constants.ProgressCallback = None, + ) -> Optional[str]: vollog.info("Download PDB file...") - file_name = ".".join(file_name.split(".")[:-1] + ['pdb']) - for sym_url in ['http://msdl.microsoft.com/download/symbols']: + file_name = ".".join(file_name.split(".")[:-1] + ["pdb"]) + for sym_url in ["http://msdl.microsoft.com/download/symbols"]: url = sym_url + f"/{file_name}/{guid}/" result = None - for suffix in [file_name, file_name[:-1] + '_']: + for suffix in [file_name, file_name[:-1] + "_"]: try: vollog.debug(f"Attempting to retrieve {url + suffix}") # We have to cache this because the file is opened by a layer and we can't control whether that caches - result = resources.ResourceAccessor(progress_callback).open(url + suffix) + with resources.ResourceAccessor(progress_callback).open( + url + suffix + ) as fp: + fp.read(10) + result = True except (error.HTTPError, error.URLError) as excp: vollog.debug(f"Failed with {excp}") if result: break if progress_callback is not None: progress_callback(100, f"Downloading {url + suffix}") - if result is None: + if not result: return None return url + suffix -if __name__ == '__main__': +if __name__ == "__main__": import argparse - class PrintedProgress(object): """A progress handler that prints the progress value and the description onto the command line.""" @@ -960,26 +994,47 @@ if __name__ == '__main__': 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]) - print(message, end = (' ' * (self._max_message_len - message_len)) + '\r') - + print(message, end=(" " * (self._max_message_len - message_len)) + "\r") parser = argparse.ArgumentParser( - description = "Read PDB files and convert to Volatility 3 Intermediate Symbol Format") - parser.add_argument("-o", "--output", metavar = "OUTPUT", help = "Filename for data output", default = None) - file_group = parser.add_argument_group("file", description = "File-based conversion of PDB to ISF") - file_group.add_argument("-f", "--file", metavar = "FILE", help = "PDB file to translate to ISF") - data_group = parser.add_argument_group("data", description = "Convert based on a GUID and filename pattern") - data_group.add_argument("-p", "--pattern", metavar = "PATTERN", help = "Filename pattern to recover PDB file") - data_group.add_argument("-g", - "--guid", - metavar = "GUID", - help = "GUID + Age string for the required PDB file", - default = None) - data_group.add_argument("-k", - "--keep", - action = "store_true", - default = False, - help = "Keep the downloaded PDB file") + description="Read PDB files and convert to Volatility 3 Intermediate Symbol Format" + ) + parser.add_argument( + "-o", + "--output", + metavar="OUTPUT", + help="Filename for data output", + default=None, + ) + file_group = parser.add_argument_group( + "file", description="File-based conversion of PDB to ISF" + ) + file_group.add_argument( + "-f", "--file", metavar="FILE", help="PDB file to translate to ISF" + ) + data_group = parser.add_argument_group( + "data", description="Convert based on a GUID and filename pattern" + ) + data_group.add_argument( + "-p", + "--pattern", + metavar="PATTERN", + help="Filename pattern to recover PDB file", + ) + data_group.add_argument( + "-g", + "--guid", + metavar="GUID", + help="GUID + Age string for the required PDB file", + default=None, + ) + data_group.add_argument( + "-k", + "--keep", + action="store_true", + default=False, + help="Keep the downloaded PDB file", + ) args = parser.parse_args() pg_cb = PrintedProgress() @@ -987,10 +1042,12 @@ if __name__ == '__main__': delfile = False filename = None if args.guid is not None and args.pattern is not None: - filename = PdbRetreiver().retreive_pdb(guid = args.guid, file_name = args.pattern, progress_callback = pg_cb) + filename = PdbRetreiver().retreive_pdb( + guid=args.guid, file_name=args.pattern, progress_callback=pg_cb + ) if filename is None: parser.error("PDB file could not be retrieved from the internet") - if parse.urlparse(filename, 'file').scheme == 'file': + if parse.urlparse(filename, "file").scheme == "file": delfile = True elif args.file: filename = args.file @@ -1001,15 +1058,17 @@ if __name__ == '__main__': parser.error("No suitable filename provided or retrieved") ctx = contexts.Context() - url = parse.urlparse(filename, scheme = 'file') - if url.scheme == 'file': + url = parse.urlparse(filename, scheme="file") + if url.scheme == "file": if not os.path.exists(filename): parser.error(f"File {filename} does not exists") location = "file:" + request.pathname2url(os.path.abspath(filename)) else: location = filename - convertor = PdbReader(ctx, location, database_name = args.pattern, progress_callback = pg_cb) + convertor = PdbReader( + ctx, location, database_name=args.pattern, progress_callback=pg_cb + ) converted_json = convertor.get_json() if args.output is None: @@ -1017,23 +1076,23 @@ if __name__ == '__main__': guid = args.guid[:-1] age = args.guid[-1:] else: - guid = converted_json['metadata']['windows']['pdb']['GUID'] - age = converted_json['metadata']['windows']['pdb']['age'] + guid = converted_json["metadata"]["windows"]["pdb"]["GUID"] + age = converted_json["metadata"]["windows"]["pdb"]["age"] args.output = f"{guid}-{age}.json.xz" output_url = os.path.abspath(args.output) open_method = open - if args.output.endswith('.gz'): + if args.output.endswith(".gz"): open_method = gzip.open - elif args.output.endswith('.bz2'): + elif args.output.endswith(".bz2"): open_method = bz2.open - elif args.output.endswith('.xz'): + elif args.output.endswith(".xz"): open_method = lzma.open with open_method(output_url, "wb") as f: - json_string = json.dumps(converted_json, indent = 2, sort_keys = True) - f.write(bytes(json_string, 'latin-1')) + json_string = json.dumps(converted_json, indent=2, sort_keys=True) + f.write(bytes(json_string, "latin-1")) if args.keep: print(f"Temporary PDB file: {filename}") diff --git a/volatility3/framework/symbols/windows/pdbutil.py b/volatility3/framework/symbols/windows/pdbutil.py index 585e96b6d..3816312cd 100644 --- a/volatility3/framework/symbols/windows/pdbutil.py +++ b/volatility3/framework/symbols/windows/pdbutil.py @@ -2,18 +2,20 @@ # which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 # -import binascii import json import logging import lzma import os import re import struct +from pathlib import PureWindowsPath from typing import Any, Dict, Generator, List, Optional, Tuple, Union -from urllib import request, parse +from urllib import parse, request from volatility3 import symbols -from volatility3.framework import constants, interfaces, exceptions +from volatility3.framework import constants, contexts, exceptions, interfaces +from volatility3.framework.automagic import symbol_cache +from volatility3.framework.configuration import requirements from volatility3.framework.configuration.requirements import SymbolTableRequirement from volatility3.framework.symbols import intermed from volatility3.framework.symbols.windows import pdbconv @@ -24,18 +26,19 @@ vollog = logging.getLogger(__name__) class PDBUtility(interfaces.configuration.VersionableInterface): """Class to handle and manage all getting symbols based on MZ header""" - _version = (1, 0, 0) + _version = (1, 0, 1) _required_framework_version = (2, 0, 0) @classmethod def symbol_table_from_offset( - cls, - context: interfaces.context.ContextInterface, - layer_name: str, - offset: int, - symbol_table_class: str = "volatility3.framework.symbols.intermed.IntermediateSymbolTable", - config_path: str = None, - progress_callback: constants.ProgressCallback = None) -> Optional[str]: + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + offset: int, + symbol_table_class: str = "volatility3.framework.symbols.intermed.IntermediateSymbolTable", + config_path: str = None, + progress_callback: constants.ProgressCallback = None, + ) -> Optional[str]: """Produces the name of a symbol table loaded from the offset for an MZ header Args: @@ -51,45 +54,80 @@ class PDBUtility(interfaces.configuration.VersionableInterface): """ result = cls.get_guid_from_mz(context, layer_name, offset) if result is None: + vollog.debug(f"Could not get GUID for {hex(offset)}") return None guid, age, pdb_name = result if config_path is None: - config_path = interfaces.configuration.path_join('pdbutility', pdb_name.replace('.', '_')) + config_path = interfaces.configuration.path_join( + "pdbutility", pdb_name.replace(".", "_") + ) - return cls.load_windows_symbol_table(context, guid, age, pdb_name, symbol_table_class, config_path, - progress_callback) + return cls.load_windows_symbol_table( + context, + guid, + age, + pdb_name, + symbol_table_class, + config_path, + progress_callback, + ) @classmethod - def load_windows_symbol_table(cls, - context: interfaces.context.ContextInterface, - guid: str, - age: int, - pdb_name: str, - symbol_table_class: str, - config_path: str = 'pdbutility', - progress_callback: constants.ProgressCallback = None): + def load_windows_symbol_table( + cls, + context: interfaces.context.ContextInterface, + guid: str, + age: int, + pdb_name: str, + symbol_table_class: str, + config_path: str = "pdbutility", + progress_callback: constants.ProgressCallback = None, + ): """Loads (downloading if necessary) a windows symbol table""" - filter_string = os.path.join(pdb_name.strip('\x00'), guid.upper() + "-" + str(age)) + filter_string = os.path.join( + pdb_name.strip("\x00"), guid.upper() + "-" + str(age) + ) isf_path = None # Take the first result of search for the intermediate file - for value in intermed.IntermediateSymbolTable.file_symbol_url("windows", filter_string): + if not requirements.VersionRequirement.matches_required( + (1, 0, 0), symbol_cache.SqliteCache.version + ): + vollog.debug(f"Required version of SQLiteCache not found") + return None + + identifiers_path = os.path.join( + constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME + ) + value = symbol_cache.SqliteCache(identifiers_path).find_location( + symbol_cache.WindowsIdentifier.generate( + pdb_name.strip("\x00"), guid.upper(), age + ), + "windows", + ) + + if value: isf_path = value - break else: # If none are found, attempt to download the pdb, convert it and try again - cls.download_pdb_isf(context, guid.upper(), age, pdb_name, progress_callback) + cls.download_pdb_isf( + context, guid.upper(), age, pdb_name, progress_callback + ) # Try again - for value in intermed.IntermediateSymbolTable.file_symbol_url("windows", filter_string): + for value in intermed.IntermediateSymbolTable.file_symbol_url( + "windows", filter_string + ): isf_path = value break if not isf_path: vollog.debug(f"Required symbol library path not found: {filter_string}") - vollog.info("The symbols can be downloaded later using pdbconv.py -p {} -g {}".format( - pdb_name.strip('\x00'), - guid.upper() + str(age))) + vollog.info( + "The symbols can be downloaded later using pdbconv.py -p {} -g {}".format( + pdb_name.strip("\x00"), guid.upper() + str(age) + ) + ) return None vollog.debug(f"Using symbol library: {filter_string}") @@ -102,13 +140,16 @@ class PDBUtility(interfaces.configuration.VersionableInterface): requirement_name = interfaces.configuration.path_head(config_path) # Construct the appropriate symbol table - requirement = SymbolTableRequirement(name = requirement_name, description = "PDBUtility generated symbol table") + requirement = SymbolTableRequirement( + name=requirement_name, description="PDBUtility generated symbol table" + ) requirement.construct(context, parent_config_path) return context.config[config_path] @classmethod - def get_guid_from_mz(cls, context: interfaces.context.ContextInterface, layer_name: str, - offset: int) -> Optional[Tuple[str, int, str]]: + def get_guid_from_mz( + cls, context: interfaces.context.ContextInterface, layer_name: str, offset: int + ) -> Optional[Tuple[str, int, str]]: """Takes the offset to an MZ header, locates any available pdb headers, and extracts the guid, age and pdb_name from them Args: @@ -122,7 +163,9 @@ class PDBUtility(interfaces.configuration.VersionableInterface): try: import pefile except ImportError: - vollog.error("Get_guid_from_mz requires the following python module: pefile") + vollog.error( + "Get_guid_from_mz requires the following python module: pefile" + ) return None layer = context.layers[layer_name] @@ -131,34 +174,40 @@ class PDBUtility(interfaces.configuration.VersionableInterface): # Check it is actually the MZ header if mz_sig != b"MZ": return None - - nt_header_start, = struct.unpack(" None: + def download_pdb_isf( + cls, + context: interfaces.context.ContextInterface, + guid: str, + age: int, + pdb_name: str, + progress_callback: constants.ProgressCallback = None, + ) -> None: """Attempts to download the PDB file, convert it to an ISF file and save it to one of the symbol locations.""" # Check for writability filter_string = os.path.join(pdb_name, guid + "-" + str(age)) for path in symbols.__path__: - # Store any temporary files created by downloading PDB files tmp_files = [] - potential_output_filename = os.path.join(path, "windows", filter_string + ".json.xz") + potential_output_filename = os.path.join( + path, "windows", filter_string + ".json.xz" + ) data_written = False try: - os.makedirs(os.path.dirname(potential_output_filename), exist_ok = True) + os.makedirs(os.path.dirname(potential_output_filename), exist_ok=True) with lzma.open(potential_output_filename, "w") as of: # Once we haven't thrown an error, do the computation - filename = pdbconv.PdbRetreiver().retreive_pdb(guid + str(age), - file_name = pdb_name, - progress_callback = progress_callback) + filename = pdbconv.PdbRetreiver().retreive_pdb( + guid + str(age), + file_name=pdb_name, + progress_callback=progress_callback, + ) if filename: - url = parse.urlparse(filename, scheme = 'file') - if url.scheme == 'file' or len(url.scheme) == 1: + url = parse.urlparse(filename, scheme="file") + if url.scheme == "file" or len(url.scheme) == 1: tmp_files.append(filename) - location = "file:" + request.pathname2url(os.path.abspath(tmp_files[-1])) + location = "file:" + request.pathname2url( + os.path.abspath(tmp_files[-1]) + ) else: location = filename - json_output = pdbconv.PdbReader(context, location, pdb_name, progress_callback).get_json() - of.write(bytes(json.dumps(json_output, indent = 2, sort_keys = True), 'utf-8')) + json_output = pdbconv.PdbReader( + context, location, pdb_name, progress_callback + ).get_json() + of.write( + bytes( + json.dumps(json_output, indent=2, sort_keys=True), + "utf-8", + ) + ) # After we've successfully written it out, record the fact so we don't clear it out data_written = True else: - vollog.warning("Symbol file could not be downloaded from remote server" + (" " * 100)) + vollog.warning( + "Symbol file could not be downloaded from remote server" + + (" " * 100) + ) break except PermissionError: - vollog.warning("Cannot write necessary symbol file, please check permissions on {}".format( - potential_output_filename)) + vollog.warning( + "Cannot write necessary symbol file, please check permissions on {}".format( + potential_output_filename + ) + ) continue finally: # If something else failed, removed the symbol file so we don't pick it up in the future @@ -232,20 +305,27 @@ class PDBUtility(interfaces.configuration.VersionableInterface): try: os.remove(filename) except PermissionError: - vollog.warning(f"Temporary file could not be removed: {filename}") + vollog.warning( + f"Temporary file could not be removed: {filename}" + ) else: - vollog.warning("Cannot write downloaded symbols, please add the appropriate symbols" - " or add/modify a symbols directory that is writable") + vollog.warning( + "Cannot write downloaded symbols, please add the appropriate symbols" + " or add/modify a symbols directory that is writable" + ) @classmethod - def pdbname_scan(cls, - ctx: interfaces.context.ContextInterface, - layer_name: str, - page_size: int, - pdb_names: List[bytes], - progress_callback: constants.ProgressCallback = None, - start: Optional[int] = None, - end: Optional[int] = None) -> Generator[Dict[str, Optional[Union[bytes, str, int]]], None, None]: + def pdbname_scan( + cls, + ctx: interfaces.context.ContextInterface, + layer_name: str, + page_size: int, + pdb_names: List[bytes], + progress_callback: constants.ProgressCallback = None, + start: Optional[int] = None, + end: Optional[int] = None, + maximum_invalid_count: int = 100, + ) -> Generator[Dict[str, Optional[Union[bytes, str, int]]], None, None]: """Scans through `layer_name` at `ctx` looking for RSDS headers that indicate one of four common pdb kernel names (as listed in `self.pdb_names`) and returns the tuple (GUID, age, pdb_name, @@ -255,6 +335,14 @@ class PDBUtility(interfaces.configuration.VersionableInterface): The UI should always provide the user an opportunity to specify the appropriate types and PDB values themselves + Args: + layer_name: The layer name to scan + page_size: Size of page constant + pdb_names: List of pdb names to scan + progress_callback: Means of providing the user with feedback during long processes + start: Start address to start scanning from the pdb_names + end: Minimum address to scan the pdb_names + maximum_invalid_count: Amount of pages that can be invalid during scanning before aborting signature search """ min_pfn = 0 @@ -263,35 +351,48 @@ class PDBUtility(interfaces.configuration.VersionableInterface): if end is None: end = ctx.layers[layer_name].maximum_address - for (GUID, age, pdb_name, - signature_offset) in ctx.layers[layer_name].scan(ctx, - PdbSignatureScanner(pdb_names), - progress_callback = progress_callback, - sections = [(start, end - start)]): + for GUID, age, pdb_name, signature_offset in ctx.layers[layer_name].scan( + ctx, + PdbSignatureScanner(pdb_names), + progress_callback=progress_callback, + sections=[(start, end - start)], + ): mz_offset = None sig_pfn = signature_offset // page_size + current_invalid_counter = 0 for i in range(sig_pfn, min_pfn, -1): - if not ctx.layers[layer_name].is_valid(i * page_size, 2): + if current_invalid_counter > maximum_invalid_count: break + if not ctx.layers[layer_name].is_valid(i * page_size, 2): + current_invalid_counter += 1 + continue + data = ctx.layers[layer_name].read(i * page_size, 2) - if data == b'MZ': + if data == b"MZ": mz_offset = i * page_size break min_pfn = sig_pfn yield { - 'GUID': GUID, - 'age': age, - 'pdb_name': str(pdb_name, "utf-8"), - 'signature_offset': signature_offset, - 'mz_offset': mz_offset + "GUID": GUID, + "age": age, + "pdb_name": str(pdb_name, "utf-8"), + "signature_offset": signature_offset, + "mz_offset": mz_offset, } @classmethod - def symbol_table_from_pdb(cls, context: interfaces.context.ContextInterface, config_path: str, layer_name: str, - pdb_name: str, module_offset: int, module_size: int) -> str: + def symbol_table_from_pdb( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + pdb_name: str, + module_offset: int = None, + module_size: int = None, + ) -> str: """Creates symbol table for a module in the specified layer_name. Searches the memory section of the loaded module for its PDB GUID @@ -307,28 +408,108 @@ class PDBUtility(interfaces.configuration.VersionableInterface): Returns: The name of the constructed and loaded symbol table """ + _, symbol_table_name = cls._modtable_from_pdb( + context, config_path, layer_name, pdb_name, module_offset, module_size + ) + return symbol_table_name + + @classmethod + def _modtable_from_pdb( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + pdb_name: str, + module_offset: int = None, + module_size: int = None, + create_module: bool = False, + ) -> Tuple[Optional[str], Optional[str]]: + if module_offset is None: + module_offset = context.layers[layer_name].minimum_address + if module_size is None: + module_size = context.layers[layer_name].maximum_address - module_offset guids = list( - cls.pdbname_scan(context, - layer_name, - context.layers[layer_name].page_size, [bytes(pdb_name, 'latin-1')], - start = module_offset, - end = module_offset + module_size)) + cls.pdbname_scan( + context, + layer_name, + context.layers[layer_name].page_size, + [bytes(pdb_name, "latin-1")], + start=module_offset, + end=module_offset + module_size, + ) + ) if not guids: raise exceptions.VolatilityException( - f"Did not find GUID of {pdb_name} in module @ 0x{module_offset:x}!") + f"Did not find GUID of {pdb_name} in module @ 0x{module_offset:x}!" + ) guid = guids[0] vollog.debug(f"Found {guid['pdb_name']}: {guid['GUID']}-{guid['age']}") - return cls.load_windows_symbol_table(context, - guid["GUID"], - guid["age"], - guid["pdb_name"], - "volatility3.framework.symbols.intermed.IntermediateSymbolTable", - config_path = config_path) + module_name = guid["pdb_name"].replace(".pdb", "") + + symbol_table_name = cls.load_windows_symbol_table( + context, + guid["GUID"], + guid["age"], + guid["pdb_name"], + "volatility3.framework.symbols.intermed.IntermediateSymbolTable", + config_path=config_path, + ) + + new_module_name = None + if create_module: + new_module = contexts.Module.create( + context, + module_name, + layer_name, + offset=guid["mz_offset"], + symbol_table_name=symbol_table_name, + ) + new_module_name = new_module.name + + return new_module_name, symbol_table_name + + @classmethod + def module_from_pdb( + cls, + context: interfaces.context.ContextInterface, + config_path: str, + layer_name: str, + pdb_name: str, + module_offset: int = None, + module_size: int = None, + ) -> str: + """Creates a module in the specified layer_name based on a pdb name. + + Searches the memory section of the loaded module for its PDB GUID + and loads the associated symbol table into the symbol space. + + Args: + context: The context to retrieve required elements (layers, symbol tables) from + config_path: The config path where to find symbol files + layer_name: The name of the layer on which to operate + module_offset: This memory dump's module image offset + module_size: The size of the module for this dump + + Returns: + The name of the constructed and loaded symbol table + """ + + module_name, _ = cls._modtable_from_pdb( + context, + config_path, + layer_name, + pdb_name, + module_offset, + module_size, + create_module=True, + ) + + return module_name class PdbSignatureScanner(interfaces.layers.ScannerInterface): @@ -340,6 +521,7 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface): .. note:: The pdb_names must be a list of byte strings, unicode strs will not match against the data scanned """ + overlap = 0x4000 """The size of overlap needed for the signature to ensure data cannot hide between two scanned chunks""" thread_safe = True @@ -351,16 +533,52 @@ class PdbSignatureScanner(interfaces.layers.ScannerInterface): super().__init__() self._pdb_names = pdb_names - def __call__(self, data: bytes, data_offset: int) -> Generator[Tuple[str, Any, bytes, int], None, None]: - pattern = b'RSDS' + (b'.' * self._RSDS_format.size) + b'(' + b'|'.join( - [re.escape(x) for x in self._pdb_names]) + b')\x00' - for match in re.finditer(pattern, data, flags = re.DOTALL): - pdb_name = data[match.start(0) + 4 + self._RSDS_format.size:match.start(0) + len(match.group()) - 1] + def __call__( + self, data: bytes, data_offset: int + ) -> Generator[Tuple[str, Any, bytes, int], None, None]: + pattern = ( + b"RSDS" + + (b"." * self._RSDS_format.size) + + b"(" + + b"|".join([re.escape(x) for x in self._pdb_names]) + + b")\x00" + ) + for match in re.finditer(pattern, data, flags=re.DOTALL): + pdb_name = data[ + match.start(0) + + 4 + + self._RSDS_format.size : match.start(0) + + len(match.group()) + - 1 + ] if pdb_name in self._pdb_names: ## this ordering is intentional due to mixed endianness in the GUID - (g3, g2, g1, g0, g5, g4, g7, g6, g8, g9, ga, gb, gc, gd, ge, gf, a) = \ - self._RSDS_format.unpack(data[match.start(0) + 4:match.start(0) + 4 + self._RSDS_format.size]) + ( + g3, + g2, + g1, + g0, + g5, + g4, + g7, + g6, + g8, + g9, + ga, + gb, + gc, + gd, + ge, + gf, + a, + ) = self._RSDS_format.unpack( + data[ + match.start(0) + 4 : match.start(0) + 4 + self._RSDS_format.size + ] + ) - guid = (16 * '{:02X}').format(g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, ga, gb, gc, gd, ge, gf) + guid = (16 * "{:02X}").format( + g0, g1, g2, g3, g4, g5, g6, g7, g8, g9, ga, gb, gc, gd, ge, gf + ) if match.start(0) < self.chunk_size: yield (guid, a, pdb_name, data_offset + match.start(0)) diff --git a/volatility3/framework/symbols/windows/services/services-win10-17763-x86.json b/volatility3/framework/symbols/windows/services/services-win10-17763-x86.json new file mode 100644 index 000000000..8f2854721 --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-17763-x86.json @@ -0,0 +1,248 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 4 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 12 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 20 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 12 + } + }, + "kind": "struct", + "size": 12 + }, + "_SERVICE_RECORD": { + "fields": { + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 48 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 160 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 12 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 24 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 56 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 44 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 160 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 52 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 156 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "vtypes_to_json.py", + "datetime": "2019-04-17T13:45:16.417006" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/services/services-win10-18362-x64.json b/volatility3/framework/symbols/windows/services/services-win10-18362-x64.json new file mode 100644 index 000000000..a6a80c1d3 --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-18362-x64.json @@ -0,0 +1,255 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 16 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 24 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 40 + } + }, + "kind": "struct", + "size": 40 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_RECORD": { + "fields": { + "ServiceList": { + "type": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + }, + "offset": 0 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 32 + }, + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 64 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 240 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 36 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 76 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 56 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 240 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 72 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 248 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "vtypes_to_json.py", + "datetime": "2019-04-17T13:45:16.417006" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/services/services-win10-18362-x86.json b/volatility3/framework/symbols/windows/services/services-win10-18362-x86.json new file mode 100644 index 000000000..4684dfe5b --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-18362-x86.json @@ -0,0 +1,248 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 4 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 12 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 20 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 12 + } + }, + "kind": "struct", + "size": 12 + }, + "_SERVICE_RECORD": { + "fields": { + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 48 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 164 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 12 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 24 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 56 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 44 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 164 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 52 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 156 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "vtypes_to_json.py", + "datetime": "2019-04-17T13:45:16.417006" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/services/services-win10-19041-x64.json b/volatility3/framework/symbols/windows/services/services-win10-19041-x64.json new file mode 100644 index 000000000..e44dbbd37 --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-19041-x64.json @@ -0,0 +1,255 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 16 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 24 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 40 + } + }, + "kind": "struct", + "size": 40 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_RECORD": { + "fields": { + "ServiceList": { + "type": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + }, + "offset": 0 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 32 + }, + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 64 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 296 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 36 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 76 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 56 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 296 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 72 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 296 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "David McDonald", + "datetime": "2023-11-16T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/services/services-win10-19041-x86.json b/volatility3/framework/symbols/windows/services/services-win10-19041-x86.json new file mode 100644 index 000000000..cc5ed9a73 --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-19041-x86.json @@ -0,0 +1,248 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 4 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 8 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 12 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 20 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 12 + }, + "_SERVICE_RECORD": { + "fields": { + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 48 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 192 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 12 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 24 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 56 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 44 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 192 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 52 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 20 + } + }, + "kind": "struct", + "size": 192 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "vtypes_to_json.py", + "datetime": "2019-04-17T13:45:16.417006" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/services/services-win10-25398-x64.json b/volatility3/framework/symbols/windows/services/services-win10-25398-x64.json new file mode 100644 index 000000000..cd29abc43 --- /dev/null +++ b/volatility3/framework/symbols/windows/services/services-win10-25398-x64.json @@ -0,0 +1,255 @@ +{ + "symbols": {}, + "enums": { + "StateEnum": { + "base": "long", + "constants": { + "SERVICE_START_PENDING": 2, + "SERVICE_STOP_PENDING": 3, + "SERVICE_STOPPED": 1, + "SERVICE_CONTINUE_PENDING": 5, + "SERVICE_PAUSE_PENDING": 6, + "SERVICE_PAUSED": 7, + "SERVICE_RUNNING": 4 + }, + "size": 4 + }, + "StartEnum": { + "base": "long", + "constants": { + "SERVICE_DEMAND_START": 3, + "SERVICE_AUTO_START": 2, + "SERVICE_BOOT_START": 0, + "SERVICE_DISABLED": 4, + "SERVICE_SYSTEM_START": 1 + }, + "size": 4 + } + }, + "base_types": { + "unsigned long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned char": { + "kind": "char", + "size": 1, + "signed": false, + "endian": "little" + }, + "pointer": { + "kind": "int", + "size": 8, + "signed": false, + "endian": "little" + }, + "unsigned int": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + }, + "unsigned short": { + "kind": "int", + "size": 2, + "signed": false, + "endian": "little" + }, + "long": { + "kind": "int", + "size": 4, + "signed": false, + "endian": "little" + } + }, + "user_types": { + "_SERVICE_LIST_ENTRY": { + "fields": { + "Flink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 16 + }, + "Blink": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + } + }, + "offset": 0 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_PROCESS": { + "fields": { + "BinaryPath": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 24 + }, + "ProcessId": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 40 + } + }, + "kind": "struct", + "size": 40 + }, + "_SERVICE_HEADER": { + "fields": { + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 0 + }, + "ServiceRecord": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + } + }, + "kind": "struct", + "size": 16 + }, + "_SERVICE_RECORD": { + "fields": { + "ServiceList": { + "type": { + "kind": "struct", + "name": "_SERVICE_LIST_ENTRY" + }, + "offset": 0 + }, + "Tag": { + "type": { + "count": 4, + "subtype": { + "kind": "base", + "name": "unsigned char" + }, + "kind": "array" + }, + "offset": 32 + }, + "DisplayName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 64 + }, + "ServiceProcess": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_PROCESS" + } + }, + "offset": 336 + }, + "PrevEntry": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "struct", + "name": "_SERVICE_RECORD" + } + }, + "offset": 16 + }, + "Start": { + "type": { + "kind": "enum", + "name": "StartEnum" + }, + "offset": 36 + }, + "State": { + "type": { + "kind": "enum", + "name": "StateEnum" + }, + "offset": 84 + }, + "ServiceName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 56 + }, + "DriverName": { + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "unsigned short" + } + }, + "offset": 296 + }, + "Type": { + "type": { + "kind": "base", + "name": "unsigned long" + }, + "offset": 80 + }, + "Order": { + "type": { + "kind": "base", + "name": "unsigned int" + }, + "offset": 32 + } + }, + "kind": "struct", + "size": 336 + } + }, + "metadata": { + "producer": { + "version": "0.0.1", + "name": "David McDonald", + "datetime": "2023-11-16T15:05:35-06:00" + }, + "format": "4.1.0" + } +} diff --git a/volatility3/framework/symbols/windows/versions.py b/volatility3/framework/symbols/windows/versions.py index d38cdb701..e1e74afc0 100644 --- a/volatility3/framework/symbols/windows/versions.py +++ b/volatility3/framework/symbols/windows/versions.py @@ -31,13 +31,18 @@ class OsDistinguisher: A function that takes a context and a symbol table name and determines whether that symbol table passes the distinguishing checks """ - def __init__(self, version_check: Callable[[Tuple[int, ...]], bool], fallback_checks: List[Tuple[str, Optional[str], - bool]]) -> None: + def __init__( + self, + version_check: Callable[[Tuple[int, ...]], bool], + fallback_checks: List[Tuple[str, Optional[str], bool]], + ) -> None: self._version_check = version_check self._fallback_checks = fallback_checks # try the primary method based on the pe version in the ISF - def __call__(self, context: interfaces.context.ContextInterface, symbol_table: str) -> bool: + def __call__( + self, context: interfaces.context.ContextInterface, symbol_table: str + ) -> bool: """ Args: @@ -53,17 +58,27 @@ class OsDistinguisher: major, minor, revision, build = pe_version return self._version_check((major, minor, revision, build)) except (AttributeError, ValueError, TypeError): - vollog.log(constants.LOGLEVEL_VVV, "Windows PE version data is not available") + vollog.log( + constants.LOGLEVEL_VVV, "Windows PE version data is not available" + ) # fall back to the backup method, if necessary for name, member, response in self._fallback_checks: if member is None: - if (context.symbol_space.has_symbol(symbol_table + constants.BANG + name) - or context.symbol_space.has_type(symbol_table + constants.BANG + name)) != response: + if ( + context.symbol_space.has_symbol( + symbol_table + constants.BANG + name + ) + or context.symbol_space.has_type( + symbol_table + constants.BANG + name + ) + ) != response: return False else: try: - symbol_type = context.symbol_space.get_type(symbol_table + constants.BANG + name) + symbol_type = context.symbol_space.get_type( + symbol_table + constants.BANG + name + ) if symbol_type.has_member(member) != response: return False except exceptions.SymbolError: @@ -73,49 +88,122 @@ class OsDistinguisher: return True -is_windows_8_1_or_later = OsDistinguisher(version_check = lambda x: x >= (6, 3), - fallback_checks = [("_KPRCB", "PendingTickFlags", True)]) +is_windows_8_1_or_later = OsDistinguisher( + version_check=lambda x: x >= (6, 3), + fallback_checks=[("_KPRCB", "PendingTickFlags", True)], +) -is_vista_or_later = OsDistinguisher(version_check = lambda x: x >= (6, 0), - fallback_checks = [("KdCopyDataBlock", None, True)]) +is_vista_or_later = OsDistinguisher( + version_check=lambda x: x >= (6, 0), + fallback_checks=[("KdCopyDataBlock", None, True)], +) -is_win10 = OsDistinguisher(version_check = lambda x: (10, 0) <= x, - fallback_checks = [("ObHeaderCookie", None, True), ("_HANDLE_TABLE", "HandleCount", False)]) +is_win10 = OsDistinguisher( + version_check=lambda x: (10, 0) <= x, + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_HANDLE_TABLE", "HandleCount", False), + ], +) -is_windows_xp = OsDistinguisher(version_check = lambda x: (5, 1) <= x < (5, 2), - fallback_checks = [("KdCopyDataBlock", None, False), - ("_HANDLE_TABLE", "HandleCount", True)]) +is_windows_xp = OsDistinguisher( + version_check=lambda x: (5, 1) <= x < (5, 2), + fallback_checks=[ + ("KdCopyDataBlock", None, False), + ("_HANDLE_TABLE", "HandleCount", True), + ], +) -is_xp_or_2003 = OsDistinguisher(version_check = lambda x: (5, 1) <= x < (6, 0), - fallback_checks = [("KdCopyDataBlock", None, False), - ("_HANDLE_TABLE", "HandleCount", True)]) +is_xp_or_2003 = OsDistinguisher( + version_check=lambda x: (5, 1) <= x < (6, 0), + fallback_checks=[ + ("KdCopyDataBlock", None, False), + ("_HANDLE_TABLE", "HandleCount", True), + ], +) -is_win10_up_to_15063 = OsDistinguisher(version_check = lambda x: (10, 0) <= x < (10, 0, 15063), - fallback_checks = [("ObHeaderCookie", None, True), - ("_HANDLE_TABLE", "HandleCount", False), - ("_EPROCESS", "KeepAliveCounter", True)]) +is_win10_up_to_15063 = OsDistinguisher( + version_check=lambda x: (10, 0) <= x < (10, 0, 15063), + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_HANDLE_TABLE", "HandleCount", False), + ("_EPROCESS", "KeepAliveCounter", True), + ], +) -is_win10_15063 = OsDistinguisher(version_check = lambda x: x == (10, 0, 15063), - fallback_checks = [("ObHeaderCookie", None, True), - ("_HANDLE_TABLE", "HandleCount", False), - ("_EPROCESS", "KeepAliveCounter", False), - ("_EPROCESS", "ControlFlowGuardEnabled", True)]) +is_win10_15063 = OsDistinguisher( + version_check=lambda x: x == (10, 0, 15063), + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_HANDLE_TABLE", "HandleCount", False), + ("_EPROCESS", "KeepAliveCounter", False), + ("_EPROCESS", "ControlFlowGuardEnabled", True), + ], +) -is_win10_16299_or_later = OsDistinguisher(version_check = lambda x: x >= (10, 0, 16299), - fallback_checks = [("ObHeaderCookie", None, True), - ("_HANDLE_TABLE", "HandleCount", False), - ("_EPROCESS", "KeepAliveCounter", False), - ("_EPROCESS", "ControlFlowGuardEnabled", False)]) +is_win10_16299_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 16299), + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_HANDLE_TABLE", "HandleCount", False), + ("_EPROCESS", "KeepAliveCounter", False), + ("_EPROCESS", "ControlFlowGuardEnabled", False), + ], +) -is_win10_18363_or_later = OsDistinguisher(version_check = lambda x: x >= (10, 0, 18363), - fallback_checks = [("_KQOS_GROUPING_SETS", None, True)]) +is_win10_17763_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 17763), + fallback_checks=[ + ("_EPROCESS", "TrustletIdentity", False), + ("ParentSecurityDomain", None, True), + ], +) -is_windows_10 = OsDistinguisher(version_check = lambda x: x >= (10, 0), - fallback_checks = [("ObHeaderCookie", None, True)]) +is_win10_18362_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 18362), + fallback_checks=[ + ("ObHeaderCookie", None, True), + ("_CM_CACHED_VALUE_INDEX", None, False), + ("_WNF_PROCESS_CONTEXT", None, True), + ], +) -is_windows_8_or_later = OsDistinguisher(version_check = lambda x: x >= (6, 2), - fallback_checks = [("_HANDLE_TABLE", "HandleCount", False)]) +is_win10_18363_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 18363), + fallback_checks=[("_KQOS_GROUPING_SETS", None, True)], +) + +is_win10_19041_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 19041), + fallback_checks=[ + ("_EPROCESS", "TimerResolutionIgnore", True), + ("_EPROCESS", "VmProcessorHostTransition", True), + ("_KQOS_GROUPING_SETS", None, True), + ], +) + +is_win10_25398_or_later = OsDistinguisher( + version_check=lambda x: x >= (10, 0, 25398), + fallback_checks=[ + ("_EPROCESS", "MmSlabIdentity", True), + ("_EPROCESS", "EnableProcessImpersonationLogging", True), + ], +) + +is_windows_10 = OsDistinguisher( + version_check=lambda x: x >= (10, 0), + fallback_checks=[("ObHeaderCookie", None, True)], +) + +is_windows_8_or_later = OsDistinguisher( + version_check=lambda x: x >= (6, 2), + fallback_checks=[("_HANDLE_TABLE", "HandleCount", False)], +) # Technically, this is win7 or less -is_windows_7 = OsDistinguisher(version_check = lambda x: x == (6, 1), - fallback_checks = [("_OBJECT_HEADER", "TypeIndex", True), - ("_HANDLE_TABLE", "HandleCount", True)]) +is_windows_7 = OsDistinguisher( + version_check=lambda x: x == (6, 1), + fallback_checks=[ + ("_OBJECT_HEADER", "TypeIndex", True), + ("_HANDLE_TABLE", "HandleCount", True), + ], +) diff --git a/volatility3/plugins/windows/registry/certificates.py b/volatility3/plugins/windows/registry/certificates.py index 91f17fb2d..5ef840f32 100644 --- a/volatility3/plugins/windows/registry/certificates.py +++ b/volatility3/plugins/windows/registry/certificates.py @@ -1,11 +1,15 @@ +import contextlib +import logging import struct -from typing import List, Iterator, Tuple +from typing import List, Iterator, Optional, Tuple, Type -from volatility3.framework import interfaces, renderers +from volatility3.framework import exceptions, interfaces, renderers from volatility3.framework.configuration import requirements from volatility3.framework.symbols.windows.extensions.registry import RegValueTypes from volatility3.plugins.windows.registry import hivelist, printkey +vollog = logging.getLogger(__name__) + class Certificates(interfaces.plugins.PluginInterface): """Lists the certificates in the registry's Certificate Store.""" @@ -15,12 +19,23 @@ class Certificates(interfaces.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.SymbolTableRequirement(name = "nt_symbols", description = "Windows kernel symbols"), - requirements.PluginRequirement(name = 'hivelist', plugin = hivelist.HiveList, version = (1, 0, 0)), - requirements.PluginRequirement(name = 'printkey', plugin = printkey.PrintKey, version = (1, 0, 0)) + requirements.ModuleRequirement( + name="kernel", + description="Windows kernel", + architectures=["Intel32", "Intel64"], + ), + requirements.PluginRequirement( + name="hivelist", plugin=hivelist.HiveList, version=(1, 0, 0) + ), + requirements.PluginRequirement( + name="printkey", plugin=printkey.PrintKey, version=(1, 0, 0) + ), + requirements.BooleanRequirement( + name="dump", + description="Extract listed certificates", + default=False, + optional=True, + ), ] def parse_data(self, data: bytes) -> Tuple[str, bytes]: @@ -28,43 +43,93 @@ class Certificates(interfaces.plugins.PluginInterface): certificate_data = renderers.NotAvailableValue() while len(data) > 12: ctype, clength = struct.unpack(" Iterator[Tuple[int, Tuple[str, str, str, str]]]: - for hive in hivelist.HiveList.list_hives(self.context, - base_config_path = self.config_path, - layer_name = self.config['primary'], - symbol_table = self.config['nt_symbols']): + @classmethod + def dump_certificate( + cls, + certificate_data: bytes, + hive_offset: int, + reg_section: str, + key_hash: str, + open_method: Type[interfaces.plugins.FileHandlerInterface], + ) -> Optional[interfaces.plugins.FileHandlerInterface]: + try: + dump_name = "{}-{}-{}.crt".format(hive_offset, reg_section, key_hash) + file_handle = open_method(dump_name) + file_handle.write(certificate_data) + return file_handle + except exceptions.InvalidAddressException: + vollog.debug(f"Unable to dump certificate file at {hive_offset:#x}") + return None + def _generator(self) -> Iterator[Tuple[int, Tuple[str, str, str, str]]]: + kernel = self.context.modules[self.config["kernel"]] + + for hive in hivelist.HiveList.list_hives( + self.context, + base_config_path=self.config_path, + layer_name=kernel.layer_name, + symbol_table=kernel.symbol_table_name, + ): for top_key in [ - "Microsoft\\SystemCertificates", - "Software\\Microsoft\\SystemCertificates", + "Microsoft\\SystemCertificates", + "Software\\Microsoft\\SystemCertificates", ]: - try: + with contextlib.suppress(KeyError, exceptions.InvalidAddressException): # Walk it - node_path = hive.get_key(top_key, return_list = True) - for (depth, is_key, last_write_time, key_path, volatility, - node) in printkey.PrintKey.key_iterator(hive, node_path, recurse = True): + node_path = hive.get_key(top_key, return_list=True) + for ( + _depth, + is_key, + _last_write_time, + key_path, + _volatility, + node, + ) in printkey.PrintKey.key_iterator(hive, node_path, recurse=True): if not is_key and RegValueTypes(node.Type).name == "REG_BINARY": name, certificate_data = self.parse_data(node.decode_data()) - unique_key_offset = key_path.casefold().index(top_key.casefold()) + len(top_key) + 1 - reg_section = key_path[unique_key_offset:key_path.index("\\", unique_key_offset)] - key_hash = key_path[key_path.rindex("\\") + 1:] + unique_key_offset = ( + key_path.casefold().index(top_key.casefold()) + + len(top_key) + + 1 + ) + reg_section = key_path[ + unique_key_offset : key_path.index( + "\\", unique_key_offset + ) + ] + key_hash = key_path[key_path.rindex("\\") + 1 :] + + if self.config["dump"]: + if not isinstance( + certificate_data, + interfaces.renderers.BaseAbsentValue, + ): + file_handle = self.dump_certificate( + certificate_data, + hive.hive_offset, + reg_section, + key_hash, + self.open, + ) + if file_handle: + file_handle.close() - if not isinstance(certificate_data, interfaces.renderers.BaseAbsentValue): - with self.open("{} - {} - {}.crt".format(hex(hive.hive_offset), reg_section, - key_hash)) as file_data: - file_data.write(certificate_data) yield (0, (top_key, reg_section, key_hash, name)) - except KeyError: - # Key wasn't found in this hive, carry on - pass def run(self) -> renderers.TreeGrid: - return renderers.TreeGrid([("Certificate path", str), ("Certificate section", str), ("Certificate ID", str), - ("Certificate name", str)], self._generator()) + return renderers.TreeGrid( + [ + ("Certificate path", str), + ("Certificate section", str), + ("Certificate ID", str), + ("Certificate name", str), + ], + self._generator(), + ) diff --git a/volatility3/plugins/windows/statistics.py b/volatility3/plugins/windows/statistics.py index e6f2016ed..7f56b75f8 100644 --- a/volatility3/plugins/windows/statistics.py +++ b/volatility3/plugins/windows/statistics.py @@ -13,21 +13,27 @@ vollog = logging.getLogger(__name__) class Statistics(plugins.PluginInterface): + """Lists statistics about the memory space.""" + _required_framework_version = (2, 0, 0) @classmethod def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: return [ - requirements.TranslationLayerRequirement(name = 'primary', - description = 'Memory layer for the kernel', - architectures = ["Intel32", "Intel64"]) + requirements.TranslationLayerRequirement( + name="primary", + description="Memory layer for the kernel", + architectures=["Intel32", "Intel64"], + ) ] def _generator(self): # Do mass mapping and determine the number of different layers and how many pages go to each one - layer = self.context.layers[self.config['primary']] + layer = self.context.layers[self.config["primary"]] - page_count = swap_count = invalid_page_count = large_page_count = large_swap_count = large_invalid_count = other_invalid = 0 + page_count = swap_count = invalid_page_count = large_page_count = ( + large_swap_count + ) = large_invalid_count = other_invalid = 0 if isinstance(layer, intel.Intel): page_addr = 0 @@ -35,8 +41,10 @@ class Statistics(plugins.PluginInterface): while page_addr < layer.maximum_address: try: - _, _, _, page_size, layer_name = list(layer.mapping(page_addr, 2 * expected_page_size))[0] - if layer_name != layer.config['memory_layer']: + _, _, _, page_size, layer_name = list( + layer.mapping(page_addr, 2 * expected_page_size) + )[0] + if layer_name != layer.config["memory_layer"]: swap_count += 1 else: page_count += 1 @@ -44,28 +52,51 @@ class Statistics(plugins.PluginInterface): large_page_count += 1 except exceptions.SwappedInvalidAddressException as excp: swap_count += 1 - page_size = (1 << excp.invalid_bits) + page_size = 1 << excp.invalid_bits if page_size != expected_page_size: large_swap_count += 1 except exceptions.PagedInvalidAddressException as excp: invalid_page_count += 1 - page_size = (1 << excp.invalid_bits) + page_size = 1 << excp.invalid_bits if page_size != expected_page_size: large_invalid_count += 1 except exceptions.InvalidAddressException as excp: other_invalid += 1 page_size = expected_page_size - vollog.debug("A non-page lookup invalid address exception occurred at: {} in layer {}".format( - hex(excp.invalid_address), excp.layer_name)) + vollog.debug( + "A non-page lookup invalid address exception occurred at: {} in layer {}".format( + hex(excp.invalid_address), excp.layer_name + ) + ) page_addr += page_size - self._progress_callback((page_addr * 100) / layer.maximum_address, "Reading memory") + self._progress_callback( + (page_addr * 100) / layer.maximum_address, "Reading memory" + ) - yield (0, (page_count, large_page_count, swap_count, large_swap_count, invalid_page_count, large_invalid_count, - other_invalid)) + yield ( + 0, + ( + page_count, + large_page_count, + swap_count, + large_swap_count, + invalid_page_count, + large_invalid_count, + other_invalid, + ), + ) def run(self): - return renderers.TreeGrid([("Valid pages (all)", int), ("Valid pages (large)", int), - ("Swapped Pages (all)", int), ("Swapped Pages (large)", int), - ("Invalid Pages (all)", int), ("Invalid Pages (large)", int), - ("Other Invalid Pages (all)", int)], self._generator()) + return renderers.TreeGrid( + [ + ("Valid pages (all)", int), + ("Valid pages (large)", int), + ("Swapped Pages (all)", int), + ("Swapped Pages (large)", int), + ("Invalid Pages (all)", int), + ("Invalid Pages (large)", int), + ("Other Invalid Pages (all)", int), + ], + self._generator(), + ) diff --git a/volatility3/schemas/__init__.py b/volatility3/schemas/__init__.py index 65329a4f5..be120f2af 100644 --- a/volatility3/schemas/__init__.py +++ b/volatility3/schemas/__init__.py @@ -6,7 +6,7 @@ import hashlib import json import logging import os -from typing import Set, Any, Dict +from typing import Any, Dict, Optional, Set from volatility3.framework import constants @@ -37,27 +37,45 @@ cached_validations = load_cached_validations() def validate(input: Dict[str, Any], use_cache: bool = True) -> bool: """Validates an input JSON file based upon.""" - format = input.get('metadata', {}).get('format', None) + format = input.get("metadata", {}).get("format", None) if not format: vollog.debug("No schema format defined") return False basepath = os.path.abspath(os.path.dirname(__file__)) - schema_path = os.path.join(basepath, 'schema-' + format + '.json') + schema_path = os.path.join(basepath, "schema-" + format + ".json") if not os.path.exists(schema_path): vollog.debug(f"Schema for format not found: {schema_path}") return False - with open(schema_path, 'r') as s: + with open(schema_path, "r") as s: schema = json.load(s) return valid(input, schema, use_cache) -def create_json_hash(input: Dict[str, Any], schema: Dict[str, Any]) -> str: +def create_json_hash( + input: Dict[str, Any], schema: Optional[Dict[str, Any]] = None +) -> Optional[str]: """Constructs the hash of the input and schema to create a unique identifier for a particular JSON file.""" - return hashlib.sha1(bytes(json.dumps((input, schema), sort_keys = True), 'utf-8')).hexdigest() + if schema is None: + format = input.get("metadata", {}).get("format", None) + if not format: + vollog.debug("No schema format defined") + return None + basepath = os.path.abspath(os.path.dirname(__file__)) + schema_path = os.path.join(basepath, "schema-" + format + ".json") + if not os.path.exists(schema_path): + vollog.debug(f"Schema for format not found: {schema_path}") + return None + with open(schema_path, "r") as s: + schema = json.load(s) + return hashlib.sha1( + bytes(json.dumps((input, schema), sort_keys=True), "utf-8") + ).hexdigest() -def valid(input: Dict[str, Any], schema: Dict[str, Any], use_cache: bool = True) -> bool: +def valid( + input: Dict[str, Any], schema: Dict[str, Any], use_cache: bool = True +) -> bool: """Validates a json schema.""" input_hash = create_json_hash(input, schema) if input_hash in cached_validations and use_cache: @@ -75,7 +93,7 @@ def valid(input: Dict[str, Any], schema: Dict[str, Any], use_cache: bool = True) cached_validations.add(input_hash) vollog.debug("JSON validated against schema (result cached)") except jsonschema.exceptions.SchemaError: - vollog.debug("Schema validation error", exc_info = True) + vollog.debug("Schema validation error", exc_info=True) return False record_cached_validations(cached_validations) diff --git a/volshell.py b/volshell.py old mode 100644 new mode 100755 index 590994324..71d35a47c --- a/volshell.py +++ b/volshell.py @@ -6,5 +6,5 @@ from volatility3.cli import volshell -if __name__ == '__main__': +if __name__ == "__main__": volshell.main()