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指针向下读到可控的内存区域。

Wednesday, April 20, 2016

CVE-2015-3864 libstagefright攻击学习


        CVE-2015-3864libstagefright.so里的一个整形溢出漏洞,在Google ProjectZero的博客里已经写的非常详细了。现在接着这个机会学习一下Arm架构以及安卓的系统,然后把这个利用移植到Samsung I9023手机上(Android版本4.1.2)。这篇博客就记录下我自己走过的坑,写点google的博客里没提到的。顺便推荐一个网站,www.androidxref.com,有各个版本的Android源码,不用自己去下了,非常方便。

        先简单介绍下背景。具体漏洞位置是在解析mp4文件的MPEG4Extractor::ParseChunk()函数里,处理tx3g标签时的一个整形溢出,进一步导致堆溢出。在4.1.2的安卓版本里,这个地方的代码还不太一样,少了一个判断语句,应该算是更早之前的一个什么洞。不过问题也不是很大,利用过程都大同小异,没太大差别。



        然后利用方法就是Google博客里写的,先堆喷,再挖空,然后把可控数据结构填进去,再触发漏洞,形成越界写,覆盖下一结构的虚表,进而控制PC。大体思路是这样的。但是4.1.2版本的libstagefright的代码不太一样。似乎是太老的缘故,对很多标签的解析都没有,所以还要重新去找。

堆喷


        Google博客上说的堆喷用的是对‘pssh’标签解析,但在4.1.2上没有实现pssh标签的解析。经过一番查找,我找到另外两个适合做堆喷的标签,一个是‘mdat’,还有个是‘covr’


        在解析mdat标签的代码里,有一个parseDrmSINF()函数,里面有个可控的循环可以不停地分配可控大小的堆块。乍一看感觉非常理想。结果后来调试的时候发现一直跑不到那个函数。一看才发现是前面的一个mIsDrm标志是空的。再仔细往下看,发现这个是和DRM(数字版权管理)有关系。具体细节后面再讲。总之最后得出结论是用不了。



        然后是covr标签,有个内存泄漏。New了之后没有做delete。可以做堆喷。



关于DRM


        要想置位mIsDrm标记,似乎唯一的机会就是在MediaExtractor::Create()函数里。



        在MediaExtractor::Create()函数里,首先会调用一系列注册好了的sniff函数。Sniff函数的作用是检查这个文件到底是什么格式的,每过一个sniff函数就会更新一个confidence值,confidence值最高的那个就是识别出来的文件格式。然后MediaExtractor::Create()函数会根据不同的文件格式,分发到不同的Extractor函数里。


        所以,关键是在SniffDRM函数里要让这个文件通过。根据代码,要通过SniffDRM关键似乎是在一开始的source->DrmInitialization()里面,然后会进一步地调用到ChromiumHTTPDataSource::DrmInitialization()。然后这个函数里会对mDrmManagerClient->openDecryptSession()函数的返回值进行判断。而,这个函数,经过一系列调用,会走到DrmEngineBase::openDecryptSession(),最后调用 onOpenDecryptSession()openDecryptSession() 定义成虚函数,经过一番查找,最后发现所有的openDecryptSession()定义的地方都是直接返回DRM_ERROR_CANNOT_HANDLE,也就是说Android framework里面默认是不对DRM进行处理的。Framework里就定义了这样个接口,开发者可以去继承这个类,实现这个接口。后来又看到了一篇博客,于是便释然了。

        结论就是,在原生的Android framework上不能用DRM,有mIsDrm标志的条件跳转一定是Null的。不过,可能有些别的厂商实现了DRM的功能,就可以用了。

关于Heap Grooming


        在Google的利用代码里还用到了‘hvcC’标签,这个在4.1.2里也是没有的。不过替代的方案很多,有很多标签用setCString()设置字符串的都可以用,不过在我的代码里用的是和hvcCavcC比较类似的esds标签。他们都是用setData()函数的。

        关于setData()函数,内部是一个对KeyedVector的操作,一个key对应一个value。当setData()key值已经存在时,setData()会先把key对于的value的内存地址free掉,然后再malloc一块新的value大小的内存,然后再memcpy把数据拷过去。setCString()函数内部就是一个重新封装了的setData()

还有关于堆喷的


        在covr标签的代码里,最后也会有一个setData()函数,不过只要每次堆喷的大小一样就不影响布局了。

        攻击过程还是通过浏览器实现的,所以堆喷的时候还不能喷地太多,否则程序会提前中止。NorthBitMetaphor里提到用XMLHTTPRequest来请求,可以绕过这个限制。


        不过,我最后还是手工地选取堆喷地址,最后也能得到一个稳定的PC control

关于Info Leak


        还是NorthBitMetaphor,里面提到4.4以下的版本没办法Info Leak。我又自己去看了下源码,确实是这样,原因还是在那个DRMflag。然后我看了下那个接口的onTransact代码,似乎是没有别的办法做Info Leak了。

最后


        ROP阶段我用的是ROPGadget,找gadget非常方便。

        写shellcode参考的是promised_lu的文章http://bbs.pediy.com/showthread.php?t=155774

        最后,因为没办法Info Leak,所以还是用Google的方法,以1/256的概率撞libc地址。测下来大概跑700次能成1次。

        我写的攻击源码在这里https://github.com/HighW4y2H3ll/libstagefrightExploit