Kernel Heap

The pain of it all

Historically, the Linux kernel has had three main heap allocators: SLOB, SLAB and SLUB.

SLUB is the latest version, replacing SLAB as of version 2.6.23. SLOB was used as the backup to SLAB and SLUB, but was removed in version 6.4. As a result, SLUB is all we really have to care about (even pre-6.4, SLOB was practically never used). From here on out, we will only talk about SLUB, unless explicitly stated.

Note that, confusingly, "chunks" in the kernel heap are called objects and they are stored in slabs.

Slabs and Caches

Unlike the glibc heap, SLUB has fixed sizes for objects, which are powers of 2 up to 8192 along with 96 and 192. These are conveniently called kmalloc-8, kmalloc-16, kmalloc-32 , kmalloc-64, kmalloc-96, kmalloc-128, kmalloc-192, kmalloc-256, kmalloc-512, kmalloc-1k, kmalloc-2k, kmalloc-4k and kmalloc-8k. We call these individual classifications caches, and they are comprised of slabs.

Each slab is assigned its own area of memory and comprised of 1 or more continuous pages. If the kernel wants to allocate space in the heap, it will call kmalloc and pass it the size (and some flags). The size will be rounded up to fit in the smallest possible cache, then assigned there. Anything larger than 8192 bytes will not use kmalloc at all, and uses page_alloc instead.

This approach is a massive performance improvement. It can also make exploitation primitives harder, as every object is the same size and it's harder to overlap. Similarly, because the sizes are determined by the cache rather than metadata, we cannot fake size.

Slab Creation

We can get to a point where we have so many objects in a cache that they fill all of the slabs. In this case, a new slab is created. This slab does not create the singular object - it will create multiple objects. Why? Because the kernel knows that this slab is only used for kmalloc-1k objects, it creates all possible objects immediately and marks the remaining as free.

These remaining three are saved in the freelist in a random order, provided that the configuration CONFIG_SLAB_FREELIST_RANDOM is enabled (which it is by default).

The default size of slabs depends on the cache it is being used for. You can read /proc/slabinfo to see the current configuration for the system:

$ sudo cat /proc/slabinfo
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> [...]
[...]
kmalloc-8k            80            80        8192          4            8
kmalloc-4k           208           208        4096          8            8
kmalloc-2k           768           768        2048         16            8
kmalloc-1k          1296          1296        1024         16            4
kmalloc-512         2190          2224         512         16            2
kmalloc-256         1917          1936         256         16            1
kmalloc-128         1024          1024         128         32            1
kmalloc-64          7532          7936          64         64            1
kmalloc-32          6442          6528          32        128            1
kmalloc-16         10123         10240          16        256            1
kmalloc-8           5120          5120           8        512            1
kmalloc-192         3885          3885         192         21            1
kmalloc-96          3506          4158          96         42            1

Here objsize is the size of each element in the cache, and objsperslab is the number of objects created at once when a new slab is initialized. Then pagesperslab is the product of objsize/0x1000 (pages per object) and objperslab, and tells you how many pages each slab has.

TODO CONFIG_SLAB_FREELIST_HARDENED.

The Kernel Heap is Global

One major difference between user- and kernel-mode heap exploitation is that the kernel heap is shared between all kernel processes. Kernel modules and every other aspect of the kernel use the same heap.

So, let's say you find some sort of kernel heap primitive - an overflow, for example. Overflowing into identical objects might not be helpful, but in the kernel, we can find common structs with powerful primitives that we can use to our advantage. Imagine that there is a struct that contains a function pointer, and you can trigger a call to this function. If this struct is allocated to the same cache as the object you can overflow, it is possible to allocate this struct such that it inhabits the object located directly behind in memory. Suddenly the overflow is incredibly powerful, and can lead immediately to something like a ret2usr.

Last updated

Was this helpful?