这几天分析了多个Adobe Reader的漏洞,发现他们很多都是使用 堆喷来将 ROP数据布置在堆中一个精确的位置上,然后通过触发漏洞ret 到ROP 所在堆地址,实现执行ROP来绕过DEP防护。虽然现在这种攻击技术在最新的Windows防护体系下,已经很难成功了。但是作为一项广泛使用的技术,我还是很想了解学习一下其机理。
0x1 Heap Spray简介
Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。
传统slide code(滑板指令)一般是NOP指令,但是随着一些新的攻击技术的出现,逐渐开始使用更多的类NOP指令,譬如0x0C(0x0C0C代表的x86指令是OR AL 0x0C),0x0D等等,不管是NOP还是0C,他们的共同特点就是不会影响shellcode的执行。使用slide code的原因下面还会详细讲到。
但是 Heap Spray 只是漏洞利用的辅助技术,是使得攻击者能够将 恶意代码注入进程内,并且能够使得其被稳定触发的技术。其还是依赖于程序 首先有漏洞能够 跳转到 堆喷的地址去执行。
现在许多的攻击方法,都已经被Windows系统作了防御机制,如下所示:
现在 我们还能够有的攻击手段就是 覆盖 函数的虚表指针 或者对象指针,实现劫持程序执行流。当我们能够劫持程序流时,又如何将程序执行流跳转到我们需要的流程上,这就是 Heap Spary 的意义。
0x2 Heap Spray原理
首先讲一下Windows 程序运行时其 虚拟内存的结构。从windows95开始,windows操作系统就开始构建在平坦内存模型上面,该模型在32位系统上总共提供了4GB的可寻址空间。一般来讲,顶层的一半(0x8000 00000xFFFF FFFF)被保留用做内核空间(NT系统),这种划分是可以修改的。而低地址的0x0000 00000x7FFF FFFF里面,前64K与后64K都是不可以使用的(前64K估计是为了兼容DOS程序或者设置NULL指针而保留,严禁进程访问,后64K用于隔离进程的用户和内核空间),因此进程可用的区域就为*\*0x0001 0000~0x7FFE FFFF****。为了进一步简化程序员的编程工作,Windows操作系统通过虚拟寻址来管理内存。从本质上来说,虚拟内存模型为每个运行中的进程提供了它自己的4GB虚拟地址空间。这项工作是通过一个从虚拟地址到物理地址的转换来完成的,并且是在一个内存管理单元(Memory Management Unit,MMU)的帮助下完成的。
程序虚拟地址空间布局
在这里,我参考 “Heap Spray原理浅析” 中的例子,在Windows10 下运行(VS2019编译),来查看程序运行时虚拟空间的状态:
代码为:
1 |
|
结果如下所示,我们能够看到在Windows10中,程序虚拟空间中地址由低到高依次是:栈数据——静态数据&全局数据——堆数据。与参考文献中的结果有一点区别。其中全局静态数据和常量数量都是在操作系统加载应用程序时直接映射到内存的,一般映射的起始地址是0x 0040 0000,而应用程序依赖的DLL一般都映射在这个地址之后(注:当然这些不是绝对的,DLL可以映射在应用程序本身的前面,应用程序自身也可以通过修改编译选项映射到其他地址,至于堆的区域,则很可能分布在虚拟地址空间的很多地方。从上面的分析可知,一个进程的内存空间在逻辑上可以分为3个部分:代码区,静态(全局)数据区和动态数据区。而动态数据区又有“堆”和“栈”两种动态数据。
而且,在Windows10上与参考文献有一个很重要的结果吻合,即 堆分配的起始地址都是 很低的。
当申请大量的内存到时候,堆很有可能覆盖到的地址是0x0A0A0A0A(160M),0x0C0C0C0C(192M),0x0D0D0D0D(208M)等等几个地址,如下图所示。结合我对 Adobe Reader POC的分析,他们一般都是 覆盖了 0x0C0C0C0C 地址,即申请内存的大小一般都是 200M。
滑板指令选取标准
关于为什么 需要在 shellcode 前加上 slide code的原因,这个很容易理解,就是增加shellcode 的命中率。
然后,在Adobe Reader中使用的 slide code 基本上都是 0x0c0c0c0c,而不是以前常见的 0x90909090。这是由于现在仅仅通过覆盖函数指针 来实现劫持控制流的 漏洞已经很少了。现在多为覆盖虚表指针。如果使用 0x90 来做 slide code,使用 0x0c0c0c0c 来覆盖虚函数指针,那么程序将会 跳转到 0x90909090(该地址处于内核空间) 执行,这就会导致 crash。所以最好的做法是 将 虚函数指针 覆盖为 0x0c0c0c0c,同时将 虚表指针 也覆盖为 0x0c0c0c0c。这样 程序执行流 就能够跳转到 0x0C0C0C0C处 执行 了。
精确命中shllcode
在 JavaScript 的字符串 “ABCD” 是以如下方式存储的:
其次是堆块头的大小问题,一般来讲每个堆块除了用户可访问部分之外还有一个前置元数据和后置元数据部分。前置元数据里面8字节堆块描述信息(包含块大小,堆段索引,标志等等信息)是肯定有的,前置数据里面可能还有一些字节的填充数据用于检测堆溢出,后置元数据里面主要是后置字节数和填充区域以及堆额外数据,这些额外数据(指非用户可以访问部分)加起来的大小在32字节左右(这些额外数据,像填充数据等是可选的,而且调试模式下堆分配时和普通运行模式下还有区别,因此一般计算堆的额外数据数据时以32字节这样一个概数计算,参考《windows高级调试》)。
0x3 实例分析
下面 我以 Adobe Reader漏洞 CVE-2010-2883 的 JS 代码,来分析Adobe Reader 中的堆喷机制,关于此漏洞的分析可以参考我之前在 CSDN中的博客。大概攻击原理就是 通过溢出漏洞,使得能够执行两个 ROP 地址,然后最后一个ROP 地址将会返回到 0x0c0c0c0c 地址所在处的数据继续执行。我们需要将 一个更加完整的 ROP 链布置在 0x0c0c0c0c 开始的堆地址处,使得能够成功绕过 DEP的检测,执行恶意函数。
JS代码如下:
1 | var unescape = unescape; |
我们大概分析一下此 JS 代码:
1 | 1. 首先是对 shellcode 使用 unescape 进行了加密; |
然后这里我们有几个问题:
- 为什么 要截取 (0x0c0c-0x24) 长度的 slide code 填充在 shellcode之前;
- 为什么 每一个块的大小为 0x7efe8(0x80000 -0x1020+0x8)
我们使用 Windbg 断在漏洞触发点前,然后查看此时堆块大小为 0xfefec
我们使用windbg,首先断在漏洞触发点前。观察此时 0x0c0c0c0c 的地址处所在堆块,如下所示。块头位于 0x0c071018,用户数据区 位于 0x0c071020。我们看一下该堆块的大小为 0x100000。
然后,我们查找我们填充的 shellcode在内存的位置,如下所示:
可以看到 shellcode 在堆块的地址 后四位 都是固定的都为 0x1c08,根据 上面 JS 代码我们可知 shellcode 是按照 0x10000 的大小填充进入 一个堆块,也就是说 shellcode 在一个堆块中 应该按照 0x10000为一个间隔出现。根据 WinDbg 结果与我们的 JS 代码吻合。
然后为什么后 四位都是 0x1c08呢? 我们看到 堆块的开始地址是从 0x0c071020 开始,然后 根据 js 代码 我们在 shellcode 之前填充了 (0x0c0c-0x24) = 0xbe8 的 slide code,也就是 如果从一个堆块的开始位置算,shellcode 第一次出现的位置为 0x0c071020+0xbe8 = 0x0c071c08, 然后以后 每一次 shellcode 出现的位置 即加上 0x10000 即可。
然后为什么 每个块的大小为 0x7efe8(0x80000 -0x1020+0x8)。这是因为 我们输入数据都是 unicode编码,而 JS 对unicode 进行计算 长度时,是一个 uncode 算一个,如下所示:
1 | > s = unescape("%u4142%u4344%u4546") |
所以,实际上 给每个块的赋值为 0XFEFE8,然后我们可以在 windbg 中查看堆信息如下:
!heap -stat显示被调试进程的所有堆使用情况:
然后,我们查看 0x2350000 位置处 堆的分配统计数据:
这里我们可以发现 size 为 0xfefec 的块 数量就为 我们JS 中申请的块数量 0x1f0。而且每个块大小为 0xfefec,和上面我们说的 0xFEFE8,只相差 0x8。而这 0x8 正是我们在后面添加的 “s” 字符串的一个长度。
通过上面的步骤分析,也就是说 我们目前已经能够精确的确定 shellcode 出现的 低四位 为 0x1c08,而且按照 0x10000的大小递增,所以高四位也是可以出现 0x0c0c。 所以基本可以确定我们的 shellcode 能够精确的布置在 0x0C0C1C08 的位置上。
最后,我们还有一个问题就是,shellcode 能够确定出现的位置 低四位 为 0x1C08,并不是 0x0C0C,这样 还是会导致没有精确命中 Shellcode。
这里正是 这个漏洞在 Windows10 上无法执行的原因。因为在 Windows10 上 一个堆块的开始地址 的 低四位 并不是确定的 0x0000,他的 第四位 是随机的,比如可能出现 0x1000,0x2000等等。如果要能够执行成功该漏洞,就需要一定的机率,使得第四位是 0x0000。
0x4 总结
写这篇文章的意义,本来是想 学习堆喷的 技术后,能够自己修改一些漏洞的 EXP。使得一些老漏洞 能在 Win10 上实现利用。但是,当自己真正 去布置 堆喷时,才发现十分困难。比如这个 CVE-2010-2883 我修改了很多次 JS 代码,但是仍然不能保证其 shellcode 能够精确布置在 0X0C0C0C0C处。还需要更多的学习吧。
参考文献
Heap Spray原理解析:https://blog.csdn.net/magictong/article/details/7391397
32 位下的堆喷射技术:https://www.anquanke.com/post/id/87048
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/08/02/Adobe-Reader堆喷研究/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!