Windows下堆喷射检查及漏报误报研究

作为漏洞利用的重要方式,堆喷射技术自诞生以来就受到广泛的关注,相关攻击与防范研究也层出不穷。我们注意到,尽管距技术提出已逾十年,但在最近的IE、PDF、Office等利用中仍可找到堆喷射的踪迹,因此仍有必要对堆喷射行为的检测及相关漏报误报原因进行深入的讨论。

堆喷射的作用是构造一个漏洞利用的上下文,在内存中形成特定重复单元,以期望后续的漏洞利用能够以较大的概率契合到精心控制的内存地址及内容。狭义上的堆喷射有着以下三个特点:短时间内申请大量内存页面;每个单元内存大小相等或者相似;各单元内存具有相似性。所以对于堆喷射的检查,一般而言是选定一个时间段,如果其间内存分配存在异常,且相邻内存具有重复性,则判定有堆喷射的行为。这个过程中,每一步判定都有一个阈值,用以控制检测的准确度,这些阈值还需要通过基于样本的训练来调整,以达到减少误报和漏报的目的。实现过程中,为了减少误报的概率,通常会采用更为严格的约束,例如检测固定大小的内存块,检测调用栈的回溯情况等。我们在进行了长时间的实验后发现,由于现实情况的多样性,有些理论上的方法实际效果并不明显,相反,利用简单但约束较强的启发式方法反而有着较高的检测效率。

判定堆喷射的一个理想情况是检测相同或相似大小堆的内容是否相似。而实际情况是,堆分配与内存赋值通常有时间差,如果以hook方式挂钩堆分配的函数,内存赋值尚未发生,其检测时间点过早,而挂钩堆销毁函数的检测则过晚,因为内存内容可能已经发生变化。如果抛开hook机制而采用定时触发扫描内存的方法,检测频率过高则影响效率,频率过低则影响检测率,二者难以折中。此外,由于DEP的广泛存在,大部分利用都是以RoP的方式,这一特性使得精确的堆喷射成为必要,结果是堆内存实际上仅小部分内容段存在意义,大部分的无关内容可以随机化以干扰对堆喷射的检测。所以,对于堆喷射检测而言,内容检测更多情况下仅扮演一个参考的角色,而不能够作为断言的一个必要条件。

对于堆检测而言,有效且实用的方法只能是在分配时进行检测,加上全局内存分配情况作为辅助。以Windows为例,最有效的方法是挂钩RtlAllocateHeap和VirtualAllocEx等函数。这里,前者对所有使用Windows标准堆管理的上下文有效,而后者则适用于一些有着自主堆管理的模块,例如Flash等。检测中结合堆喷射的特性,通过比较函数返回的堆地址,检测在短时间内是否有连续的内存分配,在达到一个阈值后,调用GetProcessMemoryInfo比较进程使用的内存大小,通常能有效地检测到堆喷射。在这之后,还可以进一步利用挂钩VirtualProtectEx函数来判断是否有RoP行为,同时区分正常的JIT和恶意的Exploit的情况。

为了避免频繁介入正常的堆分配操作以提高检测效率,对堆操作的返回结果需要过滤噪声。我们经过了大量的实验后发现,小于0x40000字节的堆分配不能在短时间内保证返回地址的对齐,所以通常不会被用于堆喷射中。此外,尽管超过这一数值的堆分配理论上都应该成为分析的候选,但大于0x400000的内存分配几乎不见于已知的堆喷射情况中,因此也可以作为噪声予以过滤。值得注意的是,在检测分配堆的大小上,上下限确立后,不应对分配堆的大小再做任何假设,我们以一漏报为例。

通常情况下为了保证利用的成功率,堆喷射除了尽量覆盖更多的地址外,还应该保证相邻堆的致密性,即,相邻分配的内存中间不应该有随机内容产生。这一目标通常通过连续分配和分配对齐的堆来实现,前者保证无其它操作影响待分配的堆,后一个保证页面无随机内容填充。由于内存页面大小固定,加之每一堆内存存在一个管理结构,致密的堆分配应该是略小于页面整数倍的请求,函数hook的记录内容类似于:

Function=RtlAllocateHeap,Handle=0x00150000,Size=0x0007ff00,Ret=0x03280020
Function=RtlAllocateHeap,Handle=0x00150000,Size=0x0007ff00,Ret=0x03300020

这样,两个返回地址之间的距离为0x80000,实际可以控制内容的大小是0x7ff00字节,页面最后0x100-0x20=0xE0字节虽属于随机填充内容,但这部分地位地址总是0xff20开头,漏洞利用代码可以避开这类地址的使用,所以对于利用的成功率几乎没有影响。

但是这个条件仅在理想利用中存在,用这个条件进行过滤,反而会遗漏一些低质量的利用,例如Metasploit等。事实上,Metasploit的堆喷射会产生类似于如下的记录:

Function=RtlAllocateHeap,Handle=0x00150000,Size=0x00080000,Ret=0x04b00020
Function=RtlAllocateHeap,Handle=0x00150000,Size=0x00080000,Ret=0x04b90020

显然Metasploit没有考虑堆管理结构的影响,直接分配了0x80000大小的内存来进行堆喷射。以此记录为例,攻击者试图分配0x80000字节,但实际需要0x80020字节,由于堆页面大小(0x10000字节)的对齐影响,相邻两个堆间隔为0x90000字节,而能够精确控制的仅有0x80000字节,剩下约0x10000字节被系统随机填充,在连续分配的放大效应后,必然会有约13%的内存无法控制,且此类不可控内容的地址横跨整个页面,无论利用代码如何选取低16位均会影响利用的成功率。这个例子可以看到,如果堆喷射的检测总是假设攻击者拥有良好的内存分配知识,从而加强检测条件,必然会在这里形成漏报。

堆大小检测的另外一个误报例子来源于页面的刷新。当页面需要实时更新时,浏览器通常会从服务端获取新数据来替换或追加页面内容,如果服务端返回的数据结构固定且大小刚好满足条件,宏观上也会产生类似于堆喷射的行为。例如页面需要频繁获取js文件或者json数据,浏览均需要反复分配内存以解析运行脚本,短时间内会形成大量的内存分配操作,且此类操作不易与正常的堆喷射行为进行区分。作为检测的有益补充,回溯调用栈可以有效缓解这一问题,例如在检测结果上过滤掉常见的系统正常操作,包含来自JSON部分的内存申请、来自xmllite的大内存申请,对表格或多选框的子项进行反复操作等。这一类调用对于某一版本或者类型的上下文是固定的,所以有必要形成一个相应的调用栈回溯白名单,以降低误报的概率。

在非浏览器的环境中,偶尔可见一些堆喷射情况,总体而言,上述思路可以检测到绝大部分的堆喷射行为,但具体的参数,例如分配内存大小的上下限等,还需要一定调整。我们以大量PDF、Office样本进行了测试和训练,发现误报漏报的情况几乎没有,但相关参数却与在浏览器下的经验数据有所差异,需要从新训练。此外,非浏览器的环境中,另一个主要问题是一些利用需要用户交互才能触发,因此,模拟用户操作反而成为减少漏报率的一个关键因素。我们在这一问题上也进行了部分的研究,并将交互行为应用到浏览器检测上,由于篇幅限制,这里不再一一展开。

漏洞利用中还有两类情况,其行为上可以归于堆喷射,即JIT喷射和小堆连续分配占位,两者皆可采用相同的思路进行检测。第一类JIT喷射涉及到大量内存分配,检测条件较为严格,可以通过栈回溯来精确检测。实战中上此类样本极少,影响的软件版本有限,这部分不必要对未知情况进行过多的处理和讨论。第二类情况则非常复杂,一般而言,小堆连续分配包括对UAF漏洞进行占位、对越界读写漏洞进行环境准备等。从宏观上看,这类行为对整体内存分配情况影响甚微,微观而言,小堆连续分配是常见且合理的操作,所以检测的粒度无论大小,均无法将其与正常的操作予以区分。因此,我们在实际中采用了特定栈回溯匹配法来进行启发式检测,对一些不常见的对象,例如Dictionary,Vector等的构造与析构函数进行Hook,设定时间与触发次数的阈值,结合返回地址和堆标记进行判断。这种方法的好处在于能够有效地减少误报率,比如购物网站包含的大表格列表页面等,但我们依然发现存在少量对使用这些不常见元素网页的误报行为。对于这一类误报,目前只能从其它因素上对站点进行判断,而无法基于文中所述方法对具体页面给出一个严格准确的断言。

对于Flash而言,目前的情况则较为简单,我们在分析了尽可能多的样本后发现,flash的漏洞利用样本有两个明显特征:第一是一定存在小内存对象的反复分配,第二是flash文件结构较为简单。所以利用栈回溯来匹配,误报率已经非常低,再以flash文件结构,或者AS3 LoadBytes函数内存解压后的文件结构进行参考,误报率和漏报率都可以达到几乎为零的程度。如果考虑支持不同版本flash下的检测,为了避免分析不同版本并设定相应Hook地址带来的巨大工作量,还有一个近似的方法可以获得类似的效果。我们知道,flash有着自己的堆管理模块,小于一定大小的堆均由flash自行管理,但是,flash需要维护这些小堆的管理结构,这部分内存属于windows自己的内存分配。在大量申请小堆对象的时候,管理结构的数量会暴增,从而导致这部分管理堆的大小不够,迫使flash本身从windows堆管理中去申请更大的内存。从函数调用记录上看,这里的特征是,短时间内向RtlAllocateHeap多次要求内存,每次内存大小是上一次的两倍,且调用的回溯栈具有类似结构。针对这三个特点进行flash漏洞利用检测,其效率更高,且准确度与前一个方法几乎不相上下。

发表评论

电子邮件地址不会被公开。 必填项已用*标注