Wednesday, April 27, 2016

Iovyroot (CVE-2015-1805) 分析


        最近Iovyroot似乎非常火,趁着新鲜,这篇文章分析一下Iovyroot,以及对应的CVE-2015-1805


先介绍下iov


        在pipe里面,为了处理异步访问,定义了readvwritev两个syscall。在pipefops里面定义为aio_readaio_write。使用方法在Linux Manual里面有写:




        最主要的是iovec这个数据结构,这个可以在用户态定义,其中iov_base指向一个有读/写权限的用户态地址,iov_len指定向这个地址里读/写多少数据。

        iovcnt参数指定了iovec数组的长度。

        readvwritev定义在fs/read_write.c文件里,下面详细分析一下readvsyscallwritev的差不太多,感兴趣的读者可以自己看一下源码。

        readv里面会对fd做很多检查,首先调用到vfs_readv(),然后调用到do_readv_writev()。其中,会首先对用户态传进来的iovec数组做检查。然后再调用fops里注册的对应函数。对readv来说就是aio_read,也就是pipe_read()


        在rw_copy_check_uvector()函数里,首先会检查传入参数nr_segs(也就是iovcnt),是不是合法,如果nr_segs小于8,就使用栈上的iovstack;如果大于8,就kmalloc出一块新的内存作为iovstack。然后用copy_from_user()把用户态的iovec数组拷到内核态。这个iovstack会在后面调用read_pipe()的时候作为iov参数传入。

        最后,rw_copy_check_uvector()会检查iovstack里面的每个iovec结构体是不是合法的。



CVE-2015-1805



        这个CVE的说明在oss-security社区里有过讨论。有问题的代码在fs/pipe.c文件里的pipe_read()pipe_write()函数里:
pipe_read()函数为例,


        其中,pipe_iov_copy_to_user()的代码如下:


        其实,这个CVE包含两个洞,一个是race condition,还有个是越界访问。

先说race condition


        从上面pipe_read()的代码里可以看出来,如果pipe_iov_copy_to_user()函数返回非零,就会走到goto redo;的代码位置。(看了下iov_fault_in_pages_write()函数,正常情况下函数的返回值应该都是0,所以一般atomic都是非零的)

        再看pipe_iov_copy_to_user()函数里面,因为atomic是非零的,所以正常情况下都会先调用__copy_to_user_inatomic()函数来把pipe里面的数据写到iov里指向的用户空间地址里。如果__copy_to_user_inatomic()函数里面出错了,就会返回非零。根据上面的分析,在pipe_read()函数里就会retry一次,把atomic置零,然后重新调用pipe_iov_copy_to_user()。这时候就会调用常规的copy_to_user()来把数据拷到用户空间。最后,再把iov里面的iov_baseiov_len更新一下。

        __copy_to_user_inatomic()copy_tu_user()唯一的不同在于,__copy_to_user_inatomic()省去了access_ok()的检查。就是说,之前都已经检查过了要写的用户空间的地址是可以写的,就不用再检查一遍,直接把数据写过去。这样做应该是出于效率的考虑,可以拷的更快一点。但是,这里就存在一个问题,第一次调用pipe_iov_copy_to_user()函数的时候,函数里面对iov的数据做了更新,而在retry的时候,调用pipe_iov_copy_to_user()的传入参数并没有更新。于是,可以构造出来一个用户态的溢出。

        这个漏洞的触发方法也非常tricky,要有一个线程不停地对一个内存地址map,再unmap,然后要有另外个线程不停地去read pipe到这个地址,就有一定概率会race成功。当然还要有第三个线程不停地write那个pipe。但是就算成功了也就只有用户态的溢出。

再说越界访问


        仔细看pipe_iov_copy_to_user()的代码,参数里面lenpipe里面的数据的长度,min_t会挑出参数里面,对应类型,较小的那个值。只要每次循环,len都不等于0,而且每个iov_len都足够小,pipe里的数据又足够长的时候,就可以每次循环都让iov递增,最终可以触发越界访问。

        值得注意的是,这个iov是一个指向内核数据的指针。指针越界访问之后,同样也会调用__copy_to_user_inatomic()函数来复制数据,而这个函数又没有对iov_base指针做检查,于是就产生了内核层面的任意地址写。

        在前面的分析中提到,当传入的iovec数组大小超过8,就会在内核堆上分配空间。这样就可以利用堆喷来控制iov指针越界访问的数据。

Iovyroot


        在Iovyroot里面,同时实现了上面两个漏洞的代码,真正有利用价值的只有对越界访问的攻击,而对race condition的攻击实在是有点鸡肋,而且还降低root的成功率。(Check 【Update】)

        在内存布局上,Iovyrootsendmmsg来做堆喷,stop_flag指向用户态的数据,标志攻击成功,并停止攻击。target_ptr指向内核态能被调用的到的函数指针,用来做控制pc


        在堆喷的同时,调用readv,把iovstack布局到内核堆上,并触发漏洞。

        控制pc后,第一步是,泄漏sp,然后找到thread_info结构体,然后修改addr_limit。之后就可以用pipereadwrite在用户态任意地址读写。之后,在thread_info里找到task_structpatch里面的cred,这里面patch还有找initsid的过程可以参考源码:
        https://github.com/dosomder/iovyroot


        找init sid的方法和KEEN的方法不一样。http://www.slideshare.net/jiahongfang5/mosec2015-jfangKEEN是通过遍历task_struct找到init_task,然后再定位到里面的sid。而Iovyroot里是直接搜索selinuxsidtab里的sidtab_node链表,在policydbsym_val_to_name成员里找到对应的名字,如果是“init”就取出对应的sid

        最后,Iovyroot里还patch了两个内核里的全局变量,selinux_enabledselinux_enforcing,应该是用来过selinux的。

########################################

【Update】

        后来又仔细想了下发现之前分析的有点问题,所以又去看了下源码。发现在调用到pipe_iov_copy_to_user()之前有做个检查:



也就是说,允许拷贝出来的数据长度是不会大于iovec数组所有iov_len长度总和的。但是,要让iov越界,就必须要chars > total_len。所以,这里就要用前面的那个race condition来bypass。

        利用race condition可以产生一定长度的错位,然后可以顺利地让iov指针向下读到可控的内存区域。

No comments:

Post a Comment