First kernel pwn challenge that I almost managed to solve but didn't manage to. I will walkthrough the process of setting up the environment for kernel pwn and how to solve the challenge
Challenge Files
bzImage - the compressed version of the linux kernel
vmlinux - compiled kernel with full metadata (used for debugging and analysis)
rootfs.ext3 - the file system of the virtualized Linux system
This is the script to run an emulated linux system. From the arguments given, there are no security protections enabled so we dont need to worry about bypassing KASLR, KPTI and etc.
Setting up the Environment
Debugging the Kernel
We can make a copy of run.sh into another file called debug.sh and modify the following line so that we can debug the kernel
qemu-system-x86_64 -s -S\
The -s flag opens a gdb server on port 1234 while -S tells qemu to pause at the start
gdb ./vmlinux
./debug.sh
target remote :1234
Next, run gdb with the kernel image. On a split screen, run debug.sh and go back to gdb to run target remote :1234
Compiling the Exploit
Normally, you would want to statically link the binary
Add the line source ~/tools/gef-kernel/gef.py to your .gdbinit file but chage the path to where you have the file
Analysis
static long obj_alloc(void) {
if (obj != NULL) {
return -1;
}
obj = kzalloc(sizeof(struct obj), GFP_KERNEL);
if (obj == NULL) {
return -1;
}
return 0;
}
static long obj_write(char *data, size_t size) {
if (obj == NULL || size > OBJ_SIZE) {
return -1;
}
if (copy_from_user(obj->buf, data, size) != 0) {
return -1;
}
return 0;
}
static long obj_free(void) {
kfree(obj);
return 0;
}
Based on these 3 functions, there is an obvious Use-After-Free (UAF) on obj_free() function we are able to write to the freed chunk. But we can only alloc the chunk once with obj_alloc() because obj is never set to NULL. The chunk that we alloc is of size 0x20 so we can only overwrite structs in the kmalloc-32 cache.
SLUB Allocator
The SLUB Allocator is kind of like the heap manager but for kernel land. The memory here is referred to as a slab not a chunk . Once a slab is freed, it is stored into a slab cache. There are many different types of slab caches such as kmalloc-64 which stores slabs of size 64 bytes, kmalloc-128 which stores slabs of size 128 bytes and etc etc. For our scenario, we will be talking advantage of kmalloc-32
Heap Sharing
Unlike userland pwn where there is a heap memory for each program, in kernel land the heap memory is shared by all kernel modules and drivers. Hence, if you somehow corrupt data in the heap, it might cause other drivers to fail aswell. Since the heap is shared between all the modules, the state of the heap is very unpredictable which is why Heap Spraying is a very useful technique in kernel pwn.
Exploitation
Getting address of kernel symbols
To read the kernel symbols, we first need to be a root user. To do this, we could look into the /etc directory in the given filesystem and look rcS or inittab . In our challenge it was located in the root directory
You need to modify the line to setsid cttyhack setuidgid 0 sh to become root user. Now that we are root, we can start reading symbols from kallsyms.
But from kernel version 6.2, we cannot just pass NULL to prepare_kernel_cred. Instead, we need to use commit_creds(&init_cred)
~ # cat /proc/kallsyms | grep commit_creds
ffffffff812a1040 T __pfx_commit_creds
ffffffff812a1050 T commit_creds
Next, we need to find the address of init_cred . We cannot just find it in kallsyms because its struct in memory, not a symbol. We can find it in gdb like this
(remote) gef➤ info addr init_cred
Symbol "init_cred" is static storage at address 0xffffffff81e3bfa0.
(remote) gef➤
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
void *data)
{
struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
int res = -ENOMEM;
if (op) {
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
op->show = show;
res = seq_open(file, op);
if (!res)
((struct seq_file *)file->private_data)->private = data;
else
kfree(op);
}
return res;
}
EXPORT_SYMBOL(single_open);
When you perform a read() on the file descriptor returned by this, it will call the function pointer at seq_operations->start . At this point, we can call open("/proc/self/stat", O_RDONLY) to reallocate our freed chunk and use the obj_write() to overwrite the start pointer and call read() on the file descriptor which will execute seq_operations->start
Ret2usr
When returning to userland, we cannot just simply pop a shell. We need to restore the userland registers. There are 5 userland registers stored on the stack that need to be setup in the order RIP > CS > RFLAGS > RSP > SS . A clever way of doing this is just saving the state of the registers before going into kernel mode with something like this
This is the solve script provided by the authors. After restoring the state, we will set RIP to point to our win function. However, it should be noted that CONFIG_SLAB_FREELIST_RANDOM was disabled for this challenge which is why the slub allocator returns the slab in LIFO order. Hence, there's no need for heap spraying.
However, when sending the payload to the server, the statically linked binary is large and might be slow when uploading to the server. You can use to create lightweight binaries
There is a special struct called seq_operations which is of size 0x20. You can read the source code in .
According to this , when open("/proc/self/stat", 0); is executed in user space, the kernel calls the single_open() function, where it allocates a memory space of size 0x20 for the seq_operations structure