k1R4

A "real world" exploit [CVE-2022-0185]

21 Aug 2023

You can find the commented exploit script here

Preface

I have been learning kernel exploitation over the past year. I mostly worked on past CTF challenges and attempted some during active CTFs as well. I briefly tried some Windows kernel exploitation through HEVD, but I kept coming back to linux since I found it fun to work with. Given all this, I wanted to know what it would take to go from solving kernel challenges in CTFs to writing real exploits. Kyle from shellphish, who has been guiding me recently, suggested that I try CVE-2022-0185. The reason he recommended this CVE was since it was a fairly simple bug, easy to exploit and is well documented. Hence the reason for the qoutes on “real world” in the title. Before we begin, huge thanks to Kyle for guiding me and motivating me to write this blog post.

CVE-2022-0185

This CVE was actually found by a popular CTF team, Crusaders of Rust. FizzBuzz101, from CoR, has an amazing writeup covering this CVE and their exploit for it. I used that as my primary reference when writing this exploit. I chose to write my exploit for Ubuntu 20.04, running 5.11.0-44 since the original writeup used the same kernel.

Some quick tips:

  • Old kernel images can be obtained using: sudo apt install linux-image-<version>-generic
  • vmlinux can be obtained by adding the ddebs repo and running: sudo apt-get install linux-image-<version>-dbgsym
  • They can be found under /boot and /usr/lib/debug/boot/ respectively

The bug is a heap overflow in kmalloc-4k occuring in legacy_parse_param(), which can be triggered from userspace using fsconfig() syscall. The original writeup describes the bug and how it can be triggered in detail, so I won’t be covering that. However I will be going through the exploit and the proces of how I managed to get it to work on a live system, having only dealt with minimal instances on QEMU with no system noise.

Getting leaks

First off we have to beat KASLR. We need code leaks in order to do that. Here we have a vulnerability that lets us overflow into the next adjacent page from a kmalloc-4k chunk. Therefore its better if we target objects in kmalloc-4k with critical members at the start of the object. The obvious choice here is to use the msg_msg object. Writeups by d3v17 and Alexander Popov go into the specifics of abusing msg_msg for arbitrary r/w. It is recommended to go through one of them before proceeding here.

In order to get an address leak,

  • Spray seq_operations objects in kmalloc-32 to be used later for leaking
  • Use msgsnd() to allocate msg_msg in kmalloc-4k and corresponding msg_msgseg in kmalloc-32
  • Use fsconfig() to overflow to adjacent msg_msg object and overwrite the m_ts member
  • Try msgrcv() with MSG_COPY flag to prevent the msg objects from being cleaned and potentially causing a panic
  • If successful, msgrcv() will copy OOB bytes, containing code address from neighboring seq_operations objects
     struct msg_msg {
     struct list_head m_list;
     long m_type;
     size_t m_ts;		/* message text size */
     struct msg_msgseg *next;
     void *security;
     /* the actual message follows immediately */
    };
    

Similarly for a heap leak,

  • Spray simple_xattr objects in kmalloc-64 with name member having pointers to kmalloc-32. This is done by using setxattr() on a tmpfs
  • This time have the corresponding msg_msgseg in kmalloc-64 instead
  • Trigger the overflow and overwrite size member of msg_msg
  • Use the OOB read to leak name member of neighboring simple_xattr which is a heap address pointing to an active kmalloc-32 slab
struct simple_xattr {
	struct list_head list;
	char *name;
	size_t size;
	char value[];
};

Pwning

The goal here would be a modprobe_path overwrite since it would be the simplest. The reference I mentioned earlier went the route of using msg_msg to achieve arbitrary write but I wanted to try something different. Kyle suggested that I try using the unlinking of simple_xattr. I must note that this technique might not work in newer kernels, since simple_xattr now uses rbtree instead of a doubly linked list.

I referred to this writeup from starlabs to abuse the unlink. The difference here is that overwriting the list_head causes the next member, name to be overwritten, since overflow ends with the bytes “,\x00”. removexattr() causes the name pointer to be freed. Having an invalid pointer would cause kernel panic. In order to avoid that we overwrite that pointer with the previously leaked name pointer. Also heap leak is used here instead of physmap leak.

In short,

  • Spray msg_msg objects in kmalloc-4k to occupy pages which helped increase reliability
  • Use setxattr() to allocate a simple_xattr object in kmalloc-4k
  • Use fsconfig() to overflow and overwrite list_head pointers to modprobe_path & 0xffxxxxxx2f706d74 and name to leaked heap address
  • Try removexattr() which will trigger unlink in simple_xattr,
    • The unknown bytes in 0xffxxxxxx2f706d74 is decided based on heap leak, but physmap could be used here as well
    • prev = modprobe_path+1 and next = 0xffxxxxxx2f706d74
    • next->prev = prev doesn’t help here so it can be ignored. Although this is why we need next to be a writeable address.
    • prev->next = next translates to *(u64 *)(modprobe_path+1) = 0xffxxxxxx2f706d74
    • So /sbin/modprobe is overwritten to /tmp/xxxprobe
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	WRITE_ONCE(prev->next, next);
}

Barebones to a live system

I initially wrote the exploit by copying the kernel image and using a CTF challenge like setup with qemu and a minimal rootfs. This was a good decision in hindsight. Debugging a standard installation of Ubuntu on a VM through kgdb was too slow. Maybe using kgdboe might’ve been faster but either way rebooting the VM would’ve been a hassle. This definitely sped up the whole process and I will probably follow this until I find a less janky method that is at least equally as fast.

First, I started off by running the exploit on a live Ubuntu VM running on VMWare Workstation and to nobody’s surprise it failed. I didn’t even get a kernel panic after running it multiple times. Having worked with only ctf challenges running on qemu with no system noise, I was using a strategy of spray and pray. I sprayed objects, triggered the bug and hoped for a successful run.

After reading some CVE writeups, I noticed that usually the exploit continously tries to trigger the bug waiting for a successful run. Although this increases chances of a successful hit, it also has chances of causing a panic. Spraying objects appropriately and grooming the slabs will decrease chances of a panic. This requires some tweaking and fiddling. I ended up doing it like a caveman, by trial and error. There are probably much better ways to do.

In the end, I had a fairly reliable exploit that works about 9/10 times on an Ubuntu VM with varying amounts of system noise and memory.

Conclusion

I learnt a lot from writing the exploit but with upcoming mitigations like CONFIG_RANDOM_KMALLOC_CACHES, CONFIG_SLAB_VIRTUAL and common objects being moved to kmalloc-cg caches, exploitation is becoming harder. The techniques used in this blog are already probably obsolete. I plan on working with newer CVEs to learn new techniques as well as explore eBPF and networking stack of linux.