TA的每日心情 | 无聊 2019-5-8 10:49 |
---|
签到天数: 26 天 [LV.4]偶尔看看III
|
本帖最后由 hkwljs 于 2016-2-10 17:53 编辑
作者:HK.幻客
上个月爆出的CVE-2016-0728 (http://www.ihonker.org/thread-7782-1-1.html)本地提权漏洞让大家的目光又一次聚焦在linux内核安全上。和CVE-2015-3636,CVE-2015-7312,CVE-2014-2851一样,CVE-2016-0728是一个Use-After-Free(UAF)类型的漏洞。我们知道,造成UAF的罪魁祸首是迷途指针(Dangling pointer )。已分配的内存释放之后,其指针并没有因为内存释放而变为NULL,而是继续指向已释放内存。然而,并不是所有的迷途指针都是危险的,其也分良性和恶性:
· 良性迷途指针:该指针不会再被使用。
· 恶性迷途指针:该指针还会被用来对已释放内存进行读写操作。
只有恶性迷途指针才能触发UAF(考虑到UAF 的定义)。 若非特别声明,本文中的迷途指针皆为恶性迷途指针,先看个例子
Struct Object1_struct{
int flag;
void (*func1)();
char message[256];
}OBJECT1;
Struct Object2_struct{
int flag;
int flag2;
char welcome[256];
}OBJECT2;
pObject1 = (OBJECT1 *) malloc( sizeof(OBJECT1));
// …initialization…
// …pass values…
// … use …
…
free(pObject1);
…
pObject2 = (OBJECT2 *) malloc( sizeof(OBJECT2));
…
if(pObject1 != NULL)
pObject1->pfunc1();
上面代码中,pObject1是一个迷途指针, 因为释放以后没有置为NULL,在后面的代码里面其指向的成员函数又被调用了一次。可能会导致以下的两种情况:
· 该内存空间虽然已经被系统回收,但是还没有被另作他用,里面的数据还是原有的数据。该调用可能返回正常结果。
· 该内存空间已经被挪为他用,假设碰巧存入了object2对象(此时pObject1,pObject2指向相同的地址)。注意新数据和原数据不是一个类型。此时调用pObject1->pfunc1 ();,会以object1的结构解读一个object2对象,从而让EIP跳转到一个奇怪的地址(地址为object2里flag2的值)。
如果flag2的值是一个不可执行的地址,系统会生成一个accessviolation错误。
如果flag2的值是一个可执行的地址,EIP会跳转并执行之。这样一来,攻击者如果有机会精心构造Object2里面的flag2,就可以成功的实现程序的跳转。
总结一下,利用一个UAF漏洞需要三个阶段:
1. 先搞出来一个迷途指针。
2. 用精心构造的数据填充被释放的内存区域。
3. 再次使用该指针,让填充的数据使EIP发生跳转。
如果内核里存在UAF漏洞,而且攻击者可以从用户空间触发以上3个步骤,那么攻击者就非常有机会在内核执行自己设计好的代码,从而获得权限提升。当然,难点是攻击者只能在用户空间通过有限的API操作内核对象。这是设计者制定的游戏规则,而我们就是要利用这些有限的游戏规则,触发漏洞,达到我们的目的:提权!
阶段1 生成迷途指针
Linux内核用垃圾回收机制释放内核对象,每个对象都有一个引用计数器(refcount)。在对象初始化的时候,refcount被设为1。当执行对象所涉及的操作时就让refcount加1,在操作结束时就让refcount减1;另外,当对象B被对象A引用时,对象B的refcount就加1,引用结束,refcount减1。当这个refcount变为0的时候,系统知道该对象不再被使用了,于是自动将其释放掉。例如,当一个文件系统还被安装在系统上时就不能将其释放,当这个文件系统不再被使用时,refcount变成了0,于是可以释放。以上操作可以这三个函数来简单表达。具体请参照https://www.kernel.org/doc/Documentation/kref.txt
void kref_init(k) k->refcount = 1;
void kref_get(k) k->refcount++;
void kref_put(k, release) if (!--k->refcount) release(k);
void release(k)
{
struct my_obj *obj = container_of(k, struct my_obj, refcount);
kfree(obj);
}
通过跟踪内核对象的使用情况,垃圾回收机制从某种意义上解决了UAF问题:只有当一个对象确定不再被使用了,才会被释放掉。也就是说,即使产生了一个迷途指针,那也是良性迷途指针,不会触发UAF。
然而,这个时候,对refcount的操作就至关重要了,试想,如果该加的时候没有加,不该减的时候却减了。那么refcount 有可能在对象还在使用的时候就不慎变成了0,从而导致了内存空间被释放掉,形成了恶性迷途指针。下面给两个这种情况的例子
该减的时候没有减,CVE-2016-0728 ( http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/)
这个漏洞利用了keyring对象
寻找名称为$name的keyring;
if (没找到)
{
创建一个名称为$name 的keyring;
kref_init(keyring) // refcount = 1;
}
else //找到了
kref_get(keyring) // refcount++;
if (找到的是本session正在使用的keyring)
goto error2
使用keyring
kref_put(keyring); // if(!--refcount) release(k);
error2:return
从以上的伪码,可以发现只要我们输入本session正在使用的keyring名,程序就会跳过kref_put(keyring); ,造成refcount只增不减。那我们怎么让refcount变成0呢?考虑到refcount是int类型,那么我们可以用整数溢出的方法:让重复运行以上代码0×100000000次,使refcount溢出,最终变成0。
说个题外话,CVE-2016-0728的作者声称此漏洞会影响66%的 android设备。可是触发该漏洞至少要重复调用keyctl API 42 亿次。。。即使Core i7的PC也要花半个小时才能跑完这42亿次,有同学在手机上跑了一晚上的进度为0.018%(http://p011ux.net/2016-01-27),那些 66%的android设备难道都是超级待机王?
不该减1的时候减了1, CVE-2015-3636 (https://www.blackhat.com/docs/us-15/materials/us-15-Xu-Ah-Universal-Android-Rooting-Is-Back-wp.pdf )
当用户用ICMP socket和AF_UNSPEC为参数,调用connect()时,系统会直接跳到disconnect(),删除当前sock对象的hash,并且让refcount递减一次,伪码如下。
在hash list里面寻找sock对象 sk 的hash
if (找到了)
{
从hash list中删除该hash;
kref_put(sk);
}
假设用户用相同的参数再调用一次connect(),此时if语句应该返回 FALSE,因为hash已经被删除了。然而,由于程序的bug, 在某个特定的情况下,此时实际会返回TRUE,导致refcount被多减了一次。因此,攻击者只需要创建一个ICMP socket,连续调用3个connect()(第一个connect()用来生成hash),就可以把refcount置为0,从而释放sock 对象。POC如下
int sockfd= socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); // refcount =1;
structsockaddr addr = { .sa_family = AF_INET };
int ret =connect(sockfd, &addr, sizeof(addr)); // refcount ++; 创建hash
structsockaddr _addr = { .sa_family = AF_UNSPEC };
ret =connect(sockfd, &_addr, sizeof(_addr)); //删除hash;refcount --;
ret =connect(sockfd, &_addr, sizeof(_addr)); // bug导致继续删除hash;refcount --; refcount 变成0
只要4个API就可以触发此漏洞,比CVE-2016-0728要实用的多。
阶段2 填充数据
在这个阶段,我们要用自己构造的数据填充上个阶段被释放的内存区域。同样,由于权限的限制,处在用户空间的我们没法直接把数据写入内存,只能利用API达到目的。
既然释放的内存是要被回收重用的,那么如果我们用API创建一个新的内核对象,而这个内核对象又“恰好”被分配到刚才释放的内存上,那不就达到我们的目的了吗?然而内存分配机制如此之复杂,新对象又怎样能如我们所愿的被分配到刚释放的内存上?这里就要说一下SLAB和SLUB。
SLAB是一种内存管理机制。为了提高效率,SLAB要求系统暂时保留已经释放的内核对象空间,以便下次申请时不需要再次初始化和分配。就比如农场主造了一个鸡笼用来放鸡,当笼子里的鸡被卖掉以后,农场主并不立即把鸡笼毁掉改作它用,而是留着用来装另一只鸡。然而,SLAB机制对内核对象的类型十分挑剔,只有类型和大小都完全一致的对象才能重用其空间,比如鸡笼只能装鸡,而不能装鸭子,尽管鸡和鸭子差不多大。这样我们释放掉sock对象A以后马上再创建sock对象B,那么B就极有可能重用A曾经占据的内存。这个机制的确帮助我们向目标迈近了一大步,可惜还不够完美。因为我们即使创建了sock对象B,也不一定有机会修改对象B的所有成员,从而达到写入任意数据的目的。
和SLAB相比,SLUB对对象类型就没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同。这样的话,同一个笼子既可以放鸡,又可以放鸭。也就是说我们释放掉sock对象A以后马上再创建对象B,只要A和B大小相同(不在乎B的类型),那么B就极有可能重用A的内存。既然B可以为任意对象类型,那我们当然希望选择一个用起来顺手的对象类型。至少要符合以下2个条件:
· 用户空间可以控制该对象的大小
· 用户空间可以对该对象任意写入数据
消息对象就是一个很好的例子,完全满足以上2个条件。我们可以在用户空间用msgsnd()或者sendmmsg() API构造内核消息对象B,只要消息大小加上消息头的大小等于对象A的大小, 那么这个消息很可能会重用A占据过的内存。CVE-2016-0728的POC用的就是这个方法。 (https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f)
阶段3
只要我们用“合理”的数据覆盖了内存,这个阶段就很简单了,我们只需要使用一下迷途指针,让EIP跳转到我们设计好的代码上即可。
还是用CVE-2016-0728举个例子,keyring对象包含一个函数指针,指向revoke()函数,目的是撤销一个keyring。用户空间可以用keyctl(KEY_REVOKE,key_name)这个API来调用revoke()。那么,当我们覆盖keyring对象的时候,我们可以控制指向revoke()的函数指针,让其指向我们准备好的提权代码。然而提权代码应该放在那里呢?
Return-to-User (ret2usr)
最简单的方法是把提权代码放在用户空间里。这样,修改了函数指针指向提权代码以后,我们就把EIP从内核劫持到了用户空间。这是最容易的一种方法,因为
· 攻击者对用户空间拥有完全控制权。
· 从用户空间进入内核有诸多的限制,而从内核进入用户空间几乎没有限制。
比如CVE-2016-0728的POC就在用户空间创建了一个假revoke函数,修改函数指针让EIP跳转到这个假revoke函数。
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred (0));
}
这个假revoke函数使用了非常传统的内核提权代码,通过调用commit_creds修改进程creds数据结构,uid = 0,gid = 0, 从而得到root权限。
然而github上非常多的评论都在抱怨该POC在实际测试中没有成功提权,为什么呢?
· 没有权限读取commit_creds 和prepare_kernel_cred的地址: 攻击者需要commit_creds和prepare_kernel_cred的地址来构造提权代码。其地址存储于/proc/kallsyms文件中,然而普通用户不一定能读取该文件。导致提权代码构造失败。
· 被SMAP, SMEP/PXN挡住了:刚才说过,“从用户空间进入内核有诸多的限制,而从内核进入用户空间几乎没有限制。”这句话5年前说还算成立。过去的经历让大家发现任由内核进入用户空间也会带来极大的安全隐患,所以提出了SMAP, SMEP/PXN机制。SMAP阻止内核随意访问用户空间的数据(只有RFLAGS.AC标志位为1的时候才可以)。而SMEP禁止内核执行用户空间的代码(PXN是ARM版本的SMEP)。目前大多数的主流系统都开始支持SMAP,SMEP/PXN。因此,ret2usr已是一种夕阳技术。(当然我们也可以修改启动选项,加入nosmep,nosmap参数来关闭SMAP,SMEP)
构造内核ROP(Return Oriented Programming)
SMEP/PXN只是阻止内核执行用户空间的代码,这么说我们依然可以让EIP在内核里任意跳转,也就是让内核执行内核代码。因此,我们可以使用内核ROP 来执行提权代码:寻找内核gadget(内核中现成的以RET结尾的代码片断),比如我们现在想实现moveax, ebx,那么就可以在内核里寻找 mox eax, ebx; ret; 假设我们在0xffffffff8143ae19找到了mox eax, ebx; ret,那么这个gadget就是:0xffffffff8143ae19。最后把挑选出来的gadget组合成ROP chain。
例如,commit_creds(prepare_kernel_cred(0)); 在x64下 (x64相比于x86,函数参数的传递不再完全依赖堆栈,其规定函数参数必须保存到寄存器中),等同于
Pop %rdi;ret
NULL
prepare_kernel_cred的地址
mov %rax,%rdi; ret
commit_creds的地址
我们可以在内核内存里找到对应的gadget, 将他们拼接起来。然后利用迷途指针让EIP跳转执行。具体如何利用linux内核ROP,请参照https://cyseclabs.com/page?n=17012016
利用physmap
Physmap是内核管理的一块非常大的连续的虚拟内存空间(在x64系统下,physmap 寻址空间可以达到64TB!)。为了提高效率,该空间地址和RAM地址直接映射。问题是,Physmap如此的大,而RAM相对要小得多,导致了任何一个RAM地址的可以在physmap中找到其对应的虚拟内存地址。另一方面,我们知道用户空间的虚拟内存也会映射到RAM。这就存在两个虚拟内存地址(一个在Physmap地址,一个在用户空间地址)映射到同一个RAM地址的情况。也就是说,我们在用户空间里创建的数据,代码很有可能映射到physmap空间。
基于这个理论,我们在用户空间可以用mmap()把提权代码映射到内存,然后再在physmap里找到其对应的副本,修改EIP跳到副本执行就可以了。因为physmap本身就是在内核空间里,所以SMAP,SMEP/PXN 都不会发挥作用。这个方法首次在ret2dir: RethinkingKernel Isolation一文中提出。
Own yourAndroid! Yet Another Universal Root一文中提出了physmap的新用法,其实该用法是实现阶段2,填充数据。 这里既然介绍了physmap, 就顺便提一下,该方法的中心思想是利用用户空间对physmap的映射来填充SLAB/SLUB。因为在内核空间里physmap和SLAB/SLUB靠得很近,攻击者先创建大量的sock对象,用来”抬高”SLAB/SLUB的地址,让新的sock对象被分配在physmap里。随后,攻击者再在用户空间里用大量调用mmap()把数据映射到physmap,得以覆盖sock对象。
1.CVE-2014-2851group_info UAF Exploitation,ttps://cyseclabs.com/page?n=02012016
2.LinuxKernel Rop, https://cyseclabs.com/page?n=17012016
3.ret2dir: Rethinking Kernel Isolation , http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf
4.ANALYSISAND EXPLOITATION OF A LINUX KERNEL VULNERABILITY (CVE-2016-0728) , http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/
5.PXN防护技术的研究与绕过, http://drops.wooyun.org/tips/7764
6.Ownyour Android! Yet Another Universal Root, https://www.blackhat.com/docs/us-15/materials/us-15-Xu-Ah-Universal-Android-Rooting-Is-Back-wp.pdf
* 作者:HK.幻客 |
|