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

New linux plugin: modxview #1330

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
121 changes: 121 additions & 0 deletions volatility3/framework/symbols/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from volatility3.framework.objects import utility
from volatility3.framework.symbols import intermed
from volatility3.framework.symbols.linux import extensions
from volatility3.framework.constants import linux as linux_constants


class LinuxKernelIntermedSymbols(intermed.IntermediateSymbolTable):
Expand Down Expand Up @@ -830,3 +831,123 @@ def get_cached_pages(self) -> Iterator[interfaces.objects.ObjectInterface]:
page = self.vmlinux.object("page", offset=page_addr, absolute=True)
if page:
yield page


class Tainting:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might want to go in a tainting.py module, so it would become linux.tainting.Tainting (and so that the init doesn't get too full).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also would need its own version number, and a required_framework_version for anything it made use of (and the __init__ would need to check those...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, see my branch for an example.

"""Tainted kernel and modules parsing capabilities.

Relevant kernel functions:
- modules: module_flags_taint
- kernel: print_tainted
"""

def __init__(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also expected these just to be function containers, rather than carrying state themselves. We can have them carry state I guess, I'm just not sure the extra complexity/separation of parameters is worth it?

self,
context: interfaces.context.ContextInterface,
kernel_module_name: str,
):
self.kernel = context.modules[kernel_module_name]

@property
def kernel_taint_flags_list(
self,
) -> Optional[List[interfaces.objects.ObjectInterface]]:
if self.kernel.has_symbol("taint_flags"):
return list(self.kernel.object_from_symbol("taint_flags"))
return None

def _module_flags_taint_pre_4_10_rc1(
self, taints: int, is_module: bool = False
) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on statically defined taints mappings in the framework.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

Returns:
The raw taints string.
"""
taints_string = ""
for char, taint_flag in linux_constants.TAINT_FLAGS.items():
if is_module and is_module != taint_flag.module:
continue

if taints & taint_flag.shift:
taints_string += char

return taints_string

def _module_flags_taint_post_4_10_rc1(
self, taints: int, is_module: bool = False
) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on kernel symbol embedded taints definitions.

struct taint_flag {
char c_true; /* character printed when tainted */
char c_false; /* character printed when not tainted */
bool module; /* also show as a per-module taint flag */
};

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

Returns:
The raw taints string.
"""
taints_string = ""
for i, taint_flag in enumerate(self.kernel_taint_flags_list):
if is_module and is_module != taint_flag.module:
continue
c_true = chr(taint_flag.c_true)
c_false = chr(taint_flag.c_false)
if taints & (1 << i):
taints_string += c_true
elif c_false != " ":
taints_string += c_false

return taints_string

def get_taints_as_plain_string(self, taints: int, is_module: bool = False) -> str:
"""Convert the taints value to a 1-1 character mapping.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module
s
Returns:
The raw taints string.

Documentation:
- module_flags_taint kernel function
"""

if self.kernel_taint_flags_list:
return self._module_flags_taint_post_4_10_rc1(taints, is_module)
return self._module_flags_taint_pre_4_10_rc1(taints, is_module)

def get_taints_parsed(self, taints: int, is_module: bool = False) -> List[str]:
"""Convert the taints string to a 1-1 descriptor mapping.

Args:
taints: The taints value, represented by an integer
is_module: Indicates if the taints value is associated with a built-in/LKM module

Returns:
A comprehensive (user-friendly) taint descriptor list.

Documentation:
- module_flags_taint kernel function
"""
comprehensive_taints = []
for character in self.get_taints_as_plain_string(taints, is_module):
taint_flag = linux_constants.TAINT_FLAGS.get(character)
if not taint_flag:
comprehensive_taints.append(f"<UNKNOWN_TAINT_CHAR_{character}>")
elif taint_flag.when_present:
comprehensive_taints.append(taint_flag.desc)

return comprehensive_taints
74 changes: 10 additions & 64 deletions volatility3/framework/symbols/linux/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,76 +279,29 @@ def get_symbol_by_address(self, wanted_sym_address):

return None

def _module_flags_taints_pre_4_10_rc1(self) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on statically defined taints mappings in the framework.

Returns:
The raw taints string.
"""
taints_string = ""
for char, taint_flag in linux_constants.TAINT_FLAGS.items():
if taint_flag.module and self.taints & taint_flag.shift:
taints_string += char

return taints_string

def _module_flags_taints_post_4_10_rc1(self) -> str:
"""Convert the module's taints value to a 1-1 character mapping.
Relies on kernel symbol embedded taints definitions.

struct taint_flag {
char c_true; /* character printed when tainted */
char c_false; /* character printed when not tainted */
bool module; /* also show as a per-module taint flag */
};

Returns:
The raw taints string.
"""
taints_string = ""
for i, taint_flag in enumerate(self.taint_flags_list):
c_true = chr(taint_flag.c_true)
c_false = chr(taint_flag.c_false)
if taint_flag.module and (self.taints & (1 << i)):
taints_string += c_true
elif taint_flag.module and c_false != " ":
taints_string += c_false

return taints_string

def get_taints_as_plain_string(self) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are externally visible additions to the API, which means a MINOR version number somewhere, needs to go up. I suspect it may be the framework itself because I don't think anything further down is versioned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, we want to include this API in a more generic place, but keep these convenient self-contained functions. So right now I'm not sure of how many bumps are needed.

"""Convert the module's taints value to a 1-1 character mapping.
Convenient wrapper around framework's Tainting capabilities.

Returns:
The raw taints string.

Documentation:
- module_flags_taint kernel function
"""

if self.taint_flags_list:
return self._module_flags_taints_post_4_10_rc1()
return self._module_flags_taints_pre_4_10_rc1()
return linux.Tainting(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite what I had in mind, because it requires LinuxUtilities to import each of the supported modules, and doesn't allow for plugins to determine whether their requested function will exist or not. I'll have a go at mocking something up tomorrow that's more what I had in mind. I think the separate class stuff is all good, we just need to tweak how we find the function we want to run...

Copy link
Contributor Author

@Abyss-W4tcher Abyss-W4tcher Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so we will have to tweak the module class, to include versioned requirements of the new Tainting object ?

class module(generic.GenericIntelProcess):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errr, there isn't really a way of versioning module, so it reverts to the version of the framework. If it's purely an additive change (which it looks like it is) then we can just bump the MINOR version (and nothing else needs to change), but if we ever want to change it in the future, then the MAJOR version of the whole framework needs to change. That's why I'm so protective of things like this (and splitting them out into distinct versioned modules is good). I think I thought these were all applying to LinuxUtilities. I hadn't realized they were all being tacked onto module. I haven't thought about how we version that separately yet. The problem with every thing you version is that it then needs checking before use (which is painful in itself). The requirements let us do that slightly more gracefully and finer grained, but we need to be really careful about what we add to major components of the framework and overriding the linux module class is one (at least at the moment)... 5:S

self._context,
linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name,
).get_taints_as_plain_string(self.taints, True)

def get_taints_parsed(self) -> List[str]:
"""Convert the module's taints string to a 1-1 descriptor mapping.
Convenient wrapper around framework's Tainting capabilities.

Returns:
A comprehensive (user-friendly) taint descriptor list.

Documentation:
- module_flags_taint kernel function
"""
comprehensive_taints = []
for character in self.get_taints_as_plain_string():
taint_flag = linux_constants.TAINT_FLAGS.get(character)
if not taint_flag:
comprehensive_taints.append(f"<UNKNOWN_TAINT_CHAR_{character}>")
elif taint_flag.when_present:
comprehensive_taints.append(taint_flag.desc)

return comprehensive_taints
return linux.Tainting(
self._context,
linux.LinuxUtilities.get_module_from_volobj_type(self._context, self).name,
).get_taints_parsed(self.taints, True)

@property
def section_symtab(self):
Expand Down Expand Up @@ -376,13 +329,6 @@ def section_strtab(self):
return self.strtab
raise AttributeError("Unable to get strtab")

@property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property needs to be exposed to avoid a bump too....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property was removed from the module class, not sure if you meant to write the comment here ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing it is a non-additive API change. That's why I said it needs to still be exposed there for anything that had been using it, otherwise a caller is going to call it expecting it to work and they'll get an ugly AttributeError. The point of the version numbers is to avoid situations like that, so happy for you to bump the version number, or put the property back, but you need to do one of them...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I see, module isn't versioned at all! 5:S That gets trickier... 5:\

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still needs to be exposed then, otherwise any code calling module_instance.taint_flags_list is going to throw an error. This is why I like people to be careful about what they throw into classes like this. Changing them in the future is tricky... 5:P

Copy link
Contributor Author

@Abyss-W4tcher Abyss-W4tcher Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property was added in this PR :), it was not present before. Changes might be confusing as we are moving things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it was only to be used internally, it needed to be called _taint_flags_list...

Copy link
Contributor Author

@Abyss-W4tcher Abyss-W4tcher Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point, however there is no need for bumping as it hasn't made it into the framework yet. I'll make sure to make it "private" though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, sorry, I know it's tricky to get right (and I'm getting lost looking at commit diffs inside the PR, so I thought it had already gone in). Yeah, anything that doesn't need to be accessed from the outside would should be marked with a leading _, otherwise it becomes part of the API and you've seen the mess of versioning that comes from that... 5;)

def taint_flags_list(self) -> Optional[List[interfaces.objects.ObjectInterface]]:
kernel = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self)
if kernel.has_symbol("taint_flags"):
return list(kernel.object_from_symbol("taint_flags"))
return None


class task_struct(generic.GenericIntelProcess):
def add_process_layer(
Expand Down
Loading