Skip to content

Commit 51726cd

Browse files
authored
Merge pull request #1553 from gcmoreira/parity_release_harden_page_cache
Linux - Parity release harden Page Cache
2 parents 0f09ecd + ec7a101 commit 51726cd

File tree

5 files changed

+199
-81
lines changed

5 files changed

+199
-81
lines changed

test/test_volatility.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,24 @@ def test_linux_page_cache_inodepages(image, volatility, python):
708708

709709
inode_address = hex(0x88001AB5C270)
710710
inode_dump_filename = f"inode_{inode_address}.dmp"
711+
712+
rc, out, _err = runvol_plugin(
713+
"linux.pagecache.InodePages",
714+
image,
715+
volatility,
716+
python,
717+
pluginargs=["--inode", inode_address],
718+
)
719+
720+
assert rc == 0
721+
assert out.count(b"\n") > 4
722+
723+
# PageVAddr PagePAddr MappingAddr .. DumpSafe
724+
assert re.search(
725+
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
726+
out,
727+
)
728+
711729
try:
712730
rc, out, _err = runvol_plugin(
713731
"linux.pagecache.InodePages",
@@ -718,13 +736,8 @@ def test_linux_page_cache_inodepages(image, volatility, python):
718736
)
719737

720738
assert rc == 0
721-
assert out.count(b"\n") > 4
739+
assert out.count(b"\n") >= 4
722740

723-
# PageVAddr PagePAddr MappingAddr .. DumpSafe
724-
assert re.search(
725-
rb"0xea000054c5f8\s0x18389000\s0x88001ab5c3b0.*?True",
726-
out,
727-
)
728741
assert os.path.exists(inode_dump_filename)
729742
with open(inode_dump_filename, "rb") as fp:
730743
inode_contents = fp.read()

volatility3/framework/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ def __str__(self):
130130

131131
class RenderException(VolatilityException):
132132
"""Thrown if there is an error during rendering"""
133+
134+
135+
class LinuxPageCacheException(VolatilityException):
136+
"""Thrown if there is an error during Linux Page Cache processing"""

volatility3/framework/plugins/linux/pagecache.py

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import logging
77
import datetime
88
from dataclasses import dataclass, astuple
9-
from typing import List, Set, Type, Iterable
9+
from typing import List, Set, Type, Iterable, Tuple
1010

11-
from volatility3.framework import renderers, interfaces
11+
from volatility3.framework import renderers, interfaces, exceptions
1212
from volatility3.framework.renderers import format_hints
1313
from volatility3.framework.interfaces import plugins
1414
from volatility3.framework.configuration import requirements
@@ -104,7 +104,7 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
104104

105105
_required_framework_version = (2, 0, 0)
106106

107-
_version = (1, 0, 1)
107+
_version = (1, 0, 2)
108108

