Skip to content

Commit

Permalink
Merge pull request #1335 from gcmoreira/linux_prot_none_L1TF_and_laye…
Browse files Browse the repository at this point in the history
…r_addr_translation_fixes

Linux - PROT_NONE, L1TF mitigation and layer address translation fix
  • Loading branch information
ikelos authored Nov 27, 2024
2 parents bdd3e1e + eff2a52 commit 0233193
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 12 deletions.
6 changes: 3 additions & 3 deletions volatility3/framework/automagic/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ def stack(
context, table_name, layer_name, progress_callback=progress_callback
)

layer_class: Type = intel.Intel
if "init_top_pgt" in table.symbols:
layer_class = intel.Intel32e
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_top_pgt"
elif "init_level4_pgt" in table.symbols:
layer_class = intel.Intel32e
layer_class = intel.LinuxIntel32e
dtb_symbol_name = "init_level4_pgt"
else:
layer_class = intel.LinuxIntel
dtb_symbol_name = "swapper_pg_dir"

dtb = cls.virtual_to_physical_address(
Expand Down
2 changes: 1 addition & 1 deletion volatility3/framework/constants/_version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# We use the SemVer 2.0.0 versioning scheme
VERSION_MAJOR = 2 # Number of releases of the library with a breaking change
VERSION_MINOR = 11 # Number of changes that only add to the interface
VERSION_MINOR = 12 # Number of changes that only add to the interface
VERSION_PATCH = 0 # Number of changes that do not change the interface
VERSION_SUFFIX = ""

Expand Down
2 changes: 1 addition & 1 deletion volatility3/framework/interfaces/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def maximum_address(self) -> int:
def minimum_address(self) -> int:
"""Returns the minimum valid address of the space."""

@property
@functools.cached_property
def address_mask(self) -> int:
"""Returns a mask which encapsulates all the active bits of an address
for this layer."""
Expand Down
111 changes: 104 additions & 7 deletions volatility3/framework/layers/intel.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
class Intel(linear.LinearlyMappedLayer):
"""Translation Layer for the Intel IA32 memory mapping."""

_PAGE_BIT_PRESENT = 0
_PAGE_BIT_PSE = 7 # Page Size Extension: 4 MB (or 2MB) page
_PAGE_BIT_PROTNONE = 8
_PAGE_BIT_PAT_LARGE = 12 # 2MB or 1GB pages

_PAGE_PRESENT = 1 << _PAGE_BIT_PRESENT
_PAGE_PSE = 1 << _PAGE_BIT_PSE
_PAGE_PROTNONE = 1 << _PAGE_BIT_PROTNONE
_PAGE_PAT_LARGE = 1 << _PAGE_BIT_PAT_LARGE

_entry_format = "<I"
_page_size_in_bits = 12
_bits_per_register = 32
Expand Down Expand Up @@ -163,12 +173,17 @@ def _translate(self, offset: int) -> Tuple[int, int, str]:
entry,
f"Page Fault at entry {hex(entry)} in page entry",
)
page = self._mask(entry, self._maxphyaddr - 1, position + 1) | self._mask(
offset, position, 0
)

pfn = self._pte_pfn(entry)
page_offset = self._mask(offset, position, 0)
page = pfn << self.page_shift | page_offset

return page, 1 << (position + 1), self._base_layer

def _pte_pfn(self, entry: int) -> int:
"""Extracts the page frame number (PFN) from the page table entry (PTE) entry"""
return entry >> self.page_shift

def _translate_entry(self, offset: int) -> Tuple[int, int]:
"""Translates a specific offset based on paging tables.
Expand Down Expand Up @@ -203,10 +218,10 @@ def _translate_entry(self, offset: int) -> Tuple[int, int]:
"Page Fault at entry " + hex(entry) + " in table " + name,
)
# Check if we're a large page
if large_page and (entry & (1 << 7)):
if large_page and (entry & self._PAGE_PSE):
# Mask off the PAT bit
if entry & (1 << 12):
entry -= 1 << 12
if entry & self._PAGE_PAT_LARGE:
entry -= self._PAGE_PAT_LARGE
# We're a large page, the rest is finished below
# If we want to implement PSE-36, it would need to be done here
break
Expand Down Expand Up @@ -252,7 +267,7 @@ def _translate_entry(self, offset: int) -> Tuple[int, int]:

return entry, position

@functools.lru_cache(1025)
@functools.lru_cache(maxsize=1025)
def _get_valid_table(self, base_address: int) -> Optional[bytes]:
"""Extracts the table, validates it and returns it if it's valid."""
table = self._context.layers.read(
Expand Down Expand Up @@ -501,3 +516,85 @@ class WindowsIntel32e(WindowsMixin, Intel32e):

def _translate(self, offset: int) -> Tuple[int, int, str]:
return self._translate_swap(self, offset, self._bits_per_register // 2)


class LinuxMixin(Intel):
@functools.cached_property
def _register_mask(self) -> int:
return (1 << self._bits_per_register) - 1

@functools.cached_property
def _physical_mask(self) -> int:
# From kernels 4.18 the physical mask is dynamic: See AMD SME, Intel Multi-Key Total
# Memory Encryption and CONFIG_DYNAMIC_PHYSICAL_MASK: 94d49eb30e854c84d1319095b5dd0405a7da9362
physical_mask = (1 << self._maxphyaddr) - 1
# TODO: Come back once SME support is available in the framework
return physical_mask

@functools.cached_property
def page_mask(self) -> int:
# Note that within the Intel class it's a class method. However, since it uses
# complement operations and we are working in Python, it would be more careful to
# limit it to the architecture's pointer size.
return ~(self.page_size - 1) & self._register_mask

@functools.cached_property
def _physical_page_mask(self) -> int:
return self.page_mask & self._physical_mask

@functools.cached_property
def _pte_pfn_mask(self) -> int:
return self._physical_page_mask

@functools.cached_property
def _pte_flags_mask(self) -> int:
return ~self._pte_pfn_mask & self._register_mask

def _pte_flags(self, pte) -> int:
return pte & self._pte_flags_mask

def _is_pte_present(self, entry: int) -> bool:
return (
self._pte_flags(entry) & (self._PAGE_PRESENT | self._PAGE_PROTNONE)
) != 0

def _page_is_valid(self, entry: int) -> bool:
# Overrides the Intel static method with the Linux-specific implementation
return self._is_pte_present(entry)

def _pte_needs_invert(self, entry) -> bool:
# Entries that were set to PROT_NONE (PAGE_PRESENT) are inverted
# A clear PTE shouldn't be inverted. See f19f5c4
return entry and not (entry & self._PAGE_PRESENT)

def _protnone_mask(self, entry: int) -> int:
"""Gets a mask to XOR with the page table entry to get the correct PFN"""
return self._register_mask if self._pte_needs_invert(entry) else 0

def _pte_pfn(self, entry: int) -> int:
"""Extracts the page frame number from the page table entry"""
pfn = entry ^ self._protnone_mask(entry)
return (pfn & self._pte_pfn_mask) >> self.page_shift


class LinuxIntel(LinuxMixin, Intel):
pass


class LinuxIntelPAE(LinuxMixin, IntelPAE):
pass


class LinuxIntel32e(LinuxMixin, Intel32e):
# In the Linux kernel, the __PHYSICAL_MASK_SHIFT is a mask used to extract the
# physical address from a PTE. In Volatility3, this is referred to as _maxphyaddr.
#
# Until kernel version 4.17, Linux x86-64 used a 46-bit mask. With commit
# b83ce5ee91471d19c403ff91227204fb37c95fb2, this was extended to 52 bits,
# applying to both 4 and 5-level page tables.
#
# We initially used 52 bits for all Intel 64-bit systems, but this produced incorrect
# results for PROT_NONE pages. Since the mask value is defined by a preprocessor macro,
# it's difficult to detect the exact bit shift used in the current kernel.
# Using 46 bits has proven reliable for our use case, as seen in tools like crashtool.
_maxphyaddr = 46

0 comments on commit 0233193

Please sign in to comment.