想学习漏洞,但是博客资料看了一大堆,好像入门了,也好像没有入门,决定从一个漏洞入手,全面的分析一下。从零开始,我把所有我遇到的问题,以及解决或者绕过替代的方案跟大家分享一下。
作为一个新手,入手一个漏洞,首先要找个环境,测试漏洞是否存在,很不幸,如果顺利,我不会写环境搭建的问题。
首先百度搜的资料,i春情有一个讲解是,看雪也有一个大佬写过相关的资料了,我手上只有一个nexus6p,为了分析漏洞专门买了一个nexus 5x,都失败了,因为没有了最早的镜像文件,现在除非google源码编译6.0,否在,市面上的镜像基本修复过了,反正我是没有找到可以的,google官网已经把这部分有漏洞的rom下架了,而如果源码编译6.0(有驱动方面的问题),这部分的版本是要趟坑的,我嫌麻烦就没有做,而且这个漏洞是内核漏洞,不是android专有的,我完全可以编译一个linux内核啊,然后使用这个内核就可以了。
漏洞补丁日志
首先下载源码,因为这个漏洞是android和linux都有,所以我们下载android内核源码也可以,linux这个网站的源码也可以,一定要git下载,因为我们通过git回退到修复前的位置。
这个漏洞据说实在3.16以前的都有,反正版本那么多,随便下载吧,我也不懂,然后下载,我下载了一个3.16的(下载错了,搞了一大堆,找了找资料才知道,才想起来git回退的办法)。
回退到这个提交的前面的版本,回退了之后,好像回退到了一个什么游标的位置,git这个我也不是很懂,然后我直接编译,最好是ubuntu14.04,我编译的是64位的,直接能编译过。(坑:我电脑是ubuntu20,gcc9,编译的时候,什么位置无光,编译选项,c语言版本什么的报错都解决了,装了一个gcc 5 ,编译过了,但是编译的内核不能跑,后来桩docker 编译过的,懂的大神,能不能解释下为什么)
内核搞定了,本想直接替换ubuntu14的内核试试,结果,系统直接崩溃了,折腾了一下,最后找了个qemu模拟内核启动的方案,然后去学了根文件系统的制作,编译了一个busybox。
因为不太懂内核堆喷,所以去看了这个大神的博客,然后我直接把他搭建的环境,一起驱动那俩直接用了,我写的太简陋了,这里,感谢大神,非常感谢。
漏洞代码
这个大神也很感谢,我看了不下20遍,我把他的代码拿过来了,可以他是32位的,而且是arm,天真的我以为小改一下就能跑
还有这个代码,他们的区别我下面会讲,这个工具,好像是个著名的提权工具
漏洞描述什么的,不好意思,看不懂,代码修改完了,提权不了,我去,蒙b了,心碎了。
第二天,天晴了,雨停了,我又行了。开始从头分析
readv函数,是一种高级I/O函数,就是加快读写速度的,每种I/O驱动都可以实现,也可以不实现,如果不实现,应该就没法使用readv。
这里使用pipe管道驱动的readv函数,最后调用到了pipe_read,这个函数有堆数组越界,导致任意读写,然后在进行一系列堆喷,溢出计算,等等看不懂的操作,据说就能提权,我们先看readv函数调用顺序。
(do_sync_readv_writev/do_loop_readv_writev ,这两个函数我没有具体研究。。。。)
代码我还是贴上吧,里面的注释,如果看不懂,可以看完在慢慢分析
我们通过这个函数来分析,为什么产生漏洞,搞清楚,不是如何利用,如何进行任意地址写,只是分析为什么会产生
如果造成 atomic=1,pipe_iov_copy_to_user执行错误返回,这个时候,iov数组就会已经copy过的项的iov_len已经等于0了,但是这个时候,函数的逻辑是将atomic=0,然后继续goto到这个函数的位置,继续copy,就从这个地方看,再次进入iov函数,只有atomic和iov的数据产生了变化,拷贝的个数chars和源地址addr + buf->offset均未发生变化,不考虑漏洞的问题,这本身就是bug了,内存拷贝,目的地址变小了,但是拷贝的个数和源缓冲区大小都没变的情况下,必然导致溢出。
溢出的位置是哪里,我们看这个函数,iov是否数组,但是是通过指针使用的,如果要写入的数据不为0,iov数组就会越界,直到len小于0为止
这个漏洞是拷贝数据函数是pipe_iov_copy_to_user里面的__copy_to_user_inatomic,要达到地任意址写入任意大小的任意数据,我们必须同时控制他的三个参数(iov->iov_base,from, copy),copy大部分情况下等于iov->iov_le,from,是我们要读的缓冲区的数据,也就是我们写进来的数据,通过readv读出去。而iov是会发生数组越界的。
我们现在需要跟踪分析这个iov的前世今生了。看看能不能控制他。
iov数组,只要大于8组,就可以在rw_copy_check_uvector这个函数通过kmalloc分配,然后将用户的数据拷贝过来的,小于8组看上个函数
64位内核,struct iovec大小=16,也就是说,我们传入多少个,iov的个数,拷贝到内核的数据,就是16*传入的个数
我们都知道这是个堆数组越界,但是我不这么分析,漏洞手段千百中,为什么这个漏洞要用堆喷那。
我们是要控制iov数组中的数据,让他达到任意写的目的,第一个想法,直接写进来,通过用户空间的iov传入(原谅我的天真和无知),因为这个iov是复制了我们用户空间传入的iov,我们直接在用户空间构造好iov行不行。抱歉,真不行!!!你以为写内核的大佬比你还菜吗!!
如果你传入类内核的地址,好像也可以啊,我试了一下,pipe_read都跑不到,你就被干掉了,就在rw_copy_check_uvector,这一行
如果你传入内核地址,让他copy这里是过不了,所以这个方案,是不可行,也就是说,我们必须想办法,过掉这个函数的检测。而这个函数是基于数组的检测,也就是说,你传进来多少个数组,他就检测多少个,并且他的copy是基于你数组的大小的,多余的数据你是没办法传进来的,那就只能堆喷了,因为这个数组是在堆里面,而且溢出了,我们想办法让他溢出到我们的想让他溢出的数据上就行了,这样我们就能控制iov数组的数据了。
原理分析完了,该干活了(我是反向分析的,也就是说,上面写的,是我代码调试中,一步一步得出的结论,最后我总结了一个这个漏洞各个知识点的串联,不只问结果,更加问过程,为什么这样做,如果不明白为什么,不能举一反三,根本就是不会)
通过上面的知识点,我们得出结论
1.写入的数据,就是要改变的地址的内容,是源缓冲区的内容。
2.必须构造参数导致iov溢出,堆溢出
3.堆喷
太多没用的我就不写了,看雪大神另一篇博客上写的很清楚,我在写显得太罗嗦了。有些大神可能说了,有些可能觉得太简单了,就没写。我把一些边边角角的补充一下。
1.写入数据,这个为什么可以控制,如何控制,这个可以看,pipe驱动别的相关代码,pipe缓冲区,是个环形缓冲区,申请了16页的内存,也一种16页的内存这个,这个好象是可以调整的
这条命令可以查看自己电脑上的pipe相关信息。
2.如何构造iov溢出,这个大神已经写过了,ummap就可以了,这个后续调试我会写遇到的问题,但是不只是ummap,必须写够4096个字节以上才行,因为,我们必须要调用三次pipe_iov_copy_to_user,如果没有读过4096可能一次就copy完了,即使中间ummap报错goto一次,也是两次,而第二次,atomic是等于0的,会导致写错误。理论上来说多几次应该也可以的。
3.要进行内核堆喷,就要了解内核堆,了解内核内存分配机制slub这一类的,可以看下下面的博客
https://blog.csdn.net/FreeeLinux/article/details/54754752
我用kmalloc进行过测试,申请相同大小的内存,内存之间是没有缝隙的,只限于组与组之间。而且内存申请释放之后,如果再次申请到相同的内存,如果没有人用过这个页面,数据极有可能没有改变。所以这里就出现了两种堆喷的方式,
(1)看雪大神,用的是,申请4k的一整个页面,数组在这个页里面全铺满,数组的溢出,就是在这一个页的相邻的下一个页,由于内核堆组内数据之间没有缝隙,可以申请很多页,总能碰到下一页是我申请的数据的情况。
(2)iovyroot,用的是堆块使用玩后不清理,再次申请的数据跟上次一样的机制,但是他使用一页,稍微大几个字节,这样内存申请内存的时候,一样返回的是两页,但是copy到内核内存中的字节却只有一页大几个字节,然后将iov溢出的数据,填在copy到内存中的数据的大小的后面,进行堆喷,这样,如果内核刚好申请到他的堆喷的页面,只copy了1页多几个自己的数据,剩下的数据,还是原来的,就可以用来控制堆喷了。
第二中方式,在这里可能更好一下,第一种方法,应该也会有一点点第二种方法的概率在里面,但是成功率,没有第二个高
环境上面写了,内核下载下来以后,git回退到了这个漏洞修复前的位置,内核突然变成3.14版本了。。。。
既然有源码,我这么菜,不如调试一下,改一下pipe驱动的代码。
问题1:我修改了pipe驱动,在pipe_read函数中增加了printk,但是在内核跑得时候,却打印不出来,没有任何显示
问题2:gdb调试内核跑飞,如何编译一个O0的内核,便于调试,我按照网上的单独增加pipe.o的编译标识未-O0还是没啥反映。
方法总比困难的,希望有大神,能解释一下我上面提出的问题,后来我又写了模仿pipe直接写了一个驱动。
wirte函数,我只设置了一个页的缓冲区,写入一次就行,多次使用,read函数只是实现了驱动的漏洞部分的代码,如果调试驱动有困难,可以试一试我这种方法。
1.堆喷,iov这个内存块,如果使用第一种方式堆喷,可以看一下他是什么时候释放的,我测试的时候,iov在使用完后释放了,单线程,完全没办法造成堆喷,也是我只跑了一个内核,任务少的原因,每次iov的地址,基本上跟上次地址都是相同的,想要进行堆喷,必须进行多线程。
2.munmap和mmap的时机问题,大神博客基本写过了,但是实际调试过程中,却发现还是有问题的,可能时间有些出入,每台机器毕竟都不相同,运行的时候
线程1 线程2 线程3
iov_fault_in_pages_write
unmap
pipe_iov_copy_to_user
mmap
然后goto调用下一次pipe_iov_copy_to_user
堆喷完成,数据布置位置准确
调用第三次pipe_iov_copy_to_user
我曾经双线程,就调试这一个问题,最后还是没办法把握好时间处理成功
3.x86有cr0位有内核写保护,我太傻了,还以为直接能跑,第16位,数组那种,0是第一位,而且,设一次不好使,cpu环境一旦切换还是不行,这好象是个基本问题吧。。。。但是我确实在这死过好几天。
native_write_cr0(0x80040033); 和 native_read_cr0();
这两个我是分析内核源码找到的。。
4.提权使用的是sys_call_table,由于是自己编译的内核,我是直接写死的地址,但是我去,调用号完全不一样,我去内核源码找,也没找到正常的,反正跟真实的都不对,最后我去查看sys_call_table的内存,然后一行一行的对函数地址才找到的。
5.gdb调试内核,多线程不好调试,动不动飞了 最后我用了set scheduler-locking step ,然后不跑了,关于这个命令,网上说的不是很明白,有没有大神,能给我通俗易懂的解释一下
我实现的驱动代码:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!