Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : Hibernation Layer and plugins. #1036

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b0cbd3c
Adding hibernation support
k1nd0ne Oct 26, 2023
c91e417
Fixing Huffman
forensicxlab Oct 27, 2023
5c44467
Adapting comments to the codec instead of the volatility3 prefetch pl…
k1nd0ne Oct 27, 2023
a13e105
Update the Xpress LZ77+Huffman decoder
k1nd0ne Oct 30, 2023
3568d3f
Code comments and cleaning.
k1nd0ne Oct 30, 2023
7e87e2d
Plugins added : hibernation.Info and hibernation.Dump. Support for ad…
k1nd0ne Nov 3, 2023
142baa6
Adding support for Windows 10 2016 1607
k1nd0ne Nov 3, 2023
cfab14f
Only parse the kernel section if the user is using the 'windows.hiber…
k1nd0ne Nov 3, 2023
f6b960a
Quick code review and comments to make it more readable. Enhanced plu…
k1nd0ne Feb 4, 2024
8db03c3
using black on the codecs
k1nd0ne Feb 4, 2024
d152b48
fixing mistakes in the lz77+huffman decompression algorithm
k1nd0ne Feb 4, 2024
6213d45
Adding codecs + black
k1nd0ne Jul 15, 2024
2bebe91
Formatting using good black version
k1nd0ne Jul 15, 2024
9fd1567
Deported the decompression algorithm in a dedicated python3 package
k1nd0ne Jul 23, 2024
9b855f1
Deported the decompression algorithm in a dedicated python3 package
k1nd0ne Jul 23, 2024
2ffa3a9
conflict
k1nd0ne Jul 23, 2024
9379e16
Merging to resolve conflict
k1nd0ne Jul 23, 2024
b17eab6
requirements
k1nd0ne Jul 23, 2024
3f428f7
Fixing requirements
k1nd0ne Jul 23, 2024
ad31052
Fixing unused import
k1nd0ne Jul 23, 2024
9d478f3
Merge branch 'volatilityfoundation:develop' into feature/hibernation-…
forensicxlab Jul 28, 2024
d779c0e
Upgrading pipeline and python3 version minimum requirement
k1nd0ne Jul 28, 2024
c947999
Upgrading pipeline and python3 version minimum requirement
k1nd0ne Jul 28, 2024
0d6c1ea
Using symbol Tables (1/2)
k1nd0ne Aug 5, 2024
f378a1a
Merge branch 'volatilityfoundation:develop' into feature/hibernation-…
forensicxlab Aug 5, 2024
3b1f2ae
Sync
Dec 1, 2024
3778c97
Merge branch 'develop' into feature/hibernation-layer
k1nd0ne Dec 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The following packages are required for core functionality.
pefile>=2023.2.7
# Include the minimal requirements
-r requirements-minimal.txt

# The following packages are optional.
# If certain packages are not necessary, place a comment (#) at the start of the line.
Expand All @@ -15,8 +15,8 @@ capstone>=3.0.5
pycryptodome

# This is required for memory acquisition via leechcore/pcileech.
leechcorepyc>=2.4.0
leechcorepyc>=2.4.0; sys_platform != 'darwin'

# This is required for memory analysis on a Amazon/MinIO S3 and Google Cloud object storage
gcsfs>=2023.1.0
s3fs>=2023.1.0
s3fs>=2023.1.0
9 changes: 3 additions & 6 deletions volatility3/framework/layers/codecs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
# This file is Copyright 2022 Volatility Foundation and licensed under the Volatility Software License 1.0
# 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 codecs

"""Codecs used for encoding or decoding data should live here


"""
# Register codecs here.
268 changes: 268 additions & 0 deletions volatility3/framework/layers/hib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# 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
#
# References:
# - https://www.forensicxlab.com/posts/hibernation/ : Vulgarized description of the hibernation file structure and the implementation of this layer.
# - https://www.cct.lsu.edu/~golden/Papers/sylvehiber.pdf : Scientific paper.
# - https://www.vergiliusproject.com/kernels/x64/ : Windows kernel structures used to track the evolution of the hibernation file structure in time.
# - https://pypi.org/project/xpress-lz77/: The decompression algorithm developped for the integration to volatility3

from typing import Optional
import logging, struct, codecs, xpress_lz77
Fixed Show fixed Hide fixed

from volatility3.framework import interfaces, constants, exceptions
from volatility3.framework.layers import segmented


vollog = logging.getLogger(__name__)


def uncompress(data: bytes, flag):
"""
Desc:
Params:
- data: the compressed data from a compression set
- flag: what is the decompression algorithm to use.
- out_size: Size of the decompressed data
Return: The decompressed data (consecutive pages).
"""
if flag == 0 or flag == 1:
return bytes(xpress_lz77.lz77_plain_decompress_py(data))

elif flag == 2 or flag == 3:
return bytes(xpress_lz77.lz77_huffman_decompress_py(data, 65536))
else:
vollog.warning(
f"A compression set could not be decompressed: Compression algorithm : {flag}"
)
raise ValueError("Cannot decompress the data.")


class HibernationLayer(segmented.NonLinearlySegmentedLayer):
"""
A TranslationLayer that maps physical memory against a x64 Microsoft Windows hibernation file.
This Translation Layer is meant to be used in conjunction with the Hibernation.Info and Hibernation.Dump plugins.
"""

WINDOWS_10_2016_1703_TO_23H2 = 0
WINDOW_8 = 1
WINDOWS_10_2016_1507_1511 = 2
WINDOWS_10_2016_1607 = 3

# TODO: Make me compatible with x86 by adding options to the Hib plugins.
PAGE_SIZE = 4096 # x64 page size.
HEADER_SIZE = 4
PAGE_DESC_SIZE = 8

def __init__(
self,
context: interfaces.context.ContextInterface,
config_path: str,
name: str,
**kwargs,
):
"""
Initializes the Hibernation file layer.
"""
self._compressed = (
{}
) # Keep track of which compression algorithm by each mapped compressed data.
self._mapping = (
{}
) # This will hold the mapping between the PageNumber in the decompressed data vs the physical page number.

if "plugins.Dump.version" in context.config:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this translation layer is meant to be used with the hibernation.Dump plugin makes me feel bad about this part.
Indeed the FirstKernelRestorePage and KernelPagesProcessed offsets can only be determine via the user input for now.

Is making a translation layer partially dependant to a plugin is an acceptable concept in the volatility3 framework ?

This concept is working well with this implementation and seems to have no side effects from all the tests I've made but an external eye from your side will be delightful :D I propably missed something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the only requirement for the layer, then make it a requirement (like the kernel_virtual_offset for Intel layers), then anything can instantiate it as long as they provide the right version number for the layer (it might be better if the layer could figure it out for itself, but if not then ask the instantiator to provide it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not find a way to figure the version out by itself in my research at time of writing, I'll look at what’s done in the Intel layer thanks for the tip!

Copy link
Member

@ikelos ikelos Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably add a PluginRequirement and that should do you. 5:)

# The user is using the hibernation.Dump plugin, so the version must be known.
# See possible version in the table below.
self.version = context.config["plugins.Dump.version"]
else:
self.version = -1

self.NPFL_OFFSET = 0x058 # (NumPagesForLoader)
self.FBRP_OFFSET = 0x068 # (FirstBootRestorePage)

"""
Mapping for each 'group' of Windows version sharing the same offsets
---------------------------------------------------------------------------------------------------------
| Windows Versions | FirstKernelRestorePage (FKRP) | KernelPagesProcessed (KPP)|
| ------------------------------------------|:-----------------------------:|:-------------------------:|
| Windows 8/8.1 | 0x68 | 0x1C8 |
| Windows 10 2016 1507-1511 | 0x70 | 0x218 |
| Windows 10 2016 1607 | 0x70 | 0x220 |
| Windows 10 2016 1703 - Windows 11 23H2 | 0x70 | 0x230 |
---------------------------------------------------------------------------------------------------------
"""
if self.version == self.WINDOWS_10_2016_1703_TO_23H2:
self.FKRP_OFFSET = 0x070
self.KPP_OFFSET = 0x230
elif self.version == self.WINDOW_8:
self.FKRP_OFFSET = 0x68
self.KPP_OFFSET = 0x1C8
elif self.version == self.WINDOWS_10_2016_1507_1511:
self.FKRP_OFFSET = 0x70
self.KPP_OFFSET = 0x218
elif self.version == self.WINDOWS_10_2016_1607:
self.FKRP_OFFSET = 0x70
self.KPP_OFFSET = 0x220
else:
raise exceptions.LayerException(name, "The version provided is not valid")
super().__init__(context, config_path, name, **kwargs)

@classmethod
def _check_header(
cls, base_layer: interfaces.layers.DataLayerInterface, name: str = ""
):
header = base_layer.read(0, 4)
if header != b"HIBR":
raise exceptions.LayerException(name, "No Hibernation magic bytes")
else:
vollog.info("Detecting an hibernation file")

def _load_segments(self):
"""
Loading segments is a 2 STEP operation:
- Step 1: extracting the pages from the BootSection if any.
- Step 2: extracting the pages from the KernelSection if any.
"""
base_layer = self.context.layers[self._base_layer]
NumPagesForLoader = int.from_bytes(
base_layer.read(self.NPFL_OFFSET, 8), "little"
)
FirstBootRestorePage = int.from_bytes(
base_layer.read(self.FBRP_OFFSET, 8), "little"
)

offset = FirstBootRestorePage * self.PAGE_SIZE
total_pages = NumPagesForLoader
treated = 0

while total_pages > treated:
page_read, next_compression_set = self._read_compression_set(offset)
offset += next_compression_set
treated += page_read

if "plugins.Dump.version" in self.context.config:
# The user is using the hibernation.Dump plugin so we can parse the KernelSection
FirstKernelRestorePage = int.from_bytes(
base_layer.read(self.FKRP_OFFSET, 8), "little"
)
KernelPagesProcessed = int.from_bytes(
base_layer.read(self.KPP_OFFSET, 8), "little"
)
offset = FirstKernelRestorePage * self.PAGE_SIZE
total_pages = KernelPagesProcessed

treated = 0
while total_pages > treated:
page_read, next_compression_set = self._read_compression_set(offset)
offset += next_compression_set
treated += page_read
self._segments = sorted(self._segments, key=lambda x: x[0])

def _read_compression_set(self, offset):
"""
Desc: Read one compression set an extract the address of the compressed data
Params:
- offset : the location of the compression set to read.
- stream : the hibernation file stream.
Return: The offset of the compressed data and the size.
"""

base_layer = self.context.layers[self._base_layer]
header = base_layer.read(offset, self.HEADER_SIZE)
data = struct.unpack("<I", header)[0] # Compression set header extraction.
number_of_descs = data & 0xFF # First 8 least significant bits.
if number_of_descs == 0 or number_of_descs > 16:
# See references
raise exceptions.LayerException(
self.name, "The hibernation file is corrupted."
)

size_of_compressed_data = (
data >> 8
) & 0x3FFFFF # Next 22 least significant bytes.
huffman_compressed = (data >> 30) & 0x3 # Most significant bit.

# Now we know where is the start of the page descriptors in the hibernation file.
mapped_address = (
offset + self.HEADER_SIZE + number_of_descs * self.PAGE_DESC_SIZE
)
total_page_count = 0
position = 0
for i in range(number_of_descs):
# Go fetch and parse each page descriptor.
location = offset + self.HEADER_SIZE + i * self.PAGE_DESC_SIZE
page_descriptor = base_layer.read(location, self.PAGE_DESC_SIZE)
data = struct.unpack("<Q", page_descriptor)[0]
Numpages = data & 0b1111 # Get the lower 4 bits
PageNum = data >> 4 # Shift right 4 bits to get the upper 60 bits
page_count = 1 + Numpages
total_page_count += page_count
self._segments.append(
(
PageNum * self.PAGE_SIZE,
mapped_address,
self.PAGE_SIZE * page_count,
size_of_compressed_data,
)
)
for j in range(page_count):
# Track the physical page number vs the page number in the compression set
self._mapping[(PageNum + j) * self.PAGE_SIZE] = (
position * self.PAGE_SIZE
)
position += 1

total_page_size = total_page_count * self.PAGE_SIZE

if total_page_size != size_of_compressed_data:
# This means compression so we track wich compression sets we actually need to decompress
self._compressed[mapped_address] = huffman_compressed
return total_page_count, (
4 + size_of_compressed_data + number_of_descs * self.PAGE_DESC_SIZE
) # Number of pages in the set, Size of the entire compression set

def _decode_data(
self, data: bytes, mapped_offset: int, offset: int, output_length: int
) -> bytes:
"""
Desc: decode the compressed data of one compression set
Params:
- data : the compressed data
- mapped_offset : starting location of the compressed data in the hib file
- offset: The offset inside the resulting raw file
- output_length: what is the size of the expected decompressed pages
Return: The decompressed data
"""
start_offset, _mapped_offset, _size, _mapped_size = self._find_segment(offset)
if mapped_offset in self._compressed:
decoded_data = uncompress(data=data, flag=self._compressed[mapped_offset])
else:
# The data is not in our mapping so it's uncompressed.
decoded_data = data
page_offset = self._mapping[start_offset]
decoded_data = decoded_data[page_offset + (offset - start_offset) :]
decoded_data = decoded_data[:output_length]
return decoded_data


class HibernationFileStacker(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]:
try:
HibernationLayer._check_header(context.layers[layer_name])
except exceptions.LayerException:
return None
new_name = context.layers.free_layer_name("HibernationLayer")
context.config[interfaces.configuration.path_join(new_name, "base_layer")] = (
layer_name
)
layer = HibernationLayer(context, new_name, new_name)
return layer
Loading
Loading