| 首页 > 新闻公告 > 公告详情
CVE-2016-8655内核竞争条件漏洞调试分析
2016-12-19

      12月5日,hilipPettersson公布了一枚已存在Linux kernel长达5年的本地提权漏洞,几乎影响所有Linux主流发行版本,一时风头无二,绝不亚于前段时间的“Dirty Cow”。对于这个黑魔法漏洞,只有走进源码才不会管中窥豹,正是源于好奇,于是开始了整个漏洞的调试和分析。

       漏洞调试是乏味的,建议大家在调试的过程中,保持心静,捋清步骤,逐渐展开。

       感谢cyg07的敦促和指导!

       作者:张开翔 360 Gear Team成员


漏洞技术分析




1、漏洞描述

漏洞作者:hilipPettersson(philip.pettersson@gmail.com

漏洞危害:低权限用户提权到root

影响范围:Linuxkernel version < 4.8.13

修复方案:Linuxkernel升级到最新版本(>=4.8.13)

官方补丁:https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=84ac7260236a49c79eede91617700174c2c19b0cLinuxkernel(net/packet/af_packet.c)代码中存在条件竞争,可造成低权限用户提权到root。漏洞利用要创建原始套接字,需具备CAP_NET_RAW能力。 然而在某些特定的Linux发行版本(Ubuntu、Fedora)允许非特权用户创建的网络命名空间具备该能力,可成功利用。

2、漏洞成因

 packet_set_ring函数在创建ringbuffer的时候,如果packet版本为TPACKET_V3,则会初始化struct timer_list,如下图所示。packet_set_ring函数返回之前,其他线程可调用setsockopt函数将packet版本设定为TPACKET_V1。前面初始化的timer未在内核队列中被注销,timer过期时触发struct timer_list中回调函数的执行,形成UAF漏洞。


switch(po->tp_version){

case TPACKET_V3:

/* Transmit path isnot supported. We checked

* it above but justbeing paranoid

*/

if(!tx_ring)

init_prb_bdqc(po, rb, pg_vec, req_u);

         break;

         default:

         break;

}


当套接字关闭,packet_set_ring函数会再次被调用,如果packet的版本大于TPACKET_V2,内核会在队列中注销掉先前的这个定时器,如下图所示。


if(closing &&(po->tp_version > TPACKET_V2)){

         /* Because we don't support block-based V3 on tx-ring */

         if(!tx_ring)

                   prb_shutdown_retire_blk_timer(po, rb_queue);

}


但是packet版本被更改为TPACKET_V1后,原本的执行流程就发生了改变,使得 prb_shutdown_retire_blk_timer()函数不被执行,timer结构体也没有在内核的队列中被注销,一旦timer过期,内核就会执行相应的处理函数。timer的类型是struct timer_list,定义如下:


struct timer_list {

         /*     * All fields that change during normal runtimegrouped to the

          * same cacheline  */

         struct hlist_node        entry;

         unsignedlong             expires;

         void                    (*function)(unsignedlong);

         unsignedlong             data;

         u32                     flags;

         int                       slack;

#ifdefCONFIG_TIMER_STATS

         int                       start_pid;

         void                    *start_site;

         char                    start_comm[16];

#endif

#ifdefCONFIG_LOCKDEP

         struct lockdep_maplockdep_map;

#endif

};


3、漏洞利用

漏洞利用的本质是通过触发竞态,使得内核不注销socket内部的timer,然后使用内存喷射的方法覆盖timer未释放前的内核空间,将timer内部的函数替换成想要执行的函数,一旦timer时间过期,内核就会执行timer的过期处理函数,实际就是替换后的函数,从而达到漏洞利用的目的。触发竞态的代码如下:


void*vers_switcher(void*arg)

{

    int val,x,y;

    while(barrier){}

    while(1){

        val = TPACKET_V1;

        x = setsockopt(sfd, SOL_PACKET, PACKET_VERSION,&val,sizeof(val));

        y++;

        if(x !=0)break;

        val = TPACKET_V3;

        x = setsockopt(sfd, SOL_PACKET, PACKET_VERSION,&val,sizeof(val));

        if(x !=0)break;

        y++;

    }

    fprintf(stderr,"version switcher stopping, x = %d (y = %d, last val = %d)\n",x,y,val);

    vers_switcher_done =1;

    returnNULL;

}


漏洞作者公布了POC,利用步骤分为三个阶段:第一阶段是准备struct ctl_table数据,并将它存放在vsyscall页,作为register_sysctl_table()函数的调用参数。具体方法是首先将vsyscall页设置成可写属性页,可通过调用set_memory_rw(syscall_page)来完成,然后再修改页内容,填充为精心构造的structctl_table结构体的数据,将该结构体的核心成员.data设置为moprobe_path。

timer覆盖前数据:


timer覆盖后数据:

     

      第二阶段是注册sysctl条目,指定内核参数为modprobe_path。具体方法是通过调用register_sysctl_table()函数来完成,调用参数就是上一阶段构造好的struct ctl_table数据,调用成功后“/proc/sys”目录下会新生成名为ctl_table. Procname的文件。此后通过改写该文件就能动态替换modprobe程序。

     

     上述过程完成了两个功能,所以触发和利用漏洞也需要成功地进行两次,但如何将准确替换先前socket内部的timer呢?

     堆喷射,通过循环调用add_key()函数(作者称此函数最稳定)在内核中分配内存,使用精心构造的exploit buffer来填充,内核通过papcket_create()函数创建socket的结构数据struct sock,大小为1408字节。

      但是POC调用kmalloc()函数时指定的payload长度为1384字节,为何?因为内核在调用add_key()函数时会先创建structuser_key_payload结构体,其大小为24字节,然后在它后面复制用户态payload数据,相关代码如下:


struct callback_head {

         structcallback_head *next;

         void(*func)(structcallback_head *head);

} __attribute__((aligned(sizeof(void*))));

#define rcu_headcallback_head

¡­ ¡­

struct user_key_payload {

         structrcu_head         rcu;            /* RCU destructor */

         unsignedshort  datalen;            /* length of this data */

         char           data[0];                     /* actual data */

};

int user_preparse(structkey_preparsed_payload *prep)

{

         structuser_key_payload *upayload;

         size_t datalen = prep->datalen;

         if(datalen <=< span="">0|| datalen>32767||!prep->data)

                   return-EINVAL;

         upayload = kmalloc(sizeof(*upayload)+ datalen, GFP_KERNEL);

         if(!upayload)

                   return-ENOMEM;

         /* attach the data */

         prep->quotalen = datalen;

         prep->payload.data[0]= upayload;

         upayload->datalen = datalen;

         memcpy(upayload->data, prep->data, datalen);

         return0;

}


       此外,POC中构造的timer相对于exploitbuf的偏移为0x35e字节,如何得到?因为structuser_key_payload中成员data相对于结构体首地址的偏移为0x12字节,两者相加得0x370字节,恰好是timer在struct packet_sock结构体中的偏移,覆盖前和覆盖后的内存对比如下图所示。

第三阶段创建root shell。具体方法是首先更改“/proc/sys/hack”文件内容,达到替换modprobe程序的目的,然后触发内核执行替换后的modprobe程序,如何做到呢?POC通过调用socket()函数引用未被内核加载的网络驱动模块实现,如果内核当前未加载此模块,就会使用modprobe程序来加载它从而达到目的。其实,替换后的modprobe程序就是POC程序,当被内核执行时会更改文件属主为root,添加S权限位。最后,当普通用户再执行POC程序时它会创建root shell。


 readlink("/proc/self/exe",(char*)&buf,256);

 write(fd,buf,strlen(buf)+1);

 socket(AF_INET,SOCK_STREAM,132);


下图是完整的执行结果:


4、参考引用

http://securityaffairs.co/wordpress/54168/hacking/cve-2016-8655-linux-kernel.html

http://seclists.org/oss-sec/2016/q4/607

https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=84ac7260236a49c79eede91617700174c2c19b0c