题记:win10都开始推广了,我还在折腾xp,真low...
DWORD SHOOT的原理是利用Release版发布的程序在正常启动(非调试启动)情况下,使用Lookaside表(又称块表)管理程序中堆频繁的申请/释放操作不检验返回给调用者地址的合法性造成的漏洞。本文以堆溢出学习笔记和<0day安全 软件漏洞分析技术>为基础,改进而来。
首先要明确Lookaside表是一个单链表,每次HeapFree释放的堆块被插到链表头,每次HeapAlloc都从链表头取一项然后返回给申请者;链表尾指向空。进程内存空间中有这样的单链表共128项,以数组形式管理。嗯,概念太多还是用程序说话
#include
#include
int main()
{
HLOCAL h1,h2,h3,h4,h5,h6,h7,h8,h9;
char* pi=NULL;
HANDLE hp;
char xpsp3Str[8] = {0};
hp = HeapCreate(0,0,0);
getchar();
__asm int 3;
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
HeapFree(hp,0,h1);
HeapFree(hp,0,h2);
HeapFree(hp,0,h3);
HeapFree(hp,0,h4);
HeapFree(hp,0,h5);
HeapFree(hp,0,h6);
HeapFree(hp,0,h7);
h8 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h9 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
先简单介绍一下程序的流程:因为系统堆经历太多风雨,因此代码中新建一个私有堆便于观察现象。调用HeapAlloca为h1-h7分配堆块时,由于私有堆块表中还不存在可用记录(新创建时,堆中快表为空),因此这些堆块都来自空闲表。之后调用HeapFree时系统并不马上回收堆块并重新加入空闲链表,取而代之,系统将这些堆块加入各个块表的单链表中,因此块表逐渐丰满起来,最后h8 h9分配到的堆块就是从块表中取出的元素。
来看下调试过程,在命令行下运行程序,程序会等待console输入,先用windbg attach程序,然后F5并在console上按键。由于int 3的作用,调试器会中断程序运行,接受用户的输入。如果去掉这两句,程序直接return返回了,gg。由于程序是release版,没有调试符号,只能忍受汇编指令。不过由于程序比较简单,重复观察push/push/push/call操作即可,因此希望你不要觉得可怕而中途溜走。
00401040 6a08 push 8 ;对于第一个HeapAlloc 压入第三个参数
0:000> p
00401042 6a08 push 8 ;压入第二个参数 8应该就是HEAP_ZERO_MEMORY的值
0:000> p
00401044 56 push esi ;esi是hp的地址,Heap句柄
0:000> p
00401045 ffd7 call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> r eax ;HealAlloca的返回值,用户可用空间
eax=003a1e90
为了证明开始时几个HeapAlloc分配得到的堆块来自私有堆空闲链表,可用HEAP验证:
0:000> dt _HEAP @esi ;esi,首次调用HeapAlloc时堆句柄值
ntdll!_HEAP
+0x178 FreeLists : [128] _LIST_ENTRY [ 0x3a1e90 - 0x3a1e90 ] ;运行RtlAllocateHeap前私有堆的空闲链表
+0x578 LockVariable : 0x003a0608 _HEAP_LOCK
+0x57c CommitRoutine : (null)
+0x580 FrontEndHeap : 0x003a0688 Void
0:000> dt _HEAP @esi ;此时esi是再次调用HeapAlloc时堆句柄值
ntdll!_HEAP
+0x178 FreeLists : [128] _LIST_ENTRY [ 0x3a1ea0 - 0x3a1ea0 ] ;运行RtlAllocateHeap后私有堆的空闲链表
+0x578 LockVariable : 0x003a0608 _HEAP_LOCK
+0x57c CommitRoutine : (null)
+0x580 FrontEndHeap : 0x003a0688 Void
0:000> r eax ;之前空闲链表FreeList[0]指向0x3a1e90 现在这个内存被分配给了h1
eax=003a1e90
0:000> dd 3a0688 ;这是系统的块表 目前还是空的
003a0688 00000000 00000000 01000004 00000000
003a0698 00000000 00000000 00000000 00000000
003a06a8 00000000 00000000 00000000 00000000
003a06b8 00000000 00000000 01000004 00000000
003a06c8 00000000 00000000 00000000 00000000
003a06d8 00000000 00000000 00000000 00000000
003a06e8 00000000 00000000 01000004 00000000
003a06f8 00000000 00000000 00000000 00000000
解释一下
1.程序开始时,堆管理器中只有一大块空闲内存,由FreeList[0]指向,当前FreeList[0]的地址是0x003a0178,空闲内存地址是0x3a1e90。对于这段代码,开始的几句HeapAlloc都是瓜分这块内存区域的内存;
2._HEAP!FrontEndHeap指向程序快表地址,对于这段代码,快表位于0x3a0688;
3.由于MS并没有公开快表结构,对于xp系统,只知每个快表项占0x30B,按调试的结论,可能是这样的结构
struct
{
DWORD* next; //同一链表中下一元素地址
WORD entryCount1; //链表中元素个数
WORD unknownW;
DWORD unknownDW1;DWORD maxEntryCount; //链表中最多容纳元素数量
DWORD unknownDW2;DWORD entryCount2;//链表中元素个数
QWORD unKnownQ D1,unKnownQ D2,unKnownQ D3;
};
4._HEAP!FrontEndHeap指向的区域的头0x30B可能是一个快表数组管理结构,之后才是快表数组。目前整个快表数组都是空的
重复以上的调试过程,观察h1/h2/h3/h4分配到的内存空间:
0:000> p
0040104e ffd7 call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> r eax ;h2=0x3a1ea0
eax=003a1ea0
0:000> p
00401058 ffd7 call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> r eax
eax=003a1eb0 ;h3=0x3a1eb0
0:000> p
0:000> r eax
eax=003a1ec0 ;h4=0x3a1ec0
因为对于非调试运行的Release程序HeapAlloc每次分配8+8*N的空间,其中8B是用户空间的管理结构HEAP_ENTRY(细节可参考张银奎老师<软件调试>23章),记载着调用HeapAlloc时分配的空间大小,紧随HEAP_ENTRY之后的8*N8是用户请求空间的数值向上取整的结果,作为用户可用空间返回给请求者;
0:000> dt _HEAP_ENTRY ;xp下_HEAP_ENTRY结构
ntdll!_HEAP_ENTRY
+0x000 Size : Uint2B ;本次分配的堆块的空间,单位为8B
+0x002 PreviousSize : Uint2B ;前一次分配的堆块的空间,单位为8B,用于检测堆溢出
+0x000 SubSegmentCode : Ptr32 Void
+0x004 SmallTagIndex : UChar
+0x005 Flags : UChar ;堆块标志,flags=1 堆块被占用
+0x006 UnusedBytes : UChar
+0x007 SegmentIndex : UChar
0:000> dt _HEAP_ENTRY 0x3a1e90-8 ;h1的Heap_Entry
ntdll!_HEAP_ENTRY
+0x000 Size : 2 ;2*8=0x10B HEAP_ENTRY占用8B 用户请求8B
+0x002 PreviousSize : 0x301
+0x000 SubSegmentCode : 0x03010002 Void
+0x004 SmallTagIndex : 0xbc ''
+0x005 Flags : 0x1 '' ;占用状态
+0x006 UnusedBytes : 0x8 ''
+0x007 SegmentIndex : 0 ''
0:000> dt _HEAP_ENTRY 0x3a1ea0-8
ntdll!_HEAP_ENTRY
+0x000 Size : 2 ;分配给h2的堆块大小 2*8=0x10B
+0x002 PreviousSize : 2 ;上次分配的堆块大小,即h1分配的空间
+0x000 SubSegmentCode : 0x00020002 Void
+0x004 SmallTagIndex : 0xbe ''
+0x005 Flags : 0x1 ''
+0x006 UnusedBytes : 0x8 ''
+0x007 SegmentIndex : 0 ''
到这我觉得该解释的都解释了,我们继续往下调试,接下来是分配h5/h6/h7的堆块,跳过了不演示了,太累!分配结束后,紧接着就是调用HeapFree释放空间,按前面结论,释放的空间应该暂住在快表中(对于xp,快表中其实只能容纳4个同样大小的堆块,超过4个还是要进入空闲链表,好小气的暂住点)
我们来看下释放h1后,快表的状态:
0:000> p
00401082 53 push ebx
0:000> p
00401083 8b1d00504000 mov ebx,dword ptr [lookside+0x5000 (00405000)] ds:0023:00405000={ntdll!RtlFreeHeap (7c92ff0d)}
0:000> p
00401089 6a00 push 0
0:000> p
0040108b 56 push esi
0:000> p
0040108f ffd3 call ebx {ntdll!RtlFreeHeap (7c92ff0d)}
0:000> p ;释放h1
释放的h1将进入快表数组第二项,即数组元素[1](第一个元素至始至终不被使用,存在感不强~)
0:000> dd 3a0688
003a0688 00000000 00000000 01000004 00000000
003a0698 00000000 00000000 00000000 00000000
003a06a8 00000000 00000007 00000000 00000000 ;前0x30B 快表管理头
003a06b8 00000000 00000000 01000004 00000000
003a06c8 00000000 00000000 00000000 00000000
003a06d8 00000000 00000000 00000000 00000000 ;中0x30B 快表数组[0]
003a06e8 003a1e90 00010001 01000004 00000004 ;快表数组[1]
003a06f8 00000004 00000001 00000000 00000000
套用前面的结构,0x3a06e8处的内容应该是这样:
struct
{
DWORD* next; //同一链表中下一元素地址=003a1e90
WORD entryCount1; //链表中元素个数=1
WORD unknownW;
DWORD unknownDW1;
DWORD maxEntryCount; //链表中最多容纳元素数量=4
DWORD unknownDW2;
DWORD entryCount2; //链表中元素个数=1
QWORD unknowQD1,unknowQD2,unknowQD3;
};
待h2-h4全部释放完,我们来看下快表[1]中单链表的内容:
0:000> dd 3a0688
003a06e8 003a1ec0 00040004 01000004 00000004
003a06f8 00000004 00000004 00000000 00000000
struct
{
DWORD* next; //同一链表中下一元素地址=003a1ec0 原h4释放
WORD entryCount1; //链表中元素个数=4
WORD unknownW;
DWORD unknownDW1;
DWORD maxEntryCount; //链表中最多容纳元素数量=4
DWORD unknownDW2;
DWORD entryCount2; //链表中元素个数=4
QWORD unknowQD1,unknowQD2,unknowQD3;
};
:000> dd 003a1ec0 L1
003a1ec0 003a1eb0 ;下一个节点地址是原h3释放的
0:000> dd 003a1ec0 L1
003a1ec0 003a1eb0 ;下一个节点地址是原h2释放的
0:000> dd 003a1eb0 L1
003a1eb0 003a1ea0 ;下一个节点地址是原h1释放的
0:000> dd 003a1ea0 L1
003a1ea0 003a1e90 ;下一个节点为空!
0:000> dd 003a1e90 L1
003a1e90 00000000
从windbg调试输出可知,后释放的堆块被链在单链表的最开头部分。
最后,我们来看下从快表中分配堆块。因为这次快表[1]中有元素了,因此如果请求的堆块的大小和快表[1]中大小相同,堆管理器就从中取堆块返回给调用者。
0:000> p
004010cc ffd7 call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> p
0:000> dd 003a06e8 L2
003a06e8 003a1eb0 00040003
0:000> p
004010d3 ffd7 call edi {ntdll!RtlAllocateHeap (7c9300a4)}
0:000> p
0:000> dd 003a06e8 L2
003a06e8 003a1ea0 00040002
第一次调用HeapAlloc从快表[1]的单链表中取表头元素,这是前面h4释放的,0003说明快表中还剩3个堆块;第二次调用HeapAlloc,仍从快表[1]的单链表中取表头元素,这是前面h3释放的,同时0002说明快表中还剩2个堆块