Skip to content

Commit 00a69c9

Browse files
authored
Merge pull request #1561 from Abyss-W4tcher/pre_linux_pagecache_recoverfs_support
Pre linux.pagecache.recoverfs support
2 parents 89c6d7a + d42ffc0 commit 00a69c9

File tree

1 file changed

+65
-32
lines changed

1 file changed

+65
-32
lines changed

volatility3/framework/plugins/linux/pagecache.py

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +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, IO
1010

11+
from volatility3.framework.constants import architectures
1112
from volatility3.framework import renderers, interfaces
1213
from volatility3.framework.renderers import format_hints
1314
from volatility3.framework.interfaces import plugins
@@ -37,6 +38,11 @@ class InodeUser:
3738
modification_time: str
3839
change_time: str
3940
path: str
41+
inode_size: int
42+
43+
@staticmethod
44+
def format_symlink(symlink_source: str, symlink_dest: str) -> str:
45+
return f"{symlink_source} -> {symlink_dest}"
4046

4147

4248
@dataclass
@@ -80,6 +86,7 @@ def to_user(
8086
access_time_dt = self.inode.get_access_time()
8187
modification_time_dt = self.inode.get_modification_time()
8288
change_time_dt = self.inode.get_change_time()
89+
inode_size = int(self.inode.i_size)
8390

8491
inode_user = InodeUser(
8592
superblock_addr=superblock_addr,
@@ -95,6 +102,7 @@ def to_user(
95102
modification_time=modification_time_dt,
96103
change_time=change_time_dt,
97104
path=self.path,
105+
inode_size=inode_size,
98106
)
99107
return inode_user
100108

@@ -104,15 +112,15 @@ class Files(plugins.PluginInterface, timeliner.TimeLinerInterface):
104112

105113
_required_framework_version = (2, 0, 0)
106114

107-
_version = (1, 0, 1)
115+
_version = (1, 1, 0)
108116

109117
@classmethod
110118
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
111119
return [
112120
requirements.ModuleRequirement(
113121
name="kernel",
114122
description="Linux kernel",
115-
architectures=["Intel32", "Intel64"],
123+
architectures=architectures.LINUX_ARCHS,
116124
),
117125
requirements.PluginRequirement(
118126
name="mountinfo", plugin=mountinfo.MountInfo, version=(1, 2, 0)
@@ -148,10 +156,10 @@ def _follow_symlink(
148156
"""
149157
# i_link (fast symlinks) were introduced in 4.2
150158
if inode and inode.is_link and inode.has_member("i_link") and inode.i_link:
151-
i_link_str = inode.i_link.dereference().cast(
159+
symlink_dest = inode.i_link.dereference().cast(
152160
"string", max_length=255, encoding="utf-8", errors="replace"
153161
)
154-
symlink_path = f"{symlink_path} -> {i_link_str}"
162+
symlink_path = InodeUser.format_symlink(symlink_path, symlink_dest)
155163

156164
return symlink_path
157165

@@ -212,12 +220,14 @@ def get_inodes(
212220
cls,
213221
context: interfaces.context.ContextInterface,
214222
vmlinux_module_name: str,
223+
follow_symlinks: bool = True,
215224
) -> Iterable[InodeInternal]:
216225
"""Retrieves the inodes from the superblocks
217226
218227
Args:
219228
context: The context that the plugin will operate within
220229
vmlinux_module_name: The name of the kernel module on which to operate
230+
follow_symlinks: Whether to follow symlinks or not
221231
222232
Yields:
223233
An InodeInternal object
@@ -289,7 +299,9 @@ def get_inodes(
289299
continue
290300
seen_inodes.add(file_inode_ptr)
291301

292-
file_path = cls._follow_symlink(file_inode_ptr, file_path)
302+
if follow_symlinks:
303+
file_path = cls._follow_symlink(file_inode_ptr, file_path)
304+
293305
inode_in = InodeInternal(
294306
superblock=superblock,
295307
mountpoint=mountpoint,
@@ -377,6 +389,7 @@ def run(self):
377389
("ModificationTime", datetime.datetime),
378390
("ChangeTime", datetime.datetime),
379391
("FilePath", str),
392+
("InodeSize", int),
380393
]
381394

382395
return renderers.TreeGrid(
@@ -389,15 +402,15 @@ class InodePages(plugins.PluginInterface):
389402

390403
_required_framework_version = (2, 0, 0)
391404

392-
_version = (2, 0, 0)
405+
_version = (3, 0, 0)
393406

394407
@classmethod
395408
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
396409
return [
397410
requirements.ModuleRequirement(
398411
name="kernel",
399412
description="Linux kernel",
400-
architectures=["Intel32", "Intel64"],
413+
architectures=architectures.LINUX_ARCHS,
401414
),
402415
requirements.PluginRequirement(
403416
name="files", plugin=Files, version=(1, 0, 0)
@@ -422,49 +435,69 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]
422435

423436
@staticmethod
424437
def write_inode_content_to_file(
438+
context: interfaces.context.ContextInterface,
439+
layer_name: str,
425440
inode: interfaces.objects.ObjectInterface,
426441
filename: str,
427442
open_method: Type[interfaces.plugins.FileHandlerInterface],
428-
vmlinux_layer: interfaces.layers.TranslationLayerInterface,
429443
) -> None:
430444
"""Extracts the inode's contents from the page cache and saves them to a file
431445
432446
Args:
447+
context: The context on which to operate
448+
layer_name: The name of the layer on which to operate
433449
inode: The inode to dump
434450
filename: Filename for writing the inode content
435451
open_method: class for constructing output files
436-
vmlinux_layer: The kernel layer to obtain the page size
437452
"""
438453
if not inode.is_reg:
439454
vollog.error("The inode is not a regular file")
440455
return None
441456

442-
# By using truncate/seek, provided the filesystem supports it, a sparse file will be
443-
# created, saving both disk space and I/O time.
444-
# Additionally, using the page index will guarantee that each page is written at the
445-
# appropriate file position.
446457
try:
447458
with open_method(filename) as f:
448-
inode_size = inode.i_size
449-
f.truncate(inode_size)
450-
451-
for page_idx, page_content in inode.get_contents():
452-
current_fp = page_idx * vmlinux_layer.page_size
453-
max_length = inode_size - current_fp
454-
page_bytes = page_content[:max_length]
455-
if current_fp + len(page_bytes) > inode_size:
456-
vollog.error(
457-
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
458-
inode.vol.offset,
459-
inode_size,
460-
page_idx,
461-
)
462-
f.seek(current_fp)
463-
f.write(page_bytes)
464-
459+
InodePages.write_inode_content_to_stream(context, layer_name, inode, f)
465460
except OSError as e:
466461
vollog.error("Unable to write to file (%s): %s", filename, e)
467462

463+
@staticmethod
464+
def write_inode_content_to_stream(
465+
context: interfaces.context.ContextInterface,
466+
layer_name: str,
467+
inode: interfaces.objects.ObjectInterface,
468+
stream: IO,
469+
) -> None:
470+
"""Extracts the inode's contents from the page cache and saves them to a stream
471+
472+
Args:
473+
context: The context on which to operate
474+
layer_name: The name of the layer on which to operate
475+
inode: The inode to dump
476+
stream: An IO stream to write to, typically FileHandlerInterface or BytesIO
477+
"""
478+
layer = context.layers[layer_name]
479+
# By using truncate/seek, provided the filesystem supports it, and the
480+
# stream is a File interface, a sparse file will be
481+
# created, saving both disk space and I/O time.
482+
# Additionally, using the page index will guarantee that each page is written at the
483+
# appropriate file position.
484+
inode_size = inode.i_size
485+
stream.truncate(inode_size)
486+
487+
for page_idx, page_content in inode.get_contents():
488+
current_fp = page_idx * layer.page_size
489+
max_length = inode_size - current_fp
490+
page_bytes = page_content[:max_length]
491+
if current_fp + len(page_bytes) > inode_size:
492+
vollog.error(
493+
"Page out of file bounds: inode 0x%x, inode size %d, page index %d",
494+
inode.vol.offset,
495+
inode_size,
496+
page_idx,
497+
)
498+
stream.seek(current_fp)
499+
stream.write(page_bytes)
500+
468501
def _generator(self):
469502
vmlinux_module_name = self.config["kernel"]
470503
vmlinux = self.context.modules[vmlinux_module_name]
@@ -528,7 +561,7 @@ def _generator(self):
528561
filename = open_method.sanitize_filename(f"inode_0x{inode_address:x}.dmp")
529562
vollog.info("[*] Writing inode at 0x%x to '%s'", inode_address, filename)
530563
self.write_inode_content_to_file(
531-
inode, filename, open_method, vmlinux_layer
564+
self.context, vmlinux_layer.name, inode, filename, open_method
532565
)
533566

534567
def run(self):

0 commit comments

Comments
 (0)