作者:k0shl;
作者博客:https://whereisk0shl.topioctlbf;
项目地址:https://github.com/koutto/ioctlbfkDriver Fuzzer;
项目地址:https://github.com/k0keoyo/kDriver-Fuzzer
kDriver Fuzzer使用说明
首先感谢ioctlbf框架作者,我在这半年的时间阅读调试了很多优秀的fuzzer,受益良多,自己也有了很多想法,正在逐步实现。同时当我调试ioctlbf的时候发现了一些问题,于是基于ioctlbf框架,加了一些自己的想法在里面,有了这个kDriver Fuzzer,利用这个kDriver Fuzzer,我也在2017年收获了不同厂商,不同驱动近100个CVE,其实关于驱动的Fuzz很早就有人做了,我将我这个kDriver Fuzzer开源出来和大家分享共同学习(必要注释已经写在代码里了),同时春节将近,在这里给大家拜年,祝大家新年红包多多,0day多多!(由于并非是自己从头到尾写的项目,其中有部分编码习惯造成的差异(已尽量向框架作者靠拢)请大家见谅,同时代码写的还不够优雅带来的算法复杂度以及代码冗余也请大家海涵,以及一些待解决的问题未来都会逐步优化:))
一些环境说明:
编译环境:Windows 10 x64 build 1607
项目IDE:VS2013
测试环境:Windows 7 x86、Windows 10 x86 build 1607
参数介绍:
“-l” :开启日志记录模式(不会影响主日志记录模块)
“-s” :驱动枚举模块
“-d” :打开设备驱动的名称
“-i” :待Fuzz的ioctl code,默认从0xnnnn0000-0xnnnnffff
“-n” :在探测阶段采用null pointer模式,该模式下极易fuzz 到空指针引用漏洞,不加则常规探测模式
“-r” :指定明确的ioctl code范围
“-u” :只fuzz -i参数给定的ioctl code
“-f” :在探测阶段采用0x00填充缓冲区
“-q” :在Fuzz阶段不显示填充input buffer的数据内容
“-e” :在探测和fuzz阶段打印错误信息(如getlasterror())
“-h” :帮助信息
常用Fuzz命令实例:
kDriver Fuzz.exe -s
进行驱动枚举,将CreateFile成功的驱动设备名称,以及部分受限的驱动设备名称打印并写入Enum Driver.txt文件中
kDriver Fuzz.exe -d X -i 0xaabb0000 -f -l
对X驱动的ioctl code 0xaabb0000-0xaabbffff范围进行探测及对可用的ioctl code进行fuzz,探测时除了正常探测外增加0x00填充缓冲区探测,开启数据日志记录(如增加-u参数,则只对ioctl code 0xaabb0000探测,若是有效ioctl code则进入fuzz阶段)
kDriver Fuzz.exe -d X -r 0xaabb1122-0xaabb3344 -n -l
对X驱动的ioctl code 0xaabb1122-0xaabb3344范围内进行探测,探测时采用null pointer模式,并数据日志记录
日志文件
log: 主日志记录文件
Enum_Driver.txt: 驱动枚举记录文件
log_detect: 探测模块日志记录文件
log_fuzz: fuzz模块日志记录文件
log_database: ioctl list数据库记录模块
CVE案例
CVE-2017-16948、CVE-2017-17049、CVE-2017-17050、CVE-2017-17113、CVE-2017-17114、CVE-2017-17683、CVE-2017-17684、CVE-2017-17700等等等等..
CVE-2017-17861 Jungo WinDriver空指针引用
CVE-2017-17112 IKARUS AntiVirus池溢出漏洞
kDriver Fuzzer整体架构及Fuzz思路
稍微讲一下这个思路,以及和原ioctlbf的一些区别,有些不细讲后面会提到。
Step 1
首先通过CreateFile打开设备驱动,之后进入ioctl code的探测部分,主要探测有效的ioctl code,这里ioctlbf中采用的是在DeviceIOControl中直接用NULL来作为Input Buff和Input Buff size,在我调试过程中发现,这样做会产生大量的空指针引用漏洞,这样就不好进入后面的Fuzz过程,我在对某厂驱动进行fuzz的过程中发现某个ioctl code除了空指针引用漏洞,后面还会因为input buff中某个特殊的结构体产生内存破坏,这是我把null pointer这个模块单独分离出来,并加入了一个正常填充后发现的。
这里如果ioctl code无效,则直接返回,开始对下一个ioctl code进行探测,最后的记录是在一个结构体,同时我用日志模块记入数据库后面会说。
Step 2
随后会根据-f参数选择是否0x00填充,因为常规模式是使用0x41填充缓冲区(这里也可以考虑改成随机数据填充,但这里随机数据在后面fuzz中有了),而我在对多家厂商驱动进行fuzz的时候发现,0x00填充也会产生一些问题,比如空指针引用等,ioctlbf中-f参数的功能我没太弄懂为什么要加,所以这里根据我的实战后的一些想法进行了修改。
Step 3
走到第二步或直接进入第三步时已经确认当前是一个有效的ioctl code,随后会对ioctl code的input buff size进行探测,主要是探测最大的buff size和最小的buff size,这里很多驱动会对input buff size进行判断,防止溢出等情况的发生,因此这里如果返回getlasterror,那么可能就是出错了,但如果返回正确,则处理正确,这样从0-max length判断找出边界就可以了。
另外这里很有可能产生溢出(池、栈都有)
Step 4
至此探测完成,这里会把所有数据记入一个struct中,叫做IOCTL List,该结构体定义如下
typedef struct IOCTLlist_ {
DWORD IOCTL;
DWORD errorCode;
size_t minBufferLength;
size_t maxBufferLength;
struct IOCTLlist_ *previous;
} IOCTLlist, *pIOCTLlist;
结构体以单项链表的形式保存,由于这里ioctl code也不会很多,所以单项链表足以操作不会由于查询等情况影响速度,否则双向链表更优,这里我将ioctl code的数据通过日志模块写入数据库,以便分析查询,后续打印等工作也是根据ioctl list完成的,这是核心,随后进入Fuzz模块。
Fuzz策略
Fuzz部分和ioctlbf出入不大,少数数据部分进行了一些修改,这里参考了bee13boy在zer0con2017演讲中的内容,增加了一些特殊数据作为种子。增加了日志记录便于对变异数据填充分析。
第一步会将一些无效地址作为input buff传入,我发现一些厂商驱动会将某些地址作为有效地址在驱动中直接引用,从而导致内存破坏的发生,这些例如某些内存高地址等等,如0xffff0000,以及低地址,如0x00001000,当然刚开始,如果用null作为input buff指针,也是无效地址。
第二步会测试某些溢出,这个实际上在探测阶段也算间接做过了,但是这里我用0x10000大小的buffer做为传入,同样input buff size大小也会很大。
第三步开始对种子进行变异,这里主要是通过随机种子填充缓冲区,因为input buff在某些驱动中可能会作为结构体,或其他成员变量,这里通过特殊的dword作为种子填充来对数据变异,这里每4个字节为一个单位依次变异,长度选择是从数据库中记录的min length到max length随机选取,这里测试过大或很小的缓冲区已经没意义,第二步已经做了。
最后一步是完全随机化数据,长度是数据库记录的max length。
kDriver Fuzzer驱动枚举模块实现
我在kDriver Fuzzer中增加了驱动模块的枚举,这里具体代码实现在scan.h中,其实难度也不大。
这里有一点和驱动打开,也就是第一步息息相关,我在测试的过程中发现很多厂商的驱动打开有问题,多数都是返回errorcode 5,也就是拒绝访问,这里如果你是普通用户权限,可以尝试用administrator的权限打开,我在测试的过程中发现很多厂商都是用administrator的权限可以打开,普通用户就不行。
这样fuzz还是有意义的,但是如果需要system打开,那么fuzz就没有意义了,毕竟系统怎么也不会给system权限,而目的就是用ring0来EoP。
题外话: 我逆向了360和腾讯的杀软、管家驱动他们的打开都是errorcode 5,但即使用administrator也打不开。我调试了一下,发现360是有一个360selfprotection,它会将360的进程pid记入一个白名单,当驱动设备被打开时,会取check白名单,如果当前打开进程不在白名单里就拒绝。腾讯的会直接check进程token,不满足权限(估计是system)就拒绝,但不是所有厂商都会这么做,毕竟驱动目的不一样,有很多驱动设备还是需要在某些状态下能和ring3或者说userspace交互的。
我在驱动枚举模块增加了对驱动名称的尝试打开,对可以打开或拒绝访问的驱动设备都进行了日志记录,当然,如果都不对的话也不意味着这个驱动就不存在,如果选中了某些目标,而驱动枚举模块没有找到它的话。
尝试用ida打开,分析具体的设备名称\DEVICE\XXXX,因为有些注册驱动名称并不是驱动本名,不过这个能找到打开驱动设备,因为多数驱动设备都是用的本名。
Driver Fuzzer日志模块实现
ioctlbf中没有日志模块,这样产生bsod不利于还原漏洞,增加日志可以记录漏洞发生前的问题,记录传入参数和内容等等,这样能超快速写出poc。
这里一个主日志记录整个过程,探测日志和fuzz日志部分可以通过-l开启或关闭,这里后面会说。
这里日志模块实现稍微费力一些,在我的logger.h中,刚开始我参考了mwrlab的kernel fuzzer中的logger,但后来大改了。
主要问题在于正常的文件写入是会先把写入内容放在一个cache里,写满了再往文件中写,这个主要好像是跟扇区对齐相关,但我们fuzz的时候会产生bsod,整个操作系统都会挂起进入异常处理,这时候缓存内容还没写到文件里,我们就记录不到发生bsod时的漏洞情况。
所以这里参考了bee13oy师傅在zer0con2017中提到的FILE_FLAG_NO_BUFFERING的标记,这个标记下会将要写的内容直接写入扇区,但这里对写入的buffer有严格要求,因为扇区是要求严格对齐的,申请位置也得是对齐的。
所以我用VirtualAlloc申请512大小的空间,并且写入,这里即使输入只有几个bytes,也得申请512,查看记录中会有大量的0x00填充,但是影响不大,用常规的文本打开即可正常查看。
在fuzz部分由于要记录变异缓冲区,所以我用了一个更大的virtualalloc的空间来记录变异数据,但这样也带来一些问题,最后一部分说。
尚未解决的问题
最后说说一些尚未解决的问题吧。
第一个问题是本来想增加ioctl爆破,之前和LC师傅也聊过这个事,这个在我的代码中可以看到注释部分,最后还是给删除了,刚开始想可以通过ioctl type爆破,比如0x22等等,但是想想unknown的情况太多了,这个unknown的范围太大,实在不好判断,还有一个暴力的方法直接从0x00000000爆破到0xffffffff,这个倒是没有问题,但是感觉太花时间(我试了一下一晚上)。
还不如直接看ida快,我看了bee13oy师傅的slide,他里面是直接在ida里找,手动找的话参考他的slide即可。
第二个问题,我在日志记录的时候增加了-l功能,用于开启和关闭日志记录,这里不影响主日志记录,之所以增加这个参数,主要是我在调试中发现这个日志实在记录的太慢了,因为要flushbuffer,每次都有文件操作,而基本上比如对长度fuzz的时候会for循环很多,导致每一次都要读写好几次,会影响速度,感觉非常明显,但日志真的很重要,还是最好开启的(比如null pointer模式可以不开,因为知道参数)。
第三个问题,我在探测日志和fuzz日志记录的时候只记录了当前fuzz的内容,也就是当前ioctl code的,当前输入参数的内容,过往的都覆盖掉了,因为我尝试过在文件末尾写入,但是由于FILE_FLAG_NO_BUFFERING,本来写入的内容就比字符串多得多,还有很多for循环,稍微写一会就上G的文件了,除非硬盘够大。这样做就导致了一个问题,比如漏洞是由两个ioctl code产生的(同一个ioctl code两次调用倒不怕,也能fuzz出来,这个我遇到过),比如UAF,double free等等,那么就不利于bsod后的分析,和poc的还原,也能分析出来,但影响速度。
当然,主程序会记录fuzz过的ioctl code,去试试就知道了,但还是有影响,对具体的参数还是未知。
还有一些暂时没有看见的问题,等待后续完善。
感谢大家的阅读,也希望以后能有更厉害的fuzzer开源,在fuzz路上越走越远…
参考(其实就是感谢)
主要还是感谢驱动fuzz的作者们,比如最早google的一个driver fuzzer,还有mwrlab kernel fuzzer给的一些日志方面的想法(kernel fuzzer是fuzz win syscall的)。
最感谢的还是bee13oy师傅在slide中的一些启发以及ioctlbf框架的作者。
https://github.com/bee13oy/AV_Kernel_Vulns/tree/master/Zer0Con2017