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

macOS: Support for recent kernels (automagic, modules) #1115

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from

Conversation

Abyss-W4tcher
Copy link
Contributor

Hello 👋,

This Pull Request aims to refactor the macOS x86/64 automagic layer stacking, as well as support for a recent kernel functionality named "KernelCache".

These changes only add APIs to the framework, without modifying inputs/outputs of existing ones.

Tested on kernels :

  • 10.9.3_build-13D65
  • 10.12.6_build-16G29
  • 10.15.7_build-19H15
  • 12.0.1_build-21A559
  • 13.6.4_build-22G513
  • 14.0_build-23A5257q

Justifications are given, in the form of a comment or a link to the official darwin source code.

Details

KernelCache

The kernelcache is a pre-compiled and pre-linked version of the XNU kernel, with essential device drivers and kernel extensions. It is stored in a compressed format and is decompressed into memory during the boot process. The kernelcache facilitates faster booting by having a ready-to-use version of the kernel and essential drivers available, thereby reducing the time and resources that would otherwise be spent loading and dynamically linking these components at startup.

source (french) : https://book.hacktricks.xyz/v/fr/macos-hardening/macos-security-and-privilege-escalation/mac-os-architecture#kernelcache

This feature impacts symbol resolving, as some need to be shifted with the "classical" KASLR shift, and some with the "vm_kernel_slide" shift. The current shift determining method, via darwin banner scanning, was actually calculating the "vm_kernel_slide" shift, from which it wasn't possible to also determine the KASLR shift. The new implementation takes advantage of the lowGlo kernel structure, which is consistent and available in old and recent kernels, and reveals the KASLR shift value. Banner scanning was also proven to be a bit slower, and more redundant, in its logic.

As discussed in #1114, a new macOS kernel module was added to the framework, which takes care of determining which slide to use to correctly resolve a symbol. Automagic is able to detect and instantiate it automatically, as well.

Additional changes

Some variable names and logics were changed in the mac stacker, to be more straight to the point, while keeping the API compatibility.


If you possess any macOS X samples, please feel free to merge this PR in your local installation, and give feedback !

Copy link
Member

@ikelos ikelos left a comment

Choose a reason for hiding this comment

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

Thanks very much for this, it looks setup the way I was hoping, but it's also quite involved (not quite as involved as the arm64 layer, but getting close). I've asked @atcuno for a review to make sure the Mac side of things and the slide stuff makes sense. I've given it a once over and it looks like I didn't implement everything needed for Modules properly, so I probably should go fix that first, but this looks very promising. 5:)

if kaslr_shift == 0:
vollog.log(
constants.LOGLEVEL_VVV,
f"Invalid kalsr_shift found at offset: {banner_offset}",
f"Unable to calculate a valid KASLR shift for banner : {banner}",
Copy link
Member

Choose a reason for hiding this comment

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

Outputting the banner seems less useful than the offset that was being checked to be a kernel? Any reason for the change in debug message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The banner offset isn't relevant anymore, to detect KASLR. This message can be useful, as there are many different banners scanned in a memory sample. KASLR shift will be banner independant, but the validation stage will stop at version_major and version_minor checks.

Copy link
Member

Choose a reason for hiding this comment

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

It does still get passed to find_aslr though, so either it is still useful, or the signature for find_aslr takes in unnecessary parameters (which should be fixed, but I don't know which version numbers would need bumping to do that safely)...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly, some parameters aren't necessary anymore, but it breaks the API...

volatility3/framework/automagic/mac.py Outdated Show resolved Hide resolved
volatility3/framework/automagic/mac.py Outdated Show resolved Hide resolved
version_minor_json_address
)
banner_major_version, banner_minor_version = [
int(x) for x in compare_banner[22:].split(b".")[0:2]
Copy link
Member

Choose a reason for hiding this comment

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

This feels like it will be fragile, could we split this out into a separate function so it's clearer, and maybe hunt for the version number a little better than hard coding 22 bytes in? I realize this was in the previous code, but this gives us an opportunity to tidy it up whilst we're here revamping everything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made a change to this logic, by leveraging a regex scanner. If you had something else in mind, do not hesitate to inform me.

)
)

if vm_kernel_slide_candidate & 0xFFF != 0:
Copy link
Member

Choose a reason for hiding this comment

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

Should this be the size of a page? If so, this should be the value from the layer.page_size - 1 rather than a hard coded value...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The page size might be architecture independant, even if it's 4kB for macOS x86/64, in the future it might be 16kB or 64kB for AArch64. To determine the page size, we would typically need to read a symbol from the kernel (if it even exists), but is a bit of a "snake biting its tail" situation.

This logic would need to be updated at two other places in this file (dtb aligned and aslr shift aligned).

Copy link
Member

Choose a reason for hiding this comment

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

Well, given it can be different we should at least parameterize these. The page size should be dependent on the architecture, so a value taken from the layer (which is why the layer has the page_size value)...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The LimeLayer, VMWareLayer , FileLayer etc. is embedding the page size ? I don't know how this could be relevant to higher layers (Intel, AArch64), as the page size is typically decided in the kernel config ?

But at least parameterizing it would be more uniform, indeed !

@@ -462,3 +462,149 @@ def __init__(
Module.__init__(
self, context, name, layer_name, offset, symbol_table_name, layer_name
)


class MacOSKernelCacheSupportModule(Module):
Copy link
Member

Choose a reason for hiding this comment

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

If you're constructing a new Module with addition configuration items, you'll need to override build_configuration and should define the get_requirements method of the underlying ConfigurableInterface. This has also highlighted that the Module/ModuleInterface classes don't define this (but should do).

Copy link
Member

Choose a reason for hiding this comment

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

It's actually not the Module/ModuleInterface that needs to define this, but the ModuleRequirement, so this might require changing mac plugins to require a mac module? 5:S

volatility3/framework/contexts/__init__.py Outdated Show resolved Hide resolved
context.config[pathjoin(config_path, self.layer_name, "kernel_start")]
& self.context.layers[self.layer_name].address_mask
)
context.config[pathjoin(config_path, "kernel_end")] = (
Copy link
Member

Choose a reason for hiding this comment

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

Rather than being a parameter, this should be based off the module offset and size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the case of a KernelCache, the offset will be the start of the machO KernelCache __TEXT, and not the "classic" kernel __TEXT start.

Using these parameters allow to use pre-determined values, instead of calculating them "on the fly", risking to mess things up.

FYI, here is a typical configuration, right before exiting mac.py automagic :

{
  "kernel_banner": "Darwin Kernel Version 21.1.0: Wed Oct 13 17:33:23 PDT 2021; root:xnu-8019.41.5~1/RELEASE_X86_64\u0000",
  "kernel_end": 18446743524345970688,
  "kernel_start": 18446743524335484928,
  "kernel_virtual_offset": 384188416,
  "memory_layer": "VmwareLayer",
  "page_map_offset": 451420160,
  "vm_kernel_slide": 379650048
}

Copy link
Member

Choose a reason for hiding this comment

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

This is a module, the module takes an offset and a size (or is supposed to). If it needs additional parameters, that's fine, but from the looks of it, it's just used different parameter names for values that actually mean the same as offset and size. The offset is where the module appears in virtual memory, and size is how many bytes it is.

context.config[pathjoin(config_path, "vm_kernel_slide")] = context.config[
pathjoin(config_path, self.layer_name, "vm_kernel_slide")
]
context.config[pathjoin(config_path, "kernel_start")] = (
Copy link
Member

Choose a reason for hiding this comment

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

@#Rather than being a new config option, this should use the existing offset value I believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

offset (a.k.a kernel_virtual_offset) is different from vm_kernel_slide (in the case of an MH_FILESET KernelCache), and needs to be passed as a config option.

Copy link
Member

Choose a reason for hiding this comment

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

kernel_virtual_offset is what we've typically passed in to the kernel module, but all modules, regardless of which module, have the common values of offset and size, but for the new module you've constructed you've chosen different names. When I add those into the get_requirements of the Module/ModuleInterface you'll see that you're defining the same values but with a different name. I'd prefer we stick with the generic value names that modules already have rather than custom ones for this specific type of module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry if the following has too much references, but I prefer to justify what I am proposing.

offset in this case is equivalent to vm_kernel_slide, yes, and I can update this.

But offset != kernel_start, as kernel_start is typically stext symbol. Doing so kernel_end - kernel_start is the size of kernel __TEXT, not especially the size of this module, starting from offset offset.

As an example, vm_kernel_slide/offset physical value can be 0x16c10000, but kernel_start is 0x16a10000.

See, as a quick reference (or even here for more details) :

paniclog_append_noflush("Kernel slide:      0x%016lx\n", (unsigned long) vm_kernel_slide);
paniclog_append_noflush("Kernel text base:  %p\n", (void *)vm_kernel_stext);

Which in the end, is needed to mimic the following behaviour :

/*
 * Returns true if the address lies in the kernel __TEXT segment range.
 */
bool
kernel_text_contains(vm_offset_t addr)
{
	return vm_kernel_stext <= addr && addr < vm_kernel_etext;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I consider kernel_start and kernel_end as additional requirements for this specific module.

@@ -501,3 +501,25 @@ class WindowsIntel32e(WindowsMixin, Intel32e):

def _translate(self, offset: int) -> Tuple[int, int, str]:
return self._translate_swap(self, offset, self._bits_per_register // 2)


class MacIntelMhFilesetKernelCache(Intel32e):
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure why these have been added to the layer, rather than the module? I think I probably need to review how configurations for modules are saved.

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 on Slack, this is the only reliable way I found to have the config options carried from automagic to the global context config (which are later used in the new MacOSKernelCacheSupportModule.

Copy link
Member

Choose a reason for hiding this comment

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

I think I'm missing something, these should be parameters on the Module and have nothing to do with the layer/architecture? Lemme get the Module/ModuleInterface stuff tidied up and then we can look at what's going on...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Basically, we determine useful values in automagic, and I want them to be accessible when we create the modules in automagic/module.py.

This have nothing to do with the layer indeed, it is just that the stacked layer has a config, and it is saved in the global context, so I can access them later (and it is also saved to the config, which can be exported with --save-config)

I'll wait for your update, I think the modules are missing the get_requirements() as you pointed out, which might help saving the config to the global context ?

Copy link
Member

@ikelos ikelos left a comment

Choose a reason for hiding this comment

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

Thanks for making some of those changes. I'm clearly still quite confused about some of the moving pieces. Specifically why the layer's are getting touched when the only thing that should be impacted are the modules. I'll get a PR out for the changes this has highlighted need making to the Module/ModuleInterface and then we can see how everything fits together after that...

)
)

if vm_kernel_slide_candidate & 0xFFF != 0:
Copy link
Member

Choose a reason for hiding this comment

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

Well, given it can be different we should at least parameterize these. The page size should be dependent on the architecture, so a value taken from the layer (which is why the layer has the page_size value)...

if kaslr_shift == 0:
vollog.log(
constants.LOGLEVEL_VVV,
f"Invalid kalsr_shift found at offset: {banner_offset}",
f"Unable to calculate a valid KASLR shift for banner : {banner}",
Copy link
Member

Choose a reason for hiding this comment

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

It does still get passed to find_aslr though, so either it is still useful, or the signature for find_aslr takes in unnecessary parameters (which should be fixed, but I don't know which version numbers would need bumping to do that safely)...

layer_class_str = context.config[layer_class_config_path]
if (
layer_class_str
== "volatility3.framework.layers.intel.MacIntelMhFilesetKernelCache"
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, it should end up as a string in the config, but we want to make sure that string relates to an existing class and that if the file gets moved/renamed etc, an IDE doing refactor can spot the break, hence converting it from an object to a string name. So, yeah, basically convert the python object to the string name (we can make a separate function for clarity) but then compare layer_class_str == class_string_name(class_we_care_about) rather than a fixed string.

You shouldn't ever need to convert from the string to a class?

context.config[pathjoin(config_path, "vm_kernel_slide")] = context.config[
pathjoin(config_path, self.layer_name, "vm_kernel_slide")
]
context.config[pathjoin(config_path, "kernel_start")] = (
Copy link
Member

Choose a reason for hiding this comment

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

kernel_virtual_offset is what we've typically passed in to the kernel module, but all modules, regardless of which module, have the common values of offset and size, but for the new module you've constructed you've chosen different names. When I add those into the get_requirements of the Module/ModuleInterface you'll see that you're defining the same values but with a different name. I'd prefer we stick with the generic value names that modules already have rather than custom ones for this specific type of module.

context.config[pathjoin(config_path, self.layer_name, "kernel_start")]
& self.context.layers[self.layer_name].address_mask
)
context.config[pathjoin(config_path, "kernel_end")] = (
Copy link
Member

Choose a reason for hiding this comment

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

This is a module, the module takes an offset and a size (or is supposed to). If it needs additional parameters, that's fine, but from the looks of it, it's just used different parameter names for values that actually mean the same as offset and size. The offset is where the module appears in virtual memory, and size is how many bytes it is.

@@ -501,3 +501,25 @@ class WindowsIntel32e(WindowsMixin, Intel32e):

def _translate(self, offset: int) -> Tuple[int, int, str]:
return self._translate_swap(self, offset, self._bits_per_register // 2)


class MacIntelMhFilesetKernelCache(Intel32e):
Copy link
Member

Choose a reason for hiding this comment

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

I think I'm missing something, these should be parameters on the Module and have nothing to do with the layer/architecture? Lemme get the Module/ModuleInterface stuff tidied up and then we can look at what's going on...

@ikelos
Copy link
Member

ikelos commented Mar 24, 2024

Ok, so ModuleRequirements differ from TranslationLayerRequirements and SymbolTableRequirements, in that they didn't have a class and therefore there was only ever expected to be one Module class. Since we're now introducing another potential class, it needs some reworking...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants