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

Linux - Boottime support #1317

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
18f7f03
linux: fix datetime import and remove unused ones
gcmoreira Oct 18, 2024
f822468
linux: Implement boot time support in the Volatility 3 core framework
gcmoreira Oct 18, 2024
69512dc
Linux: pslist: Add creation time column and timeline support to the l…
gcmoreira Oct 18, 2024
d25df23
Linux: pslist: Add the boottime plugin
gcmoreira Oct 18, 2024
1651ecb
linux: boottime api: Fix explicit returns mixed with implicit (fall t…
gcmoreira Oct 18, 2024
4895af4
Linux: Boottime timeliner: Rollback timeliner event type changes and …
gcmoreira Oct 29, 2024
c4274c9
Linux: Fix exception message in TimespecVol3::__sub__()
gcmoreira Oct 29, 2024
57ffd5b
Linux: Boottime API: Refactor TimespecVol3::negate() to return a new …
gcmoreira Oct 29, 2024
bb6dc9a
Merge branch 'develop' into linux_boottime_support
gcmoreira Oct 29, 2024
bee2a39
Linux: Minor: Add comment/header on each set of constants
gcmoreira Oct 29, 2024
42d918c
Linux: Boottime API: Refactor Timespec Methods.
gcmoreira Oct 30, 2024
7abe92c
Linux: Boottime API: Minor. Move negate() up
gcmoreira Oct 30, 2024
e197fba
Linux: Boottime API: Refactor __sub__ to operate through __add__() an…
gcmoreira Oct 30, 2024
a05397e
Linux: Boottime API: Minor. Fix docstring typo
gcmoreira Oct 30, 2024
2efb4e7
Merge branch 'develop' into linux_boottime_support
gcmoreira Nov 5, 2024
c0fa2cf
Linux: Boottime API: User linux_constanst import
gcmoreira Nov 5, 2024
f05a169
Merge branch 'develop' into linux_boottime_support
gcmoreira Nov 5, 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
3 changes: 3 additions & 0 deletions volatility3/framework/constants/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,6 @@ class ELF_CLASS(IntEnum):
ELFCLASSNONE = 0
ELFCLASS32 = 1
ELFCLASS64 = 2


NSEC_PER_SEC = 1e9
98 changes: 98 additions & 0 deletions volatility3/framework/plugins/linux/boottime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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 datetime
from typing import List, Tuple, Iterable


from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.plugins import timeliner
from volatility3.plugins.linux import pslist


class Boottime(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Shows the time the system was started"""

_required_framework_version = (2, 11, 0)

_version = (1, 0, 0)

@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
return [
requirements.ModuleRequirement(
name="kernel",
description="Linux kernel",
architectures=["Intel32", "Intel64"],
),
requirements.PluginRequirement(
name="pslist", plugin=pslist.PsList, version=(2, 3, 0)
),
]

@classmethod
def get_time_namespaces_bootime(
cls,
context: interfaces.context.ContextInterface,
vmlinux_module_name: str,
) -> Iterable[Tuple[int, int, int, str, datetime.datetime]]:
"""Enumerates tasks' boot times based on their time namespaces.

Args:
context: The context to retrieve required elements (layers, symbol tables) from
vmlinux_module_name: The name of the kernel module on which to operate
pids: Pid list
unique: Filter unique time namespaces

Yields:
A tuple with the fields to show in the plugin output.
"""
time_namespace_ids = set()
for task in pslist.PsList.list_tasks(context, vmlinux_module_name):
time_namespace_id = task.get_time_namespace_id()
# If it cannot get the time namespace i.e. kernels < 5.6, this still works
# using None to just get the first tasks
if time_namespace_id in time_namespace_ids:
continue
time_namespace_ids.add(time_namespace_id)
boottime = task.get_boottime(root_time_namespace=False)

fields = (
time_namespace_id,
boottime,
)
yield fields

def _generator(self):
for (
time_namespace_id,
boottime,
) in self.get_time_namespaces_bootime(
self.context,
self.config["kernel"],
):
fields = [
time_namespace_id or renderers.NotAvailableValue(),
boottime,
]
yield 0, fields

def generate_timeline(self):
for (
time_namespace_id,
boottime,
) in self.get_time_namespaces_bootime(
self.context,
self.config["kernel"],
):
description = f"System boot time for time namespace {time_namespace_id}"

yield description, timeliner.TimeLinerType.BOOTTIME, boottime

def run(self):
columns = [
("TIME NS", int),
("Boot Time", datetime.datetime),
]
return renderers.TreeGrid(columns, self._generator())
31 changes: 26 additions & 5 deletions volatility3/framework/plugins/linux/pslist.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file is Copyright 2021 Volatility Foundation and licensed under the Volatility Software License 1.0
# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
#
import datetime
from typing import Any, Callable, Iterable, List, Tuple

from volatility3.framework import interfaces, renderers
Expand All @@ -9,15 +10,16 @@
from volatility3.framework.renderers import format_hints
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux.extensions import elf
from volatility3.plugins import timeliner
from volatility3.plugins.linux import elfs


class PsList(interfaces.plugins.PluginInterface):
class PsList(interfaces.plugins.PluginInterface, timeliner.TimeLinerInterface):
"""Lists the processes present in a particular linux memory image."""

_required_framework_version = (2, 0, 0)

_version = (2, 2, 1)
_version = (2, 3, 0)

@classmethod
def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
Expand Down Expand Up @@ -81,7 +83,7 @@ def filter_func(x):
@classmethod
def get_task_fields(
cls, task: interfaces.objects.ObjectInterface, decorate_comm: bool = False
) -> Tuple[int, int, int, str]:
) -> Tuple[int, int, int, int, str, datetime.datetime]:
"""Extract the fields needed for the final output

Args:
Expand All @@ -96,13 +98,14 @@ def get_task_fields(
tid = task.pid
ppid = task.parent.tgid if task.parent else 0
name = utility.array_to_string(task.comm)
start_time = task.get_create_time()
if decorate_comm:
if task.is_kernel_thread:
name = f"[{name}]"
elif task.is_user_thread:
name = f"{{{name}}}"

task_fields = (task.vol.offset, pid, tid, ppid, name)
task_fields = (task.vol.offset, pid, tid, ppid, name, start_time)
return task_fields

def _get_file_output(self, task: interfaces.objects.ObjectInterface) -> str:
Expand Down Expand Up @@ -177,14 +180,17 @@ def _generator(
else:
file_output = "Disabled"

offset, pid, tid, ppid, name = self.get_task_fields(task, decorate_comm)
offset, pid, tid, ppid, name, creation_time = self.get_task_fields(
task, decorate_comm
)

yield 0, (
format_hints.Hex(offset),
pid,
tid,
ppid,
name,
creation_time or renderers.NotAvailableValue(),
file_output,
)

Expand Down Expand Up @@ -233,8 +239,23 @@ def run(self):
("TID", int),
("PPID", int),
("COMM", str),
("CREATION TIME", datetime.datetime),
("File output", str),
]
return renderers.TreeGrid(
columns, self._generator(filter_func, include_threads, decorate_comm, dump)
)

def generate_timeline(self):
pids = self.config.get("pid")
filter_func = self.create_pid_filter(pids)
for task in self.list_tasks(
self.context, self.config["kernel"], filter_func, include_threads=True
):
offset, user_pid, user_tid, _user_ppid, name, creation_time = (
self.get_task_fields(task)
)

description = f"Process {user_pid}/{user_tid} {name} ({offset})"

yield (description, timeliner.TimeLinerType.CREATED, creation_time)
13 changes: 11 additions & 2 deletions volatility3/framework/plugins/timeliner.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class TimeLinerType(enum.IntEnum):
MODIFIED = 2
ACCESSED = 3
CHANGED = 4
BOOTTIME = 5
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved


class TimeLinerInterface(metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -171,18 +172,22 @@ def _generator(
TimeLinerType.CHANGED,
renderers.NotApplicableValue(),
),
times.get(
TimeLinerType.BOOTTIME,
renderers.NotApplicableValue(),
),
],
)
)

# Write each entry because the body file doesn't need to be sorted
if fp:
times = self.timeline[(plugin_name, item)]
# Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime
# Body format is: MD5|name|inode|mode_as_string|UID|GID|size|atime|mtime|ctime|crtime|boottime
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved

if self._any_time_present(times):
fp.write(
"|{} - {}|0|0|0|0|0|{}|{}|{}|{}\n".format(
"|{} - {}|0|0|0|0|0|{}|{}|{}|{}|{}\n".format(
plugin_name,
self._sanitize_body_format(item),
self._text_format(
Expand All @@ -197,6 +202,9 @@ def _generator(
self._text_format(
times.get(TimeLinerType.CREATED, "0")
),
self._text_format(
times.get(TimeLinerType.BOOTTIME, "0")
),
)
)
except Exception as e:
Expand Down Expand Up @@ -320,6 +328,7 @@ def run(self):
("Modified Date", datetime.datetime),
("Accessed Date", datetime.datetime),
("Changed Date", datetime.datetime),
("Boot Date", datetime.datetime),
],
generator=self._generator(plugins_to_run),
)
Expand Down
98 changes: 98 additions & 0 deletions volatility3/framework/symbols/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
#
import math
import contextlib
import datetime
import dataclasses
from abc import ABC, abstractmethod
from typing import Iterator, List, Tuple, Optional, Union

from volatility3 import framework
from volatility3.framework import constants, exceptions, interfaces, objects
from volatility3.framework.renderers import conversion
from volatility3.framework.constants.linux import NSEC_PER_SEC
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux import extensions
Expand Down Expand Up @@ -830,3 +834,97 @@ def get_cached_pages(self) -> interfaces.objects.ObjectInterface:
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
if page:
yield page


@dataclasses.dataclass
class TimespecVol3(object):
ikelos marked this conversation as resolved.
Show resolved Hide resolved
"""Internal helper class to handle all required timespec operations, convertions and
adjustments.

NOTE: This is intended for exclusive use with get_boottime() and its related functions.
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved
"""

tv_sec: int = 0
tv_nsec: int = 0

@classmethod
def new_from_timespec(cls, timespec) -> "TimespecVol3":
"""Creates a new instance from a TimespecVol3 or timespec64 object"""
if not isinstance(timespec, (TimespecVol3, extensions.timespec64)):
raise TypeError("It requires either a TimespecVol3 or timespec64 type")

tv_sec = int(timespec.tv_sec)
tv_nsec = int(timespec.tv_nsec)
return cls(tv_sec=tv_sec, tv_nsec=tv_nsec)

@classmethod
def new_from_nsec(cls, nsec) -> "TimespecVol3":
"""Creates a new instance from an integer in nanoseconds"""

# Based on ns_to_timespec64()
if nsec > 0:
tv_sec = nsec // NSEC_PER_SEC
tv_nsec = nsec % NSEC_PER_SEC
elif nsec < 0:
tv_sec = -((-nsec - 1) // NSEC_PER_SEC) - 1
rem = (-nsec - 1) % NSEC_PER_SEC
tv_nsec = NSEC_PER_SEC - rem - 1
else:
tv_sec = tv_nsec = 0

return cls(tv_sec=tv_sec, tv_nsec=tv_nsec)

def to_datetime(self) -> datetime.datetime:
"""Converts this TimespecVol3 to a UTC aware datetime"""
return conversion.unixtime_to_datetime(
self.tv_sec + self.tv_nsec / NSEC_PER_SEC
)

def to_timedelta(self) -> datetime.timedelta:
"""Converts this TimespecVol3 to timedelta"""
return datetime.timedelta(seconds=self.tv_sec + self.tv_nsec / NSEC_PER_SEC)

def __add__(self, timespec) -> "TimespecVol3":
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved
"""Returns a new TimespecVol3 object that sums the current values with those
in the timespec argument"""
if not isinstance(timespec, (TimespecVol3, extensions.timespec64)):
raise TypeError("Cannot add a TimespecVol3 to this object")

result = TimespecVol3(
tv_sec=self.tv_sec + timespec.tv_sec,
tv_nsec=self.tv_nsec + timespec.tv_nsec,
)

result.normalize()

return result

def __sub__(self, timespec) -> "TimespecVol3":
"""Returns a new TimespecVol3 object that subtracts the values in the timespec
argument from the current object's values"""
if not isinstance(timespec, (TimespecVol3, extensions.timespec64)):
raise TypeError("Cannot add a TimespecVol3 to this object")
gcmoreira marked this conversation as resolved.
Show resolved Hide resolved

result = TimespecVol3(
ikelos marked this conversation as resolved.
Show resolved Hide resolved
tv_sec=self.tv_sec - timespec.tv_sec,
tv_nsec=self.tv_nsec - timespec.tv_nsec,
)
result.normalize()

return result

def normalize(self):
"""Normalize any overflow in tv_sec and tv_nsec after previous addition or subtractions"""
# Based on kernel's set_normalized_timespec64()
while self.tv_nsec >= NSEC_PER_SEC:
self.tv_nsec -= NSEC_PER_SEC
self.tv_sec += 1

while self.tv_nsec < 0:
self.tv_nsec += NSEC_PER_SEC
self.tv_sec -= 1

def negate(self):
"""Negates the sign of both tv_sec and tv_nsec"""
self.tv_sec = -self.tv_sec
self.tv_nsec = -self.tv_nsec
Loading
Loading