-
Notifications
You must be signed in to change notification settings - Fork 463
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: Address limitations in determining KASLR shifts by introducing VMCoreInfo support #1332
base: develop
Are you sure you want to change the base?
Linux: Address limitations in determining KASLR shifts by introducing VMCoreInfo support #1332
Conversation
This is extremely interesting! Great work! It seems like with a little more digging we can use it to get to kallsyms using this method and then get all the symbols for the image. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks awesome, and I'm really excited to see it land! 5:D Just needs a version check at the point of use, and some confidence that we do some kind of verification of the results we get, and then it's all good to go in! 5:D
context, layer_name, progress_callback=progress_callback | ||
) | ||
if aslr_shifts: | ||
kaslr_shift, aslr_shift = aslr_shifts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the rest of the automagic ever validate these values in any way? If not, perhaps they should (checking for an ELF signature or mapping the virtual kernel to the physical one and checking a number of bytes match, just something to make sure the map works correctly)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. But, if it already validates these values for find_aslr_classic()
, it will also do so for find_aslr_vmcoreinfo()
and vice versa.
To validate these values and, if it fails, proceed with a fallback action like trying the next potential candidate, we should convert the find_aslr_*() to return a generator and make a loop. Once the layer is created here try to do something with it to test if it works.
The find_aslr_vmcoreinfo()
could easily test if the ASLR address is correct even without creating a layer. The VMCOREINFO provides the virtual address (with the aslr shift already applied) of many symbols.
For instance, we can check that the address provided by get_symbol("init_uts_ns"), which is the System.map value (without the shift applied) be equal to the address provided in VMCOREINFO -> SYMBOL(init_uts_ns) minus the KERNELOFFSET. If that doesn't match there's something wrong with that VMCOREINFO table and it will have to find the next one. We can validate one, some or all of them, for instance:
SYMBOL(init_uts_ns)=ffffffffb99e82e0
SYMBOL(node_online_map)=ffffffffb9a4b680
SYMBOL(swapper_pg_dir)=ffffffffb963c000
SYMBOL(_stext)=ffffffffb7200000
SYMBOL(vmap_area_list)=ffffffffb983ce70
SYMBOL(mem_section)=ffff8d57ff7d1000
SYMBOL(prb)=ffffffffb96a24e0
SYMBOL(printk_rb_static)=ffffffffb96a2500
SYMBOL(clear_seq)=ffffffffba01d220
SYMBOL(kallsyms_names)=ffffffffb89ace68
SYMBOL(kallsyms_num_syms)=ffffffffb89ace60
SYMBOL(kallsyms_token_table)=ffffffffb8c74fb0
SYMBOL(kallsyms_token_index)=ffffffffb8c75338
SYMBOL(kallsyms_offsets)=ffffffffb8c75538
SYMBOL(kallsyms_relative_base)=ffffffffb8d3bc00
SYMBOL(init_top_pgt)=ffffffffb963c000
SYMBOL(node_data)=ffffffffb9a46720
However, this approach makes the VMCOREINFO implementation dependent on a symbol table, which IMO is a mistake. It would prevent future capabilities, such as retrieving symbols from kallsyms. VMCOREINFO should be the first step and must remain completely independent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand, I think that we could also move the find_aslr_vmcoreinfo() to be executed before and outside the layer.scan() banner loop. Since it's independent of the ISF, I think it should be here.
@ikelos I would also like to store the vmcoreinfo dict in a location that's accessible to other parts of the framework, including the core and plugins. What are your thoughts on this? Where do you think would be the best place for it? |
Wouldn't it just be an extended type with an offset you could construct from a symbol? That feels like it should be a one liner to construct, and that would allow you to add helper functions to the class if needed? Anything else would likely require a fair bit of special casing to support properly? If it's something that explicitly needs locating, then a plugin to do the location and a static/classmethod other plugins can call to get to it? |
Maximize use of VMCOREINFO data without reliance on ISF symbols: - Obtain the DTB - Utilize OSRELEASE (the same as UTS_RELEASE used in the Linux banner and init_uts_ns/new_utsname) to prefilter the list of Linux banners, reducing search time for linux_banner in memory. - Find the correct layer using the VMCOREINFO data (including 32bit PAE).
Hey @ikelos, apologies for the post-review refactor, but I found a better approach and many improvements. In this latest commit, I moved the VMCoreInfo implementation into its own stacker class, leaving the original untouched (except for the virtual_to_physical_address that's now staticmethod) and making it easier to fall back to the traditional one if something goes wrong. Apart from that, these are all the improvements from the previous implementation: Maximize use of VMCOREINFO data without reliance on ISF symbols:
|
…OREINFO if DTB is not found
…tely, includes the 0x prefix, allowing the parser to still handle these cases correctly
Hi, thanks for the awesome work as usual ! Before your PR, to handle potential new architectures in the future, I started to work on a local branch about a more scalable LinuxStacker, which can be easily modified to integrate new stackers. Do you think it would be possible to directly scale your implementation here, where code is unified as much as possible ? Even if there is still only Intel for now, at least the ground work will be laid down and no complete rethinking will be needed later. Something like that, as pseudocode: class LinuxVMCOREINFOStacker(interfaces.automagic.StackerLayerInterface):
def stack():
arch_stacker_to_use : LinuxIntelVMCOREINFOStacker = cls._determine_arch()
layer = arch_stacker_to_use.stack()
class LinuxIntelVMCOREINFOStacker():
def stack():
# implies _vmcoreinfo_find_aslr() is a classmethod
kaslr, aslr = LinuxVMCOREINFOStacker._vmcoreinfo_find_aslr()
pass
# This might be done in another PR as it's not vmcore related ?
class LinuxStacker(interfaces.automagic.StackerLayerInterface):
def stack():
arch_stacker_to_use : LinuxIntelStacker = cls._determine_arch()
layer = arch_stacker_to_use.stack()
class LinuxIntelStacker():
def stack():
kaslr, aslr = LinuxStacker.find_aslr()
pass Thanks by advance, and feel free to tell me if something is not clear about what I'm asking or if it doesn't fit 👍. |
Ok, before I review the refactored version, lemme get a couple quick points down:
I'll try to give this a review soon, but since it's affecting a core component it might take me a little while to go through it properly. I like the idea very much though, just not as its own stacker, either as part of the LinuxIntel stacker, or its own automagic... |
Hey @ikelos
It performs the same tasks as LinuxIntelStacker: finding ASLR offsets, the DTB, selecting the ISF, and returning the appropriate layer. The only difference is in how it locates these elements. LinuxIntelStacker find the init task and derives all other parameters based on it. This new approach pulls values directly from VMCOREINFO, providing a more direct solution. |
That's awesome, this worked like a charm: layer._direct_metadata = collections.ChainMap(
{"vmcoreinfo": vmcoreinfo}, layer._direct_metadata
) |
Hmmmm, ok. If it serves the same purpose. I guess things can't rely on metadata existing (since there's no guarantee). I assume this is prioritized to run first and that if it succeeds, the second one bombs out immediately? |
Yeah, we'll need to check if that's presnet i.e The LinuxIntelVMCOREINFOStacker has higher priority than the LinuxIntelStacker, so it runs first. Since it returns a valid layer, the subsequent stackers are not executed. |
I mean, it could get a bit sticky, but you may also need to consider virtualization, so the layer below this might be its own linux layer with its own |
Unless I'm missing something, since the the metadata is also stackable, I don't think that will be an issue. There will be two (or more) vmcoreinfo at different levels. The same way there more than one "architecture" key: >>> layer.metadata._dict
ChainMap({'os': 'Linux'}, ChainMap({'architecture': 'Intel64'}, ChainMap({'architecture': 'Intel32'}, {'mapped': True}, {'architecture': 'Unknown', 'os': 'Unknown'})), <volatility3.framework.interfaces.objects.ReadOnlyMapping object at 0x70360bd48a90>) As long as you are in the right layer, you will get the right vmcoreinfo. Same way, we get >>> layer.metadata.architecture
'Intel64' On the other hand, regarding virtualization :) ... I don't think LinuxIntelStacker supports it, does it? That could also be a challenge for LinuxIntelVMCOREINFOStacker. We'll need to investigate further to figure out how to properly identify OS instances and stack them correctly. So far, both methods use the first "valid" info they can find in memory. |
The concern was using the presence of it in the metadata to bomb out of trying to find it. Otherwise stacking should work as expected (and the lower layer can be interrogated for its vmcoreinfo if needed. I don't think the linux stacker supports it yet, just thinking of my work on the VMCS stuff, and Intel stacked on Intel. Yeah, should be ok (the lower layers will only have access to their own version, but you might have to test one and see if it fits as to whether to keep searching... |
Oh you mean if for instance, the current layer (let's say a guess OS) doesn't have a vmcoreinfo but the host (or the parent OS) does. If we do Hmm, is there a way to check if the key is present at the top of the metadata? Alternatively, we could set the vmcoreinfo key to empty in the metadata if it wasn't obtained from LinuxIntelVMCOREINFOStacker. Is there a way to assign 'default' values, or do you have any suggestions on the best approach to handle this? |
Yeah, that kind of issue. Hmmm, well, either you can check the lower layers to make sure their |
As I say, if you traverse up for layer list, each layer will have its version of the metadata in it, so you could get back to old (now hidden) |
How does the last commit look? |
…o be modified; otherwise, it will only exist within the instance namespace.
Actually, there was a bug. It needs to be added in the class namespace. It should be fine now. |
I found what's happening. The layer instance returned by the stacker object is added to the context here. However, that's not the final
|
First off, don't update the class metadata, or it could bleed through into other layers that share the same class, it needs to be on a per-instance basis. Also, each layer creates its own chainmap, so you shouldn't need a new one, but there should be very little impact on making a new one if you really want or see a need to... The reason for stashing the original context and rebuilding it at the end is because the stacker will try lots of layers, and we didn't want the context filling up with bogus layers that didn't work. There should be a way of getting the information across the gap, but I'm not sure how best to do it. We could:
I think I'm leaning towards the configuration option, but the downside with that is, the configuration intentionally only allows simple data values, so you'd need to split up the object into the bits you want/need. It might be tricky to access (although, to be fair, we should probably do that with the metadata, so that the object isn't constructed on a clone context that then deviates from the "real" context later on). So yeah, some questions that could do with answering at some point, but I still feel there's a way of achieving the goal, we just need to choose it carefully... |
…info dict to the layer metadata
Sounds good! I updated the code at this line to instead include vmcoreinfo in the layer configuration, like this: for key, value in vmcoreinfo.items():
layer.config[join("vmcoreinfo", key)] = value However, when it's merged here, the vmcoreinfo config path is not merged in the context. context.config.merge(
path, new_context.layers[layer].build_configuration()
)
...
>>> list(new_context.layers[layer].config)
['kernel_banner', 'memory_layer', 'page_map_offset', 'kernel_virtual_offset', 'vmcoreinfo.OSRELEASE', 'vmcoreinfo.BUILD-ID', 'vmcoreinfo.PAGESIZE', 'vmcoreinfo.SYMBOL(init_uts_ns)', ...]
>>> list(context.config)
['automagic.LayerStacker.single_location', 'automagic.LayerStacker.stackers', 'plugins.PsList.threads', 'plugins.PsList.decorate_comm', 'plugins.PsList.dump', 'plugins.PsList.pid', 'plugins.PsList.elfs.elfs', 'plugins.PsList.elfs.pslist.pslist', 'plugins.PsList.elfs.pslist.elfs.elfs', 'plugins.PsList.kernel.layer_name.page_map_offset', 'plugins.PsList.kernel.layer_name.kernel_virtual_offset', 'plugins.PsList.kernel.layer_name.kernel_banner', 'plugins.PsList.kernel.layer_name.class', 'plugins.PsList.kernel.layer_name.memory_layer.class', 'plugins.PsList.kernel.layer_name.memory_layer.base_layer.location', 'plugins.PsList.kernel.layer_name.memory_layer.base_layer.class']
>>> list(new_context.layers[layer].build_configuration())
['page_map_offset', 'kernel_virtual_offset', 'kernel_banner', 'class', 'memory_layer.class', 'memory_layer.base_layer.location', 'memory_layer.base_layer.class'] And if I take that layer from an automagic that runs after LayerStacker, the vmcoreinfo is not there. >>> layer_name
'layer_name'
>>> context.layers[layer_name].config
['page_map_offset', 'kernel_virtual_offset', 'kernel_banner', 'class', 'memory_layer', 'memory_layer.class', 'memory_layer.base_layer', 'memory_layer.isf_url', 'memory_layer.symbol_mask', 'memory_layer.base_layer.location', 'memory_layer.base_layer.class'] any clue? EDIT: Ok, it's only merging the values required by the Intel layer here. >>> layer_name
'layer_name'
>>> layer = context.layers[layer_name]
>>> pickle.loads(layer.config["vmcoreinfo"])
{'OSRELEASE': '6.8.0-41-generic', 'BUILD-ID': '60a8f2b523b8e496d3358c463af50fcfa301a572', 'PAGESIZE': 4096, 'SYMBOL(init_uts_ns)': 184467440717779769 ... |
Hey @ikelos, in the last commit, I reverted the changes related to storing the vmcoreinfo. It's not needed at this stage, and including it would delay and block this PR. Let's proceed without that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this looks pretty good, just a couple minor points (possibly from some edits/experiments at some point). Would probably've been quicker/easier to review as a plugin and then the automagic bit, but happy to keep them all in one PR if you'd prefer.
identifiers_path = os.path.join( | ||
constants.CACHE_PATH, constants.IDENTIFIERS_FILENAME | ||
) | ||
sqlite_cache = symbol_cache.SqliteCache(identifiers_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this could be a CacheManagerInterface
rather than requiring the SQL implementation. It'll make it easier for us in the future if it's the more generic interface please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That also technically means you shouldn't have to check the version of it. 5;P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm.. I was mirroring what LinuxIntelStacker does since both stackers call symbol_cache.SqliteCache()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, well, that shouldn't unless it's making SQL specific requests (which it shouldn't). We should probably get that fixed up (in a different PR) if you want to file a ticket about it please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, I see, it's because we're loading an identifiers file of a particular format... 5:S Looks like I'll need to add something that globally loads a symbol cache...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, #1363 should take care of this now (if you review it and are happy with it, I'll fast track it rather than the couple of days wait).
context.symbol_space.append(table) | ||
|
||
# Build the new layer | ||
new_layer_name = context.layers.free_layer_name("IntelLayer") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will name the layer something like IntelLayer1
? I'd thought you wanted to change that to something like primary
or similar?
|
||
# Build the new layer | ||
new_layer_name = context.layers.free_layer_name("IntelLayer") | ||
config_path = join("IntelHelper", new_layer_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should include vmcore, since IntelHelper
could be used by any intel automagic and this is specifically for the vmcore stuff. I don't think it's signficant, it just needs to be unique (also, I dunno that we often use capitals in the config keys?) 5;)
"""Converts the input VMCoreInfo data buffer into a dictionary""" | ||
|
||
# Ensure the whole buffer is printable | ||
if not all(c in string.printable.encode() for c in vmcoreinfo_data): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big fan of single letter variable names... 5:S Could I tempt you to use char
(or even chr
) instead of c
, pretty please? 5:)
return vmcoreinfo_dict | ||
|
||
@classmethod | ||
def _parse_value(cls, key, value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully this isn't too brittle. It's a shame there isn't an existing parser for this kind of data, but just one to keep on top of (if we start returning things as strings when we don't know what they are)...
module = context.module(elf_table_name, layer_name, 0) | ||
layer = context.layers[layer_name] | ||
|
||
# Both Elf32_Note and Elf64_Note are of the same size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the comment, much appreciated! Saved me asking the question! 5:D
linux_constants.VMCOREINFO_MAGIC_ALIGNED | ||
) | ||
|
||
# Also, confirm this with the first tag, which has consistently been OSRELEASE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do like verify data and this is well documented, but strictly there's nothing that requires it to be first, so this may rule out valid data. I'm happy to go with it (because it's commented so we can quickly diagnose if it's going wrong), just another thing to keep in mind.
…the layer metadata dict
This pull request addresses a limitation in the current method for determining KASLR and ASLR shifts by extracting these values from the VMCOREINFO ELF note. It also retrieves the DTB and selects the layer using only data from this ELF note and independently of any ISF symbol.
When present, VMCOREINFO offers a more accurate way to retrieve the current kernel parameters, making it the preferred method in tools like crash, makedumpfile, and drgn.
The KASLR and ASLR shifts issue
The existing scanning method can inaccurately calculate these shifts in certain situations. For instance, a QEMU memory dump demonstrates this issue.
This is because the sample being analyzed has not just one
swapper
string match, but four, with the last one being the correct match.This causes the current find_aslr() scanning implementation to incorrectly calculate the KASLR/ASLR shifts, resulting in the entire analysis failing.
VMCoreInfo
The VMCoreInfo was introduced in Linux kernel 2.6.24 to assist user-space tools such as Crash and makedumpfile in analyzing the kernel's memory layout.
It provides all the essential information and kernel parameters needed to analyze a crash memory dump.
Volatility3 using VMCoreInfo
This PR enhances the Volatility 3 framework by adding support for searching the VMCoreInfo ELF note during initialization. It retrieves the ASLR shift and calculates the physical shift. If the VMCoreInfo ELF note is not found, it gracefully falls back to the traditional method.
Pros
Cons
Demo
$ time python3 ./vol.py \ -f ./dump_ubuntu180464bit_4.15.0-213-generic_reptile.core \ linux.pslist Volatility 3 Framework 2.11.0 OFFSET (V) PID TID PPID COMM File output 0x8c1cbc8997c0 1 1 0 systemd Disabled 0x8c1cbc89af80 2 2 0 kthreadd Disabled 0x8c1cbc898000 3 3 2 kworker/0:0 Disabled ... 0x8c1cb7f897c0 1126 1126 1125 bash Disabled real 0m8.990s user 0m7.880s sys 0m0.982s
The VMCoreInfo plugin
Additionally, this PR introduces the VMCoreInfo plugin. It's a particular plugin that works without relying on an external ISF symbol file, needing only the elf.json already included with the framework. It supports any architecture, with the only requirement being that it must be
little-endian
. This can be addressed in future updates.Demos
x86-64
i686
aarch64
AARCH64 uses NUMBER() with hex values, see line which seems odd, or at least it’s not what other kernel developers are doing on x86, where NUMBER is always a decimal number.
Anyway, fortunately, it includes the
0x
prefix, allowing the API value parser to handle these cases correctly. Unfortunately, whenlinux.vmcoreinfo
performs the inverse process, it cannot determine whether to display the value as a decimal or hexadecimal number. That’s why it shows lines like:Nonetheless, it doesn’t hurt. Internally, the VMCOREINFO API converts all values to integers and manages them correctly.