CONSTIFY: Fast Defenses for New Exploits

By Mathias Krause

October 2, 2023

Introduction

Two weeks ago, Seth Jenkins of Google Project Zero posted an article about an in-the-wild exploit targeting Android. If you haven’t read the article yet, you should do so now. It’s quite interesting and the rest of this article builds on top of it.

The bugs themselves weren’t all that interesting, as the exploit was solely based on n-days, meaning Android was simply lacking patches for known vulnerabilities. However, the exploitation technique caught our attention, as it was, indeed, novel.

Part of our standard process in grsecurity when exploits or techniques are published, even if they involve out-of-tree components as in this case, is to fully analyze the publication, identify novel techniques, and find ways to prevent their reuse against customer systems. This short blog provides the results of that recent investigation.

Evaluating the Exploitation Technique

The exploitation technique abused the limited write primitive provided by CVE-2023-26083 to (1) craft a fake struct file_operations object, (2) replace ashmem_misc.fops to point to that crafted object and (3) make use of simple open() / read() / write() system calls to escalate privileges further.

This has the advantage that it not only complies with the rules of a possible CFI implementation, as the fake fops object doesn’t attempt to start a ROP chain but instead contains only legitimate function pointers for each hook it replaces. The exploitable type confusion happens in the callbacks themselves, that mix struct ashmem_area and struct configfs_buffer objects.

This technique also allows using simple system calls for further exploitation, making the exploit much more reliable, as it can implement error handling in userspace – something that would be hard to do within a ROP chain.

While struct file_operations objects are usually read-only, the exploit abused the fact that struct miscdevice objects are not, allowing the ->fops pointer to be manipulated at runtime.

Implementing a Defense

Having read that, we quickly looked into protecting struct miscdevice in grsecurity. With an arsenal of compiler plugins implementing security-focused enhancements to the C language, CONSTIFY, and specifically its capability to protect objects with static storage at runtime while still allowing them to get modified at the source level was the preferred choice. We could protect all static miscdevice objects from stray write attempts, including the targeted ashmem_misc.

As not all objects are of static storage, we also wanted to randomize the type using RANDSTRUCT, to provide a certain level of binary diversity, making exploits require target specific knowledge, e.g. the concrete offset of the fops member to craft an exploit.

As the required functionality was readily available, the below diff is the core of the change that was needed to protect struct miscdevice instances in grsecurity:

diff --git a/include/linux/miscdevice.h b/include/linux/miscdevice.h
index 0676f18093f9..5c505500a1bb 100644
--- a/include/linux/miscdevice.h
+++ b/include/linux/miscdevice.h
@@ -86,7 +86,7 @@ struct miscdevice  {
    const struct attribute_group **groups;
    const char *nodename;
    umode_t mode;
-};
+} __randomize_layout __mutable_const;

 extern int misc_register(struct miscdevice *misc);
 extern void misc_deregister(struct miscdevice *misc);

The __mutable_const type annotation will instruct the CONSTIFY plugin to move all static instances of struct miscdevice to a write-protected memory segment and instrument source-level writes to temporarily allow the write access to get through for the current CPU. Stray attempts, however, as from a UAF-bug based write primitive will be caught, as the underlying memory is write-protected.

Making the type randomizable (the __randomize_layout annotation) required changing all static instances to be using the designated initializer scheme instead of a positional one, which lead to quite some code churn as, for example, can be seen below for KVM:

diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
index a4ca1aa154bc..45f7c95f2797 100644
--- a/virt/kvm/kvm_main.c
+++ b/virt/kvm/kvm_main.c
@@ -4869,9 +4869,9 @@ static struct file_operations kvm_chardev_ops = {
 };
 
 static struct miscdevice kvm_dev = {
-   KVM_MINOR,
-   "kvm",
-   &kvm_chardev_ops,
+   .minor  = KVM_MINOR,
+   .name   = "kvm",
+   .fops   = &kvm_chardev_ops,
 };
 
 static void hardware_enable_nolock(void *junk)

And even though the randomization of struct miscdevice’s members isn’t strictly needed to thwart the exploit technique, less so for protecting the targeted ashmem_misc object, it further supports the required prior knowledge an attacker needs to acquire before dynamically allocated miscdevice objects can be attacked.

While going over all static instances in the Linux kernel we were able to improve a few ones even further by avoiding runtime modifications to the static part altogether as well as fixing some bugs along the way. Reading code is always worth it!

Conclusion

The compiler plugins the team has built over the years provide us a rich tool set to choose from when thinking about new defenses. At the same time, they can be very easy to use, as can be seen in the above source code changes – a type annotation is all that’s needed to activate the protection.

This, in turn, allows us to react to new threats quickly, develop new defenses and bring them to customers fast.

The new defense was added to all grsecurity kernels released last week.