-
Notifications
You must be signed in to change notification settings - Fork 0
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
Validation mechanism for user-supplied kernel object pointers #2025
Comments
by Andrew Boie: I started to look into an implementation of this, this could even be used by itself outside of memory protection if there is suspicion that garbage or uninitialized pointers are being passed to kernel objects. I've found a problem though. This mechanism depends on run-time initialization of kernel objects, since any kernel object will need its encrypted pointer value stored. For example:
Any kernel object will have struct k_object as first member so you can just do a cast:
This is fine for runtime initialization, you just stick a _k_object_init() call in the init function.
It doesn't work for this case, k_sem_init() is never called.
I need to figure out some kind of preprocessor voodoo such that all objects of a particular type initialized this way, even ones embedded within other objects, will get their pointer values stuck in a special section that can be iterated over at boot to set the encrypted pointer value. this would be a lot easier to do if K_INITIALIZER macros were private to the kernel and not public. If we can do that, and enforce that only K_DEFINE() macros are public, then this is easy, the data structures already get put in a special section that can be iterated over. |
by Inaky Perez-Gonzalez: Hmm, good point This might be a security issue if we do it at link time, as if we know the location and the crypted key we could guess the cookie. We could, however, at link time, record in a section the list of pointers that have to be crypted and with which type cookie and at kernel initialization time, go over that table, generate the cookies and then free up the table. Would this work? |
by Andrew Boie: {quote} {quote} I'm planning on doing that. The trick is that it's easy if people use K_DEFINE(). I already have some code which does this. |
by Inaky Perez-Gonzalez: Oh yes, I agree, they have to be randomly generated at boot--sorry I made it confusing. Unless there is a need to keep K_*_INITIALIZER() around, seems a very sensible solution to me. |
by David Brown: I think I'm ok with using this XOR'd marker to validate objects, but we really shouldn't use the terms "encrypted". This isn't an encryption in any sense. Perhaps "token" or something as just a validator of the object. |
by David Brown: Also, 32-bits isn't all that big and may not offer all that much protection against rogue pointers passed in. |
by Andrew Boie: {quote}Perhaps "token" or something as just a validator of the object.{quote} Fine with me. {quote}Also, 32-bits isn't all that big and may not offer all that much protection against rogue pointers passed in.{quote} You lost me here. A bad pointer, to pass this check, would have to have the first 4 bytes of memory that it points to XOR'd with the validator and the bad pointer value be the result. Considering that these bad pointers will also be bounds checked to only be within the kernel's memory area and the odds of this happening by accident seem astronomical to me. |
by David Brown: bq. You lost me here. A bad pointer, to pass this check, would have to have the first 4 bytes of memory that it points to XOR'd with the validator and the bad pointer value be the result. Considering that these bad pointers will also be bounds checked to only be within the kernel's memory area and the odds of this happening by accident seem astronomical to me. As a way of detecting bugs and such, 4 bytes would be perfectly adequate. I wouldn't consider this adequate for security, since that is a reasonable number for a malicious agent to retry. |
by Andrew Boie: David Brown that's reasonable, thanks |
by Inaky Perez-Gonzalez: While 4G tries on an MCU might take quite a while, the risk is there. However, adding that:
what is your concern here? Is the concern that process A being able to find process' B pointer? It'd have to try a max of 2^64 combinations (assuming byte alignment) and then multiply that by the combinations of type key (so another 2^32). I think hitting one out of 2^96 will impose a good protection in there (of course, this is assuming proper RNG). As well, as Andrew mentioned, there will be bounds checks, so we know we are not being directed to a hyperspace location for DoS. What other concerns do you have? I'm curious to make sure we close all the possible holes or have rationales for them. |
by David Brown: I think my main concern is that we're effectively trying to do to much, and will end up moving ourselves away from a microcontroller OS. I think in light of both a debugging use, as well as a protection against some malicious uses, the 32-bit value will be fine. It would not be adequate against a platform where untrusted users are able to run arbitrary code. But, in our environment, the code is generally controlled, and this will reasonably protect these structures. At least as long as we don't try to call it encryption. |
by Andrew Boie: {quote}I think my main concern is that we're effectively trying to do to much, and will end up moving ourselves away from a microcontroller OS{quote} David Brown can you please elaborate here? |
by David Brown: I think looking at FreeRTOS-MPU is useful. I just fear we are implementing a bunch of stuff, and I'm not sure we know what our threat model even is. It seems to be easy to say "We have an MPU/MMU, what can we do with it", rather than figuring out what our use cases are, what threads we have, prioritize those threats. This is important as we have significantly limited resources here, and Zephyr wants to work across a fairly diverse range of device capabilities (from something with 16K of RAM, and 8 MPU slots, to things much more capable). Having a kernel/user-space separation is a fairly drastic step, doesn't fit particularly well with the current APIs, and as I understand, early in Zephyr's life was an explicit decision to not support. For example, it is perfectly reasonable to support an MPU/MMU with everything running in privileged mode. If our threats are against accidental errors in code, and things like errors resulting in rogue reads that could leak data, we can still protect against a lot, even though truly malicious code could just reprogram the MPU/MMU, a lot won't. I'm not saying we shouldn't separate threads, or any of the things discussed, but we need to understand our use cases, our what threats we have, and weigh them against the costs of implementing these features. |
by Andrew Boie: David Brown , unless I'm greatly misunderstanding something, nothing planned here will prevent Zephyr from continuing to run on the lowest-end devices. They may not be able to use all the new features they are bringing in but we are not saying good-bye to any class of device with this effort. I would also contend that you are underestimating how much we have considered our needs for this feature, maybe you weren't part of the discussion or didn't see the mailing list threads, I don't know. The threat we (Intel) are primarily interested in is to make it easier for developers to write complex multi-threaded applications and be able to debug them more effectively. Security for untrusted code on the system is a secondary goal, which we would like to also support well, but right now the outside requests we are getting are all to have thread protection, with FreeRTOS-MPU's design specifically called out as an example. As I discussed on the mailing list, this effort has several layers which can be supported on devices depending on their capabilities, in increasing order of complexity:
Right now the focus on our side for this effort is on No. 3. No. 4 is desirable as an iterative refinement. {quote}doesn't fit particularly well with the current APIs{quote} {quote} |
by Anas Nashif:
|
by Anas Nashif:
|
by Anas Nashif: David Brown Not sure you were present when Alex presented the outcome of the kernel dive-in we had. The outcome was captured in this slide: !screenshot-1.png|thumbnail! from this original slide set. [^2017-01-12_1_Zephyr_Kernel_assets.pptx] Thread isolation is the bare minimum we need to be able to call a system secure. I know Ruud Derwig was present in this discussion at least. |
by Andrew Boie: I'm still debugging and enabling validation for various kernel objects, but a sneak preview of this work can be seen here: https://github.com/andrewboie/zephyr/tree/kobject Certain network stack data structures have kernel objects embedded within them, I need to figure out the best way to deal with those. |
by Andrew Boie: |
by Andrew Boie: Taking a different approach than the XOR method. I have an RFC drafted and will present to the memory protection working group tomorrow morning, and a revision after that discussion will be posted here. |
by Andrew Boie: Problem statement:There should be no way for userspace threads, either through mistakes or Terminology:When we speak of "kernel objects" here, this includes all the typical Design goals:We would like to have the same APIs for both userspace and kernel space,
The scope of this proposal is to show how we intend, once we do the privilege
We are NOT trying to prevent against malfeasance in supervisor mode, We are only trying to limit the scope of kernel object misuse, not catch all TLDR proposal:At build time, parse the ELF binary to find all the kernel objects declared The constraint of this approach is that at minimum for objects referenced by For objects referenced by supervisor threads, heap allocation can be allowed Permission tracking of kernel objects will be done with bitfields, imposing Implementation DetailsCreating the perfect hash tableWe need to know where all the kernel objects are. These come in several flavors:
We can find all of these using the DWARF debugging data inside the ELF binaries This information will then be used to create a perfect hashtable: https://en.wikipedia.org/wiki/Perfect_hash_function Creating this table will be easy, we can just use GNU gperf to do it: https://www.gnu.org/software/gperf/manual/gperf.html This emits some C code which we can build and link into the kernel. Our Object Metadata and PermissionsThe hash function created by gperf will only tell us that a particular memory struct k_object __packed { We will need to implement this array such that given the address of a kernel With a default of max 16 threads, this whole thing will fit in a u32_t. There The permissions field will be initially zero. This is a bitfield; when threads
The API call to grant permission would be something like: k_object_grant_access(void *object, struct k_thread *thread); Validation will be done to ensure that the 'object' parameter actually points System call code flowNote that in the below caae, "fail" will typically mean "throw an exception It's also worth noting that not all kernel APIs will be exposed to userspace The code flow, for a userspace thread making an API call, would look roughly user code: What happens next is performed by the system call stubs, which are going to be
For kernel threads making API calls, much or all of this can be skipped, ConstraintsConstraints imposed by any protection implementationRegardless of whether this or some other method is used, it is a hard This means that no matter what, putting kernel objects onto a user thread's User-allocated data structures that don't live in kernel memory will not be User threads will not be able to allocate kernel objects at runtime on Additional constraints imposed by this proposalWith this proposal, and without making some potential exceptions (described To compensate for this, the plan is to augment the existing If, for whatever reason, an application just has to be able to put objects
Both of these are perilous and should only be enabled as a last resort. Lifecycle management:For the initial implementation, very little lifecycle management is planned,
Other CommentsFor the initial implementation, we are trying to change kernel APIs as little In the future, we may try to see if some objects could be implemented without |
by Andrew Boie: I have the DWARF parsing working, and can generate the hashtable with gperf. Now for the fun part: ensuring that including the text/data generated by gperf does not shift the memory addresses that we are hashing. |
by Andrew Boie: This is now available for code review: |
Reported by Andrew Boie:
Since we are using the same kernel APIs for userspace, we are going to need to validate any kernel object pointers passed in via system calls. When userspace passes in a pointer to a kernel object, we need to enforce that pointer is valid, lives in a region of memory controlled by the kernel, and actually corresponds to the requested kernel object type.
Brief summary, from an email thread on the subject:
{quote}
We want to have the same kernel APIs with and without memory protection. This will mean that kernel APIs called from userland will still take pointers to kernel objects as arguments even though the kernel objects live in memory private to the kernel.
When userspace passes in a kernel object pointer, we will need to verify that it indeed points to a kernel object of the expected type.
So we have 2 cases of pointers from userspace: pointers to buffers, and pointers to kernel objects. I think we need safe_memcpy for the latter case.
The proposed method (credit to Inaky) IIRC is as follows. Any given kernel object (let's use struct k_sem as an example), with memory protection turned on, will always have as its first member a struct:
struct kernel_object {
uint32_t encrypted_ptr;
... other metadata as needed
};
struct k_sem {
#ifdef CONFIG_MEMORY_PROTECTION
struct kernel_object ko;
#endif
... regular struct k_sem members
};
At boot for every kernel object type, the kernel will randomly generate some XOR keys, stored in memory only visible to the kernel. When a kernel object is created, the pointer value of that object is XOR'd with that key and the encrypted value stored in encrypted_ptr.
When userspace passes the kernel an object with address A:
{quote}
(Imported from Jira ZEP-2187)
The text was updated successfully, but these errors were encountered: