在后堆栈时代,安全人员在挖掘信息泄露类漏洞时,面临哪些机遇和挑战?
5月13日, 微软安全响应中心发布了一份报告”Solving Uninitialized Stack Memory on Windows”, 讲述了微软在解决堆栈数据未初始化方面所做的努力,里面提到微软从Windows10 1903版本开始,在系统内核与Hyper-V代码中启用了一个被称为InitAll的功能,该功能对内核代码分配在堆栈上的数据进行了自动初始化,从而避免了内核数据被泄露给用户模式代码。
读完这份报告后,我们马上意识到该报告宣布了一个时代的结束,微软不再对此类漏洞采取见一个补一个的方案,而是提供了一个大一统的解决方案,彻底封堵了此类漏洞的发生。安珀实验室也对这类漏洞进行了深入研究,今天我们就结合微软的这份报告,谈一谈在“后堆栈”时代,安全人员在挖掘信息泄露类漏洞时,所面临的机遇和挑战。
首先我们需要了解一些基础知识, 包括信息泄露类漏洞的利用,缓解,以及绕过等多个环节。
ROP定义:
ROP的全称为Return OrientedProgramming, 即返回导向编程,这是一种高级的内存攻击技术,可以用来绕过操作系统的各种通用防御,如堆栈代码不可执行等。ROP的关键之处就在于它所采用的代码组件,即gadgets必须是地址固定的,这里给出了一个gadgets片段:
第一行的0xB7E17FFC表明在该地址处,存在着一条指令为pop edx, ret. 该指令用来将堆栈中的数据弹出到edx寄存器,然后返回到下一条指令处继续运行。一旦该地址处不再是这条指令,那么整个ROP所依赖的代码流将会断裂掉,此次攻击将会彻底失败。后面我们还将会提到这一点。
信息泄露定义:
广义上的信息泄露是指个人或实体隐私数据被他人恶意窃取,然后被用来进行非法行为。在本文中,特指操作系统内核中发生的信息泄露,由于操作系统的分层机制,内核代码运行在Ring0级,拥有最高权限,用户代码运行在Ring3级,拥有最低权限。如果系统内核存在信息泄露bug,则用户层代码可利用该bug获得操作系统的关键信息,结合其他的地址读写漏洞,即可非法获取操作系统的最高权限。
信息泄露历史:
在Windows Vista发布之前,是没有信息泄露这个概念的,因为那里到处都是信息泄露,操作系统对自己的隐私一点儿都不看中,只要系统能够运行得又快又好,牺牲一点隐私算的了什么。
随着Windows市场占有率的提升,越来越多的黑客将目标瞄向了它,微软开始认真考虑这个问题了。于是在Vista发布的时候,微软启用了一个重要的功能KASLR,即内核基址随机化,该机制将内核模块的基地址加载到随机的位置,不再使用固定的地址,从而挫败了黑客们使用ROP的企图。那么对于黑客来说,挖掘出信息泄漏漏洞,泄漏出所需要的模块基址,就是直接对抗KASLR的办法。
信息泄露漏洞挖掘方法:
了解了基础知识之后, 我们还需要了解当前被微软封堵的这类漏洞的挖掘方法。 据公开资料显示,全球几大安全实验室,包括Google的Project Zero,百度安全实验室,360的IceSword实验室,都开发了自己的Fuzzing工具,用来挖掘内核中的信息泄露漏洞。根据部分POC所泄露出来的信息,我们可以确定,这3家都使用了同一种方法来对系统内核进行挖掘,即内核栈污染。
那么什么是内核栈污染呢? 简单来说,就是创建一个内核模式驱动,hook内核的KiFastCallEntry函数,该函数是所有内核函数的入口函数,在发生内核函数调用的时候,对内核栈空间填充指定的数据,比如0x41414141, 如果某内核函数在栈上分配变量时没有对其进行正确的初始化,那么它的值就被我们的指定数据给污染了,在内核向用户模式代码复制数据的时候,我们对其进行监控,如果发现了我们之前填充的数据,就表明该函数存在信息泄露bug,通过进一步的堆栈回溯,就能够发现信息泄露的原始内核函数。这里是内核栈污染的关键代码:
通过调用IoGetStackLimits函数,得到当前栈空间的范围, 然后对其填充16进制字符0x41,即大写字母A,对内核栈空间进行污染。由于内核向用户模式代码复制数据的时候,会使用memcpy函数,因此我们还得对这个函数进行hook,下面是相关代码:
我们首先检查传递给memcpy的相关参数,如果原地址位于内核模式空间,目的地址位于用户模式空间,就判断其内容是否包含特定的污染数据,如果存在就将其记录到日志文件中,然后集中进行分析。对于CVE-2018-0745漏洞,内核会向用户代码复制如下数据, 该输出表明在0x8C处,存在4个字节的内存泄露:
有了这些数据, 同时结合内核的调用栈回溯,就能定位到真正的信息泄露函数。
然而, 正如我们开头所说的, 5.13之后, 微软祭出了终极大杀器-InitAll,通过对内核代码分配在栈上的变量进行自动初始化,这类bug将再也不会存在了。
微软的解决方案:
为了系统的稳定性,并不是所有的模块都开启了InitAll功能,目前微软将其优先应用到如下几个地方:
1. 内核模块代码,你可以简单理解为所有的sys文件。
2. Hyper-V代码,微软自家的虚拟机产品线,避免虚拟机逃逸类漏洞的发生。
3. 与网络相关的用户模式服务代码,避免远程代码执行类漏洞的发生。
4. 基于内核池(pool)的初始化已在进行中,虽然仅在文末提了一句。
尚未被覆盖的地方包括:
1. 应用层代码,你可以简单理解为所有的exe,dll文件。
2. 部分使用汇编编写的代码,由于InitAll是基于C/C++的编译期优化功能,因此不适配汇编语言。
3. 部分对性能影响较大的结构, 如_CONTEXT,里面包含了很多寄存器的值。
微软已计划将InitAll功能应用到将来发布的所有Windows模块中,这样将彻底终结此类信息泄露漏洞的发生。
安全人员的应对方案:
作为安全人员,其职责就是和各大厂商进行安全竞赛,赶在黑客发现漏洞之前对其进行修复。通过了解微软的解决方案,我们意识到再在内核栈上进行信息泄露类漏洞的挖掘,将不再可能。但信息泄露类漏洞注定不会完全消失,它会以其它的方式出现在我们身边。我们来看看除了在栈上进行漏洞挖掘,历史上还有哪些方法能够挖掘出来信息泄露类漏洞。
CVE-2015-0010:
这个漏洞存在于cng.sys模块中,该模块是微软的内核密码学驱动模块,里面包含了开放给其它内核驱动和用户模式程序的控制功能。在使用特殊的控制代码进行访问时,它会返回特定的函数接口地址,以便用户进行相关的密码学操作,而无需自己从头实现这些代码,本来这些功能应该仅开放给内核模式代码,但在cng模块的早期实现中,这些功能同时可以被内核和用户模式代码调用,这样就出现了问题。这种类型的信息泄露并没有利用内核栈上的未初始化变量,但依然能够获得特定内核变量的地址,成功实现了内核的信息泄露。下面的POC代码显示了在用户态传入控制码0x390048时,会返回特定的内核变量的地址:
CVE-2019-1071:
这个漏洞利用了内核结构体的联合功能(union)定义,用户之所以定义联合体,其本意是为了节省内存,可是在特定的情况下,却成了危险的来源。在C语言中,如果定义了这样一个联合体:
那么ID 和pointer是占用同一段内存空间的,用户首先让pointer指向合法的内存地址,然后通过特殊的手段,让内核代码误以为它实际代表了ID,就能在获取ID的时候,返回pointer所指向的内核地址。该POC代码如下所示:
CVE-2019-1436:
这个漏洞利用了函数的返回值未被置空的bug。在内核函数调用的时候,部分函数的返回值被定义为VOID, 表明它不返回任何有效的结果,系统仅仅利用了函数的执行过程。虽然它被定义为返回VOID,但并不代表它真的不返回任何结果,在这个例子中,它返回了一个内核变量的地址,通常该地址并不会被用到,但是你不能保证它不被有心人利用,内核模块的反汇编代码如下:
在0x2035C7处,eax寄存器中存储了特定的内核变量的地址,通过关键的两次跳转,保证了在函数的结尾处,依然存储了该内核变量的地址,这就为我们利用它提供了可能。而在微软的修复方案中,微软使用了清空返回值的操作,也就是xor eax, eax. 下面是该漏洞的POC代码:
总结:
综上所述,虽然基于内核栈变量未初始化的漏洞即将消亡,但我们仍然有许多种不同的方法来找到内核中的信息泄露漏洞,对这些漏洞的挖掘方法可以归类为:
1. 基于内核控制码的枚举。
2. 基于联合体变量的误用。
3. 基于函数返回值的利用。
我们相信并不会仅存在这3种挖掘方法。而这种信息泄露漏洞基本上都是逻辑类漏洞,不能再依靠简单的无脑fuzzing来捕获它,它对安全研究人员的技能提出了新的挑战,也告诉我们哪个方向是可行的,哪个方向不可行。信息泄露类漏洞将会一直存在下去,只是挖掘出它们会变得越来越困难。永远保持好奇心,迎难而上,是成为一个合格黑客的基本素养。
来源:金山云安珀实验室
*本文作者:kingsoftsec,转载请注明来自FreeBuf.COM