109109
@classmethod
110110
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -147,7 +147,13 @@ def _follow_symlink(
147147
Otherwise, it returns the same symlink_path
148148
"""
149149
# i_link (fast symlinks) were introduced in 4.2
150-
if inode and inode.is_link and inode.has_member("i_link") and inode.i_link:
150+
if (
151+
inode
152+
and inode.is_link
153+
and inode.has_member("i_link")
154+
and inode.i_link
155+
and inode.i_link.is_readable()
156+
):
151157
i_link_str = inode.i_link.dereference().cast(
152158
"string", max_length=255, encoding="utf-8", errors="replace"
153159
)
@@ -253,6 +259,10 @@ def get_inodes(
253259
if not root_inode.is_valid():
254260
continue
255261

262+
if not (root_inode.i_mapping and root_inode.i_mapping.is_readable()):
263+
# Retrieving data from the page cache requires a valid address space
264+
continue
265+
256266
# Inode already processed?
257267
if root_inode_ptr in seen_inodes:
258268
continue
@@ -284,6 +294,10 @@ def get_inodes(
284294
if not file_inode.is_valid():
285295
continue
286296

297+
if not (file_inode.i_mapping and file_inode.i_mapping.is_readable()):
298+
# Retrieving data from the page cache requires a valid address space
299+
continue
300+
287301
# Inode already processed?
288302
if file_inode_ptr in seen_inodes:
289303
continue
@@ -316,10 +330,12 @@ def _generator(self):
316330
if self.config["find"]:
317331
if inode_in.path == self.config["find"]:
318332
inode_out = inode_in.to_user(vmlinux_layer)
333+
319334
yield (0, astuple(inode_out))
320335
break # Only the first match
321336
else:
322337
inode_out = inode_in.to_user(vmlinux_layer)
338+
323339
yield (0, astuple(inode_out))
324340

325341
def generate_timeline(self):
@@ -389,7 +405,7 @@ class InodePages(plugins.PluginInterface):
389405

390406
_required_framework_version = (2, 0, 0)
391407

392-
_version = (2, 0, 0)
408+
_version = (2, 0, 1)
393409

394410
@classmethod
395411
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
@@ -443,28 +459,80 @@ def write_inode_content_to_file(
443459
# created, saving both disk space and I/O time.
444460
# Additionally, using the page index will guarantee that each page is written at the
445461
# appropriate file position.
462+
inode_size = inode.i_size
446463
try:
447-
with open_method(filename) as f:
448-
inode_size = inode.i_size
449-
f.truncate(inode_size)
450-
464+
file_initialized = False
465+
with open_method(filename) as file_obj:
451466
for page_idx, page_content in inode.get_contents():
452467
current_fp = page_idx * vmlinux_layer.page_size
453468
max_length = inode_size - current_fp
454-
page_bytes = page_content[:max_length]
455-
if current_fp + len(page_bytes) > inode_size:
469+
page_bytes_len = min(max_length, len(page_content))
470+
if (
471+
current_fp >= inode_size
472+
or current_fp + page_bytes_len > inode_size
473+
):
456474
vollog.error(
457475
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
458476
inode.vol.offset,
459477
inode_size,
460478
page_idx,
461479
)
462-
f.seek(current_fp)
463-
f.write(page_bytes)
464-
480+
continue
481+
page_bytes = page_content[:page_bytes_len]
482+
483+
if not file_initialized:
484+
# Lazy initialization to avoid truncating the file until we are
485+
# certain there is something to write
486+
file_obj.truncate(inode_size)
487+
file_initialized = True
488+
489+
file_obj.seek(current_fp)
490+
file_obj.write(page_bytes)
491+
except exceptions.LinuxPageCacheException:
492+
vollog.error(
493+
f"Error dumping cached pages for inode at {inode.vol.offset:#x}"
494+
)
465495
except OSError as e:
466496
vollog.error("Unable to write to file (%s): %s", filename, e)
467497

498+
def _generate_inode_fields(
499+
self,
500+
inode: interfaces.objects.ObjectInterface,
501+
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
502+
) -> Iterable[Tuple[int, int, int, int, bool, str]]:
503+
inode_size = inode.i_size
504+
try:
505+
for page_obj in inode.get_pages():
506+
if page_obj.mapping != inode.i_mapping:
507+
vollog.warning(
508+
f"Cached page at {page_obj.vol.offset:#x} has a mismatched address space with the inode. Skipping page"
509+
)
510+
continue
511+
page_vaddr = page_obj.vol.offset
512+
page_paddr = page_obj.to_paddr()
513+
page_mapping_addr = page_obj.mapping
514+
page_index = page_obj.index
515+
page_file_offset = page_index * vmlinux_layer.page_size
516+
dump_safe = (
517+
page_file_offset < inode_size
518+
and page_mapping_addr
519+
and page_mapping_addr.is_readable()
520+
)
521+
page_flags_list = page_obj.get_flags_list()
522+
page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list])
523+
fields = (
524+
page_vaddr,
525+
page_paddr,
526+
page_mapping_addr,
527+
page_index,
528+
dump_safe,
529+
page_flags,
530+
)
531+
532+
yield 0, fields
533+
except exceptions.LinuxPageCacheException:
534+
vollog.warning(f"Page cache for inode at {inode.vol.offset:#x} is corrupt")
535+
468536
def _generator(self):
469537
vmlinux_module_name = self.config["kernel"]
470538
vmlinux = self.context.modules[vmlinux_module_name]
@@ -486,7 +554,6 @@ def _generator(self):
486554
else:
487555
vollog.error("Unable to find inode with path %s", self.config["find"])
488556
return None
489-
490557
elif self.config["inode"]:
491558
inode = vmlinux.object("inode", self.config["inode"], absolute=True)
492559
else:
@@ -501,27 +568,6 @@ def _generator(self):
501568
vollog.error("The inode is not a regular file")
502569
return None
503570

504-
inode_size = inode.i_size
505-
for page_obj in inode.get_pages():
506-
page_vaddr = page_obj.vol.offset
507-
page_paddr = page_obj.to_paddr()
508-
page_mapping_addr = page_obj.mapping
509-
page_index = int(page_obj.index)
510-
page_file_offset = page_index * vmlinux_layer.page_size
511-
dump_safe = page_file_offset < inode_size
512-
page_flags_list = page_obj.get_flags_list()
513-
page_flags = ",".join([x.replace("PG_", "") for x in page_flags_list])
514-
fields = (
515-
page_vaddr,
516-
page_paddr,
517-
page_mapping_addr,
518-
page_index,
519-
dump_safe,
520-
page_flags,
521-
)
522-
523-
yield 0, fields
524-
525571
if self.config["dump"]:
526572
open_method = self.open
527573
inode_address = inode.vol.offset
@@ -530,6 +576,8 @@ def _generator(self):
530576
self.write_inode_content_to_file(
531577
inode, filename, open_method, vmlinux_layer
532578
)
579+
else:
580+
yield from self._generate_inode_fields(inode, vmlinux_layer)
533581

534582
def run(self):
535583
headers = [

0 commit comments

Comments
 (0)