|
|
Subscribe / Log in / New account

Hardening the kernel against heap-spraying attacks

Please consider subscribing to LWN

Subscriptions are the lifeblood of LWN.net. If you appreciate this content and would like to see more of it, your subscription will help to ensure that LWN continues to thrive. Please visit this page to join up and keep LWN on the net.

By Jonathan Corbet
March 21, 2024
While a programming error in the kernel may be subject to direct exploitation, usually a more roundabout approach is required to take advantage of a security bug. One popular approach for those wishing to take advantage of vulnerabilities is heap spraying, and it has often been employed to compromise the kernel. In the future, though, heap-spraying attacks may be a bit harder to pull off, thanks to the "dedicated bucket allocator" proposed by Kees Cook.

Consider, for example, a use-after-free bug of the type that is, unfortunately, common in programs written in languages like C. Memory that is freed can be allocated to another user and overwritten; at that point, the code that freed the memory prematurely is likely to find an unpleasant surprise. The surprise will become even less endearing, though, if an attacker is able to control the data that is written into the freed memory. Often, that is all that is needed to turn a use-after-free bug into a full kernel compromise.

It is, of course, difficult for an attacker to get their hands on precisely the chunk of memory that is being mishandled in the kernel. When precision is not possible, sometimes brute force will do. Heap spraying involves allocating as many chunks of memory as possible, and writing the crafted data into each, in the hope of happening upon the right one. Given the way that the kernel's slab allocator works, the chances of succeeding are higher than one might expect.

The kernel has a variety of ways to allocate memory but, much of the time, a simple call to kmalloc() is used; this is especially true if the size of the memory to be allocated is not known ahead of time. Within the allocator, the requested memory size is rounded up to the nearest "bucket" size, and the requested chunk is allocated from the associated bucket. Those sizes are (mostly, but not exclusively) powers of two, so any allocation request between 33 and 64 bytes, for example, will be satisfied from the 64-byte bucket.

If an attacker has determined that a given structure, allocated with kmalloc(), is used after being freed, they can attempt a heap-spraying attack by forcing the kernel to allocate (and write) a large number of objects from the same bucket. As it turns out, there are ways in which an attacker can get the kernel to do just that.

The solution to this kind of problem (beyond fixing every use-after-free bug, of course) is to keep the allocation pools separate. If every allocation comes from its own heap, it cannot be used to spray somebody else's heap. Unfortunately, the current design exists for a reason; using common buckets for allocations across the kernel significantly increases allocation efficiency and memory utilization. So that is unlikely to change.

During the 6.6 development cycle, an effort to improve the kernel's resistance to heap spraying, in the form of the kmalloc() randomness patches, was merged. This work split each of the kmalloc() buckets 16 ways and caused the allocator to pick a random bucket (based on the request call site) to satisfy each request, with the result that a heap-spraying attack has a high chance of hitting the wrong bucket. It is a way of partially separating allocations without giving up entirely on a common set of heaps. Randomness hardens the kernel to an extent, but it is a probabilistic defense that will surely fail at times.

The key point behind Cook's approach is that it is not necessary to separate all allocations into their own heaps; if the kernel could ensure that any user-controllable allocation is satisfied from a different pool than anything else, heap spraying would be much harder to implement. To get there, a new API must be created for kernel subsystems that perform user-controllable allocations; that is what the patch series does.

The first step for such a subsystem is to create its own heap for variable-size allocations with a call to:

    kmem_buckets *kmem_buckets_create(const char *name, unsigned int align,
			  	      slab_flags_t flags,
			  	      unsigned int useroffset,
				      unsigned int usersize,
			  	      void (*ctor)(void *));

This call is similar to kmem_cache_create(), which creates a heap for fixed-size allocations. The name of the heap (which can be used for debugging) is given by name, and align describes the required alignment for objects allocated there. Flags for the slab allocator are given in flags. The useroffset and usersize parameters describe the portion of an object that might be copied to or from user space (information that is used by the kernel's user-space copying hardening mechanism), and ctor() is an optional constructor for allocated objects. The return value is a pointer to a kmem_buckets object that can be used for future allocations.

Once that call succeeds, objects can be allocated with:

    void *kmem_buckets_alloc(kmem_buckets *b, size_t size, gfp_t flags);

Here, b is the pointer returned by kmem_buckets_create(), size is the size of the allocation, and flags is the usual GFP flags; a pointer to the allocated memory is returned. A normal kfree() call can be used to free objects when they are (truly) no longer needed. There is also a kmem_buckets_valloc() that can fall back to vmalloc() if need be.

The other part of the puzzle is to use this new allocator in the right places. Seemingly, the msgsnd() system call is a favorite tool for heap-spraying attacks, since the kernel implements it by allocating a structure to contain the message to be sent (the size and contents of which are controlled by user space). Cook's series includes a patch causing msgsnd() to use the new bucket allocator, separating its allocations from all others and removing its utility in this kind of attack. Another patch switches the internal memdup_user() and vmemdup_user() functions, which are used to copy data from user space into the kernel. Many of the call sites for those functions will give user space some control over allocation sizes, so isolating them could prevent a lot of problems.

While the proposed changes are relatively small, they could have an oversized impact on kernel security. Separating off user-controllable allocations in this way can block many of the exploits that have succeeded against the kernel in the past. Creating a kernel that is free of memory-safety bugs does not seem like a feasible goal in the near future, but making one where such bugs are harder to exploit is possible. Chances are that this patch series, in some form, will show up in the mainline before too long.

Index entries for this article
KernelMemory management/Slab allocators
KernelSecurity/Kernel hardening
SecurityLinux kernel/Hardening


(Log in to post comments)

Hardening the kernel against heap-spraying attacks

Posted Mar 22, 2024 11:18 UTC (Fri) by makendo (subscriber, #168314) [Link]

Would this feature become a CVE?

Hardening the kernel against heap-spraying attacks

Posted Mar 22, 2024 14:45 UTC (Fri) by Paf (subscriber, #91811) [Link]

Since it would presumably not be backported as a fix to stable kernels, I think no.

Hardening the kernel against heap-spraying attacks

Posted Mar 22, 2024 23:47 UTC (Fri) by dullfire (subscriber, #111432) [Link]

> The first step for such a subsystem is to create its own heap for variable-size allocations with a call to:

Hmmm it seems like it might be both more performant AND less work overall to make the bucket selection be a function of the call stack. Say a hash function based of the return IP of frames 3-6 (can't include frame zero because that would always be the same). That would result in a given call site always going to the same bucket first, while also (given a good hash) spreading out which bucket other call sites got to.

Hardening the kernel against heap-spraying attacks

Posted Mar 23, 2024 6:40 UTC (Sat) by corbet (editor, #1) [Link]

That is essentially what the randomization patches do, tossing in a random nonce so that each boot is different. It's an improvement, but it won't eliminate situatoins where the attacker is able to target the same bucket.

Hardening the kernel against heap-spraying attacks

Posted Mar 25, 2024 11:46 UTC (Mon) by dullfire (subscriber, #111432) [Link]

Bah, thanks. Silly me. I completely missed that part when I started looking to see if was about to (at the time) suggest was present.

Hardening the kernel against heap-spraying attacks

Posted Mar 27, 2024 8:59 UTC (Wed) by jpfrancois (subscriber, #65948) [Link]

Interesting discussion of the patch set
https://dustri.org/b/notes-on-the-slab-introduce-dedicate...

Hardening the kernel against heap-spraying attacks

Posted Mar 28, 2024 14:18 UTC (Thu) by kees (subscriber, #27264) [Link]


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds