diff --git a/test/automagic/__init__.py b/test/automagic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/automagic/test_windows.py b/test/automagic/test_windows.py new file mode 100644 index 0000000000..93ece8abb8 --- /dev/null +++ b/test/automagic/test_windows.py @@ -0,0 +1,167 @@ +# This file is Copyright 2026 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""Unit tests for the Windows automagic, focused on LA57 (5-level paging) +detection from the x64 Low Stub. + +These build a synthetic _PROCESSOR_START_BLOCK inside a BufferDataLayer, so +no memory image is required. +""" + +import struct + +from volatility3.framework import constants, contexts +from volatility3.framework.automagic import windows +from volatility3.framework.layers import intel, physical + +STUB_OFFSET = 0x2000 +CR3_VALUE = 0x1AD000 +BUFFER_SIZE = 0x3000 + + +def build_low_stub_buffer(cr4_value: int, *, stub: bool = True) -> bytes: + """Builds a physical buffer optionally containing a valid x64 Low Stub.""" + buffer = bytearray(BUFFER_SIZE) + if stub: + buffer[STUB_OFFSET : STUB_OFFSET + 8] = struct.pack( + " bytes: + """Builds a synthetic Windows physical image with a self-referential x64 + DTB and (optionally) a Low Stub advertising LA57 via CR4.""" + buffer = bytearray(STACK_IMAGE_SIZE) + + # --- Self-referential top-level page table (PML4/PML5) at STACK_DTB_OFFSET + def put_entry(index: int, value: int) -> None: + off = STACK_DTB_OFFSET + index * 8 + buffer[off : off + 8] = struct.pack("= 10 valid pointers). None set bit 7 (reserved) and + # none alias the DTB offset. + for i in range(12): + put_entry(i, (0x10000 + i * 0x1000) | 0x3) + + # --- x64 Low Stub (_PROCESSOR_START_BLOCK) ----------------------------- + if with_stub: + sig_off = STACK_STUB_OFFSET + buffer[sig_off : sig_off + 8] = struct.pack( + " ordinary 4-level layer, not the LA57 subclass + assert type(layer) is intel.WindowsIntel32e + + def test_stacks_plain_64bit_without_low_stub(self): + # No Low Stub at all (e.g. a snapshot) -> fall back to 4-level + layer = stack_la57_image(cr4_la57=True, with_stub=False) + assert layer is not None + assert type(layer) is intel.WindowsIntel32e diff --git a/test/layers/__init__.py b/test/layers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/layers/test_intel.py b/test/layers/test_intel.py new file mode 100644 index 0000000000..01749c87f6 --- /dev/null +++ b/test/layers/test_intel.py @@ -0,0 +1,268 @@ +# This file is Copyright 2026 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# +"""Unit tests for the Intel translation layers, with a focus on 5-level +paging (LA57). + +These tests build synthetic page tables inside a BufferDataLayer, so no +memory image is required. They cover: + +- The full 5-level translation walk (4K, 2M and 1G pages) +- Canonicalization / decanonicalization of 57-bit addresses +- The address_mask regression that broke virtual pointer following + (pslist failing while psscan worked) on LA57 images +- Windows transition / swapped page table entries on the LA57 layer +- Linux PROT_NONE PFN inversion on the LA57 layer +""" + +import struct + +import pytest + +from volatility3.framework import contexts, exceptions +from volatility3.framework.layers import intel, physical + +# Physical layout of the synthetic image +PML5_OFFSET = 0x1000 +PML4_OFFSET = 0x2000 +PDPT_OFFSET = 0x3000 +PD_OFFSET = 0x4000 +PT_OFFSET = 0x5000 +DATA_OFFSET = 0x6000 +BUFFER_SIZE = 0x10000 + +DATA_CONTENTS = b"LA57 5-level paging test page!!\x00" * (0x1000 // 32) + +# Table indices used by the synthetic mappings (all non-zero, all distinct, +# so no table trips the duplicate-entry heuristic in _get_valid_table) +I5, I4, I3, I2, I1 = 0x1A, 0x2B, 0x3C, 0x4D, 0x5E +I5_HIGH = 0x1A0 # bit 8 set -> high (kernel) half of the 57-bit space +I3_1G = 0x3D # 1G large page entry in the PDPT +I2_2M = 0x4E # 2M large page entry in the PD +I1_FAULT = 0x5F # PT entry left as zero -> page fault +I1_TRANS = 0x60 # Windows: PTE in transition state +I1_SWAP = 0x61 # Windows: swapped-out PTE +I1_PROT = 0x62 # Linux: PROT_NONE (inverted PFN) PTE + +PHYS_2M = 0x800000 # 2M-aligned, intentionally outside the buffer +PHYS_1G = 0x40000000 # 1G-aligned, intentionally outside the buffer + +ENTRY_FLAGS = 0x3 # present | writable +LARGE_FLAGS = 0x83 # present | writable | PSE + + +def make_va(i5: int, i4: int, i3: int, i2: int, i1: int, offset: int) -> int: + """Builds a (decanonicalized) 57-bit virtual address from table indices.""" + return (i5 << 48) | (i4 << 39) | (i3 << 30) | (i2 << 21) | (i1 << 12) | offset + + +VA_4K = make_va(I5, I4, I3, I2, I1, 0x123) +VA_HIGH = make_va(I5_HIGH, I4, I3, I2, I1, 0x123) +VA_1G = make_va(I5, I4, I3_1G, 0, 0, 0) | 0x1234567 +VA_2M = make_va(I5, I4, I3, I2_2M, 0, 0) | 0x12345 +VA_FAULT = make_va(I5, I4, I3, I2, I1_FAULT, 0) +VA_TRANS = make_va(I5, I4, I3, I2, I1_TRANS, 0x123) +VA_SWAP = make_va(I5, I4, I3, I2, I1_SWAP, 0) +VA_PROT = make_va(I5, I4, I3, I2, I1_PROT, 0x123) + +SWAP_PAGE_NUMBER = 0x1234 + + +def build_la57_buffer(layer_class) -> bytes: + """Builds a synthetic physical memory buffer containing 5-level page + tables rooted at PML5_OFFSET.""" + buffer = bytearray(BUFFER_SIZE) + + def set_entry(table_offset: int, index: int, value: int) -> None: + buffer[table_offset + index * 8 : table_offset + (index + 1) * 8] = ( + struct.pack(" intel.Intel: + """Creates a context with a synthetic physical layer and stacks the + requested Intel layer class on top of it.""" + context = contexts.Context() + base_name = "base_layer" + base_layer = physical.BufferDataLayer( + context, "test.base", base_name, build_la57_buffer(layer_class) + ) + context.add_layer(base_layer) + + config_path = "test.intel" + context.config[f"{config_path}.memory_layer"] = base_name + context.config[f"{config_path}.page_map_offset"] = PML5_OFFSET + layer = layer_class(context, config_path, "intel_layer") + context.add_layer(layer) + return layer + + +class TestIntel32eLA57Constants: + def test_class_constants(self): + assert intel.Intel32e_LA57._maxvirtaddr == 57 + assert len(intel.Intel32e_LA57._structure) == 5 + assert intel.Intel32e_LA57.maximum_address == (1 << 57) - 1 + # Plugins require Intel32/Intel64 architectures; LA57 must stay Intel64 + assert intel.Intel32e_LA57._direct_metadata["architecture"] == "Intel64" + + def test_address_mask_covers_57_bits(self): + layer = build_layer(intel.Intel32e_LA57) + assert layer.address_mask == (1 << 57) - 1 + + def test_4level_address_mask_unchanged(self): + """Regression: the 4-level layer must keep its 48-bit mask.""" + assert intel.Intel32e.maximum_address == (1 << 48) - 1 + + +class TestIntel32eLA57Translation: + def test_4k_page_translation(self): + layer = build_layer(intel.Intel32e_LA57) + mapped_offset, size, layer_name = layer._translate(VA_4K) + assert mapped_offset == DATA_OFFSET | 0x123 + assert size == 0x1000 + assert layer_name == "base_layer" + + def test_4k_page_read(self): + layer = build_layer(intel.Intel32e_LA57) + assert layer.read(VA_4K & ~0xFFF, 0x1000) == DATA_CONTENTS + + def test_high_half_canonical_address(self): + """A sign-extended (canonical) kernel address must translate to the + same physical page as its decanonicalized form.""" + layer = build_layer(intel.Intel32e_LA57) + canonical_va = layer.canonicalize(VA_HIGH) + assert canonical_va >> 57 != 0 # really sign-extended + mapped_offset, _, _ = layer._translate(canonical_va) + assert mapped_offset == DATA_OFFSET | 0x123 + + def test_2m_large_page(self): + layer = build_layer(intel.Intel32e_LA57) + mapped_offset, size, _ = layer._translate(VA_2M) + assert mapped_offset == PHYS_2M | 0x12345 + assert size == 1 << 21 + + def test_1g_large_page(self): + layer = build_layer(intel.Intel32e_LA57) + mapped_offset, size, _ = layer._translate(VA_1G) + assert mapped_offset == PHYS_1G | 0x1234567 + assert size == 1 << 30 + + def test_page_fault(self): + layer = build_layer(intel.Intel32e_LA57) + with pytest.raises(exceptions.PagedInvalidAddressException) as excinfo: + layer.read(VA_FAULT, 1) + assert excinfo.value.invalid_bits == 12 + + def test_mapping_skips_invalid_pages(self): + layer = build_layer(intel.Intel32e_LA57) + page_va = VA_4K & ~0xFFF + # Two pages: the faulting one and the valid one right after it + mappings = list(layer.mapping(page_va - 0x1000, 0x2000, ignore_errors=True)) + assert mappings == [(page_va, 0x1000, DATA_OFFSET, 0x1000, "base_layer")] + + def test_non_canonical_address_is_masked(self): + """The framework deliberately masks addresses with address_mask + rather than rejecting non-canonical ones (matching the 4-level + layers and the Pointer masking in objects/__init__.py), so a + non-canonical address translates as its masked equivalent.""" + layer = build_layer(intel.Intel32e_LA57) + # Bit 62 set but bit 56 clear: non-canonical under LA57 + mapped_offset, _, _ = layer._translate((1 << 62) | VA_4K) + assert mapped_offset == DATA_OFFSET | 0x123 + + +class TestIntel32eLA57Canonicalization: + def test_canonicalize_boundaries(self): + layer = build_layer(intel.Intel32e_LA57) + # Highest low-half address is untouched + assert layer.canonicalize((1 << 56) - 1) == (1 << 56) - 1 + # First high-half address gets sign-extended from bit 56 + assert layer.canonicalize(1 << 56) == 0xFF00000000000000 + assert layer.decanonicalize(0xFF00000000000000) == 1 << 56 + assert layer.decanonicalize((1 << 56) - 1) == (1 << 56) - 1 + + def test_canonicalize_roundtrip(self): + layer = build_layer(intel.Intel32e_LA57) + for addr in ((1 << 56) | 0x1234, (1 << 57) - 1, 0x123456789A, 0): + assert layer.decanonicalize(layer.canonicalize(addr)) == addr + + def test_pointer_masking_preserves_la57_pointers(self): + """The original bug: 57-bit canonical kernel pointers were truncated + to 48 bits by address_mask during pointer dereference + (objects/__init__.py), so list walking (pslist) broke while physical + scanning (psscan) kept working.""" + kernel_ptr = 0xFF91800000001234 # canonical 57-bit kernel pointer + + la57 = build_layer(intel.Intel32e_LA57) + masked = kernel_ptr & la57.address_mask + # Masking must be equivalent to decanonicalization: no information loss + assert masked == la57.decanonicalize(kernel_ptr) + assert la57.canonicalize(masked) == kernel_ptr + + # The same pointer through the 4-level mask is destroyed, which + # documents why the bug only manifests on LA57 images + mask_48 = (1 << 48) - 1 + assert ((kernel_ptr & mask_48) | (0xFFFF << 48)) != kernel_ptr + + +class TestWindowsIntel32eLA57: + def test_transition_page_is_valid(self): + """A PTE in transition state (bit 11 set, prototype clear) must + still translate on the LA57 layer.""" + layer = build_layer(intel.WindowsIntel32e_LA57) + mapped_offset, _, _ = layer._translate(VA_TRANS) + assert mapped_offset == DATA_OFFSET | 0x123 + + def test_swapped_page_raises_swap_exception(self): + layer = build_layer(intel.WindowsIntel32e_LA57) + with pytest.raises(exceptions.SwappedInvalidAddressException) as excinfo: + layer.read(VA_SWAP, 1) + assert excinfo.value.swap_offset == SWAP_PAGE_NUMBER << 12 + + def test_4k_page_read(self): + layer = build_layer(intel.WindowsIntel32e_LA57) + assert layer.read(VA_4K & ~0xFFF, 0x1000) == DATA_CONTENTS + + +class TestLinuxIntel32eLA57: + def test_maxphyaddr_is_52_bits(self): + """5-level kernels always use a 52-bit __PHYSICAL_MASK_SHIFT.""" + assert intel.LinuxIntel32e_LA57._maxphyaddr == 52 + + def test_protnone_inverted_pfn(self): + """A PROT_NONE PTE stores its PFN inverted (L1TF mitigation); the + LinuxMixin must recover the original PFN on the LA57 layer.""" + layer = build_layer(intel.LinuxIntel32e_LA57) + mapped_offset, _, _ = layer._translate(VA_PROT) + assert mapped_offset == DATA_OFFSET | 0x123 + + def test_4k_page_read(self): + layer = build_layer(intel.LinuxIntel32e_LA57) + assert layer.read(VA_4K & ~0xFFF, 0x1000) == DATA_CONTENTS diff --git a/volatility3/framework/automagic/linux.py b/volatility3/framework/automagic/linux.py index 511b957310..03557fd974 100644 --- a/volatility3/framework/automagic/linux.py +++ b/volatility3/framework/automagic/linux.py @@ -5,7 +5,7 @@ import logging from typing import Optional, Tuple -from volatility3.framework import constants, interfaces +from volatility3.framework import constants, exceptions, interfaces from volatility3.framework.automagic import symbol_cache, symbol_finder from volatility3.framework.configuration import requirements from volatility3.framework.layers import intel, scanners @@ -71,7 +71,10 @@ def stack( ) if "init_top_pgt" in table.symbols: - layer_class = intel.LinuxIntel32e + if cls._l5_paging_enabled(context, layer_name, table, kaslr_shift): + layer_class = intel.LinuxIntel32e_LA57 + else: + layer_class = intel.LinuxIntel32e dtb_symbol_name = "init_top_pgt" elif "init_level4_pgt" in table.symbols: layer_class = intel.LinuxIntel32e @@ -197,6 +200,37 @@ def find_aslr( vollog.debug("Scanners could not determine any ASLR shifts, using 0 for both") return 0, 0 + @classmethod + def _l5_paging_enabled( + cls, + context: interfaces.context.ContextInterface, + layer_name: str, + table: linux.LinuxKernelIntermedSymbols, + kaslr_shift: int, + ) -> bool: + """Determines whether the kernel is running with 5-level page tables + (LA57). + + Kernels built with CONFIG_X86_5LEVEL only switch to 5-level paging + at boot when the CPU supports LA57, recording the decision in + __pgtable_l5_enabled (kernels >= 4.17). The value is read directly + from the physical layer, since no Intel layer exists yet. The + kernel text mapping used by virtual_to_physical_address is identical + under 4 and 5-level paging, so the conversion remains valid. + """ + if "__pgtable_l5_enabled" not in table.symbols: + return False + vaddr = table.get_symbol("__pgtable_l5_enabled").address + kaslr_shift + paddr = cls.virtual_to_physical_address(vaddr) + try: + value = context.layers[layer_name].read(paddr, 4) + except exceptions.InvalidAddressException: + vollog.debug("Unable to read __pgtable_l5_enabled, assuming 4-level paging") + return False + enabled = int.from_bytes(value, "little") != 0 + vollog.debug(f"Linux 5-level paging (LA57) enabled: {enabled}") + return enabled + @staticmethod def virtual_to_physical_address(addr: int) -> int: """Converts a virtual linux address to a physical one (does not account @@ -295,6 +329,10 @@ def stack( is_32bit, is_pae = cls._vmcoreinfo_is_32bit(vmcoreinfo) if is_32bit: layer_class = intel.IntelPAE if is_pae else intel.Intel + elif vmcoreinfo.get("NUMBER(pgtable_l5_enabled)", 0): + # Exported by kernels >= 4.17, set when the kernel switched + # to 5-level paging (LA57) at boot + layer_class = intel.Intel32e_LA57 else: layer_class = intel.Intel32e diff --git a/volatility3/framework/automagic/pdbscan.py b/volatility3/framework/automagic/pdbscan.py index c03db7582e..5a0ddc2b73 100644 --- a/volatility3/framework/automagic/pdbscan.py +++ b/volatility3/framework/automagic/pdbscan.py @@ -268,8 +268,11 @@ def _method_layer_pdb_scan( 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 scanning near the top of the canonical kernel space. The + # top-level table index width is (maxvirtaddr - 9), so this is + # 0x1F0 << 39 for 4-level and 0x1F0 << 48 for 5-level (LA57). + maxvirtaddr = context.layers[layer_to_scan]._maxvirtaddr + start_scan_address = 0x1F0 << (maxvirtaddr - 9) kernel_pdb_names = [ bytes(name + ".pdb", "utf-8") @@ -439,8 +442,8 @@ def method_low_stub_offset( ) if 0x3 & potential_kernel_hint: continue - kernel_hint = potential_kernel_hint & 0xFFFFFFFFFFFF - kernel_base = kernel_hint & (~0x1FFFFF) & 0xFFFFFFFFFFFF + kernel_hint = potential_kernel_hint & vlayer.address_mask + kernel_base = kernel_hint & (~0x1FFFFF) & vlayer.address_mask break except exceptions.InvalidAddressException: continue diff --git a/volatility3/framework/automagic/windows.py b/volatility3/framework/automagic/windows.py index 1627f7c1a8..b3c978eb02 100644 --- a/volatility3/framework/automagic/windows.py +++ b/volatility3/framework/automagic/windows.py @@ -31,7 +31,7 @@ import struct from typing import Generator, Iterable, List, Optional, Tuple, Type -from volatility3.framework import constants, interfaces, layers +from volatility3.framework import constants, exceptions, interfaces, layers from volatility3.framework.configuration import requirements from volatility3.framework.layers import intel @@ -194,6 +194,55 @@ class WindowsIntelStacker(interfaces.automagic.StackerLayerInterface): stack_order = 40 exclusion_list = ["mac", "linux"] + @staticmethod + def _la57_from_low_stub( + base_layer: interfaces.layers.DataLayerInterface, + page_map_offset: Optional[int] = None, + ) -> Optional[bool]: + """Determines whether 5-level paging (LA57) is enabled by reading CR4 + from the x64 Low Stub (_PROCESSOR_START_BLOCK). + + The Low Stub holds the boot processor's CR3 and CR4 in its + SpecialRegisters. CR4 bit 12 (LA57) is the authoritative runtime + indicator of 5-level paging, which the self-referential DTB scan + cannot distinguish (a PML5 looks identical to a PML4). + + Returns True/False when a matching Low Stub is found, or None when no + Low Stub is present (e.g. virtualized snapshots), in which case the + caller should fall back to its default 4-level behaviour. + """ + for offset in range(0x1000, 0x100000, 0x1000): + try: + jmp_and_completion = int.from_bytes( + base_layer.read(offset, 0x8), "little" + ) + if ( + 0xFFFFFFFFFFFF00FF & jmp_and_completion + != constants.windows.JMP_AND_COMPLETION_SIGNATURE + ): + continue + cr3_value = int.from_bytes( + base_layer.read( + offset + constants.windows.PROCESSOR_START_BLOCK_CR3_OFFSET, 0x8 + ), + "little", + ) + # If we already know the DTB, only trust a Low Stub that agrees + if page_map_offset is not None and ( + cr3_value & ~0xFFF + ) != (page_map_offset & ~0xFFF): + continue + cr4_value = int.from_bytes( + base_layer.read( + offset + constants.windows.PROCESSOR_START_BLOCK_CR4_OFFSET, 0x8 + ), + "little", + ) + return bool(cr4_value & constants.windows.CR4_LA57_MASK) + except exceptions.InvalidAddressException: + continue + return None + # Group these by region so we only run over the data once test_sets = [ ( @@ -256,6 +305,10 @@ def stack( layer_type: Type = intel.WindowsIntel if arch == "Intel64": layer_type = intel.WindowsIntel32e + if cls._la57_from_low_stub( + base_layer, base_layer.metadata.get("page_map_offset") + ): + layer_type = intel.WindowsIntel32e_LA57 elif base_layer.metadata.get("pae", False): layer_type = intel.WindowsIntelPAE # Construct the layer @@ -362,7 +415,14 @@ def page_table_is_dummy(page_table, ptr_size: int): config_path, "page_map_offset" ) ] = page_map_offset - layer = test.layer_type( + # A self-referential PML5 is indistinguishable from a PML4, + # so refine the 64-bit choice with CR4.LA57 from the Low Stub + layer_type = test.layer_type + if layer_type is intel.WindowsIntel32e and cls._la57_from_low_stub( + base_layer, page_map_offset + ): + layer_type = intel.WindowsIntel32e_LA57 + layer = layer_type( context, config_path=config_path, name=new_layer_name, diff --git a/volatility3/framework/constants/windows/__init__.py b/volatility3/framework/constants/windows/__init__.py index b08713cc99..00adcfa7b7 100644 --- a/volatility3/framework/constants/windows/__init__.py +++ b/volatility3/framework/constants/windows/__init__.py @@ -29,4 +29,10 @@ # CR3 register within structures describing initial processor state to be started PROCESSOR_START_BLOCK_CR3_OFFSET = 0xA0 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr3, ULONG64 8 bytes +# CR4 register, immediately after Cr3 in KSPECIAL_REGISTERS (Cr0, Cr2, Cr3, Cr4) +PROCESSOR_START_BLOCK_CR4_OFFSET = 0xA8 # PROCESSOR_START_BLOCK->ProcessorState->SpecialRegisters->Cr4, ULONG64 8 bytes + +# CR4.LA57 (bit 12): set when the CPU is running with 5-level paging +CR4_LA57_MASK = 1 << 12 + MAX_PID = 0xFFFFFFFC diff --git a/volatility3/framework/layers/intel.py b/volatility3/framework/layers/intel.py index 380d0e49f4..3a2db62e68 100644 --- a/volatility3/framework/layers/intel.py +++ b/volatility3/framework/layers/intel.py @@ -489,6 +489,26 @@ class Intel32e(Intel): ] +class Intel32e_LA57(Intel32e): + """Class for handling 64-bit Intel architectures with 5-level paging + (LA57). + + LA57 extends the linear address space to 57 bits by adding a fifth + page map level (PML5) above the PML4. All derived values + (maximum_address, address_mask, canonicalization prefix and the + translation walk) are computed from _maxvirtaddr and _structure. + """ + + _maxvirtaddr = 57 + _structure = [ + ("page map layer 5", 9, False), + ("page map layer 4", 9, False), + ("page directory pointer", 9, True), + ("page directory", 9, True), + ("page table", 9, False), + ] + + class WindowsMixin(Intel): @staticmethod def _page_is_valid(entry: int) -> bool: @@ -563,6 +583,15 @@ def _translate(self, offset: int) -> Tuple[int, int, str]: return self._translate_swap(self, offset, self._bits_per_register // 2) +class WindowsIntel32e_LA57(WindowsMixin, Intel32e_LA57): + # Same transition-state PFN workaround as WindowsIntel32e. + # See https://github.com/volatilityfoundation/volatility3/pull/475 + _maxphyaddr = 45 + + 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: @@ -643,3 +672,12 @@ class LinuxIntel32e(LinuxMixin, Intel32e): # 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 + + +class LinuxIntel32e_LA57(LinuxMixin, Intel32e_LA57): + # 5-level paging requires kernel >= 4.14 (CONFIG_X86_5LEVEL), where + # __PHYSICAL_MASK_SHIFT is always 52 bits for x86-64 + # (see b83ce5ee91471d19c403ff91227204fb37c95fb2). LA57-capable hardware + # (Ice Lake and later) can also expose physical memory beyond 46 bits, + # so the 46-bit value used by LinuxIntel32e would truncate high PFNs. + _maxphyaddr = 52 diff --git a/volatility3/framework/plugins/windows/modules.py b/volatility3/framework/plugins/windows/modules.py index 18a7b59193..660890066b 100644 --- a/volatility3/framework/plugins/windows/modules.py +++ b/volatility3/framework/plugins/windows/modules.py @@ -147,7 +147,12 @@ def get_kernel_space_start(cls, context, module_name: str) -> int: context=context, symbol_table_name=module.symbol_table_name ): object_type = "unsigned long long" - default_start = 0xFFFF800000000000 + # Derive the kernel-space start from the layer so it is correct for + # both 4-level (0xFFFF800000000000) and 5-level/LA57 + # (0xFF00000000000000) paging. + layer = context.layers[module.layer_name] + kernel_half = 1 << (layer.maximum_address.bit_length() - 1) + default_start = layer.canonicalize(kernel_half) else: object_type = "unsigned long" default_start = 0x80000000 diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 836810a6de..e19c0b116a 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -2733,12 +2733,8 @@ def _intel_vmemmap_start(self) -> int: vmemmap_start = vmemmap_base_l4 else: # 5-Level paging -> VMEMMAP_START = __VMEMMAP_BASE_L5 - # FIXME: Once 5-level paging is supported, uncomment the following lines and remove the exception - # vmemmap_base_l5 = 0xFFD4000000000000 - # vmemmap_start = vmemmap_base_l5 - raise exceptions.VolatilityException( - "5-level paging is not yet supported" - ) + vmemmap_base_l5 = 0xFFD4000000000000 + vmemmap_start = vmemmap_base_l5 elif vmlinux.has_symbol("mem_map"): # FLATMEM physical memory model, typically 32bit