一、前言
堆對於開發者一般來說是熟悉又陌生的,熟悉是因為我們常常使用new/delete或者malloc/free使用堆,陌生是因為我們基本沒有去了解堆的結構。堆在什么地方?怎么申請?怎么釋放?系統又是怎么管理堆的呢?
帶着疑問,這兩天看了<軟件漏洞分析技術>與<漏洞戰爭>中關於堆的說明,終於對於堆有一點點的了解了。這里記錄一下在學習和調試中的一點筆記。
二、關於堆的基本知識
1).首先了解空閑雙向鏈表和快速單向鏈表的概念
1.空閑雙向鏈表(空表)
空閑堆塊的塊首中包含一對重要的指針,這對指針用於將空閑堆塊組織成雙向鏈表。按照堆塊的大小不同,空表總共被分為128條。
堆區一開始的堆表區中有一個128項的指針數組,被稱作空表索引(Freelist array)。該數組的每一項包括兩個指針,用於標識一條空表。
如圖所示,空表索引的第二項(free[1])標識了堆中所有大小為8字節的空閑堆塊。之后每個索引項指示的空閑堆塊遞增8字節。例如free[2]為16字節的空閑堆塊,free[3]為24字節的空閑堆塊,free[127]為1016字節的空閑堆塊。
空閑堆塊的大小 = 索引項(ID) x 8(字節)
把空閑堆塊按照大小的不同鏈入不同的空表,可以方便堆管理系統高效檢索指定大小的空閑堆塊。需要注意的是,空表索引的第一項(free[0])所標識的空表相對比較特殊。這條雙向鏈表鏈入了所有大於等於1024字節的堆塊(小於512KB),升序排列。
2.快速單項鏈表(快表)
快表是Windows用來加速堆塊分配而采用的一種堆表。這里之所以叫做"快表"是因為這類單項鏈表中從來不會發生堆塊合並(其中的空閑塊塊首被設置為占用態,用來防止堆塊合並)
快表也有128條,組織結構與空表類似,只是其中的堆塊按照單項鏈表組織。
快表總是被初始化為空,而且每條快表最多只有4個結點,故很快就會被填滿。
2)堆塊的結構
堆塊分為塊首和塊身,實際上,我們使用函數申請得到的地址指針都會越過8字節(32位系統)的塊首,直接指向數據區(塊身)。堆塊的大小包括塊首在內的,如果申請32字節,實際會分配40字節,8字節的塊首+32字節的塊身。同時堆塊的單位是8字節,不足8字節按8字節分配。堆塊分為占用態和空閑態。
其中空閑態結構為:
占用態結構為
空閑態將塊首后8個字節用於存放空表指針了。
在64位系統,塊首大小為16字節,按16字節對齊。
3)堆塊的分配和釋放
1.堆塊分配
堆塊分配可以分為三類:快表分配、普通空表分配和零號空表(free[0]分配)。
從快表中分配堆塊比較簡單,包括尋找到大小匹配的空閑堆塊、將其狀態修改為占用態、把它從堆表中"卸下"、最后返回一個指向堆塊快身的指針給程序使用。
普通空表分配時首先尋找最優的空閑塊分配,若失敗,則尋找次優的空閑塊分配,即最小的能滿足要求的空閑塊。
零號空表中按照大小升序鏈着大小不同的空閑塊,故在分配時先從free[0]反向查找最后一個塊(即最大塊),看能否滿足要求,如果滿足要求,再正向搜索最小能滿足要求的空閑堆塊進行分配。
當空表中無法找到匹配的"最優"堆塊時,一個稍大些的塊會被用於分配,這種次優分配發生時,會先從大塊中按請求的大小精確地"割"出一塊進行分配,然后給剩下的部分重新標注塊首,鏈入空表。
由於快表只有在精確匹配才會分配,所以不存在上述現象。
2.堆塊的釋放
釋放堆塊的操作包括將堆塊狀態改為空閑,鏈入相應的堆表。所有的釋放塊都鏈入堆表的末尾,分配的時候也先從堆表末尾拿。
另外需要強調,快表最多只有4項。
3.堆塊的分配和釋放
在具體進行堆塊分配和釋放時,根據操作內存大小不同,Windows采取的策略也會有所不同。可以把內存按照大小分為三類:
小塊:Size < 1KB
大塊:1KB < Size < 512KB
巨塊:Size >= 512KB
分配 | 釋放 | |
小塊 | 首先進行快表分配 |
優先鏈入快表(只能鏈入4個空閑塊) |
大塊 | 首先使用堆緩存進行分配 |
優先將其放入堆緩存 |
巨塊 | 一般來說巨塊申請非常罕見,要用到虛分配方法(實際上並不是從堆區分配的) |
直接釋放,沒有堆表操作 |
在分配的過程中需要注意的幾點是:
(1)快表中的空閑塊被設置為占用態,故不會發生堆塊合並操作,且只能精確匹配時才會分配。
(2)快表是單鏈表,操作比雙鏈表簡單,插入刪除都少用很多指令
(3)快表只有4項,很容易被填滿,因此空表也是被頻繁使用的
三、調試堆在PEB中的數據結構
1)完成C代碼,在x64下編譯為Release版本,運行
#include "stdafx.h" #include <Windows.h> #include <iostream> using namespace std; extern "C" PVOID64 _cdecl GetPebx64(); int _tmain(int argc, _TCHAR* argv[]) { PVOID64 Peb = 0; Peb = GetPebx64(); printf("Peb is 0x%p\r\n",Peb); HANDLE hHeap; char *heap; char str[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; //0x20 hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS,0x1000,0xffff); getchar(); //用於暫停,便於調試器附加 heap = (char*)HeapAlloc(hHeap,0,0x20); printf("Heap addr:0x%08p\r\n",heap); strcpy(heap,str); printf("str is %s\r\n",heap); cin>>Peb; HeapFree(hHeap,0,heap); //釋放 HeapDestroy(hHeap); cin>>Peb; return 0; }
其中GetPebx64()函數為使用.asm文件的匯編,通過gs:[0x60]獲得
.CODE GetPebx64 PROC mov rax,gs:[60h] ret GetPebx64 ENDP END
運行結果為
我們使用Windbg附加,查看PEB結構
0:001> dt _PEB 0x000007FFFFFDB000 ntdll!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x8 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsLegacyProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 +0x003 SpareBits : 0y000 +0x008 Mutant : 0xffffffff`ffffffff Void +0x010 ImageBaseAddress : 0x00000001`3f050000 Void +0x018 Ldr : 0x00000000`77522640 _PEB_LDR_DATA +0x020 ProcessParameters : 0x00000000`00242170 _RTL_USER_PROCESS_PARAMETERS +0x028 SubSystemData : (null) +0x030 ProcessHeap : 0x00000000`00240000 Void //進程默認堆的地址 +0x038 FastPebLock : 0x00000000`7752a960 _RTL_CRITICAL_SECTION +0x040 AtlThunkSListPtr : (null) +0x048 IFEOKey : (null) +0x050 CrossProcessFlags : 0 +0x050 ProcessInJob : 0y0 +0x050 ProcessInitializing : 0y0 +0x050 ProcessUsingVEH : 0y0 +0x050 ProcessUsingVCH : 0y0 +0x050 ProcessUsingFTH : 0y0 +0x050 ReservedBits0 : 0y000000000000000000000000000 (0) +0x058 KernelCallbackTable : (null) +0x058 UserSharedInfoPtr : (null) +0x060 SystemReserved : [1] 0 +0x064 AtlThunkSListPtr32 : 0 +0x068 ApiSetMap : 0x000007fe`ff710000 Void +0x070 TlsExpansionCounter : 0 +0x078 TlsBitmap : 0x00000000`77522590 Void +0x080 TlsBitmapBits : [2] 0x11 +0x088 ReadOnlySharedMemoryBase : 0x00000000`7efe0000 Void +0x090 HotpatchInformation : (null) +0x098 ReadOnlyStaticServerData : 0x00000000`7efe0a90 -> (null) +0x0a0 AnsiCodePageData : 0x000007ff`fffa0000 Void +0x0a8 OemCodePageData : 0x000007ff`fffa0000 Void +0x0b0 UnicodeCaseTableData : 0x000007ff`fffd0028 Void +0x0b8 NumberOfProcessors : 4 +0x0bc NtGlobalFlag : 0 +0x0c0 CriticalSectionTimeout : _LARGE_INTEGER 0xffffe86d`079b8000 +0x0c8 HeapSegmentReserve : 0x100000 //堆的默認保留大小 +0x0d0 HeapSegmentCommit : 0x2000 //堆的默認提交大小 +0x0d8 HeapDeCommitTotalFreeThreshold : 0x10000 //解除提交的總空閑塊閾值 +0x0e0 HeapDeCommitFreeBlockThreshold : 0x1000 //解除提交的單塊閾值 +0x0e8 NumberOfHeaps : 5 //進程堆的數量 +0x0ec MaximumNumberOfHeaps : 0x10 //ProcessHeaps數組目前的大小 +0x0f0 ProcessHeaps : 0x00000000`7752a6c0 -> 0x00000000`00240000 Void //一個數組,記錄了每一個堆的地址 +0x0f8 GdiSharedHandleTable : (null) +0x100 ProcessStarterHelper : (null) +0x108 GdiDCAttributeList : 0 +0x110 LoaderLock : 0x00000000`77527490 _RTL_CRITICAL_SECTION +0x118 OSMajorVersion : 6 +0x11c OSMinorVersion : 1 +0x120 OSBuildNumber : 0x1db1 +0x122 OSCSDVersion : 0x100 +0x124 OSPlatformId : 2 +0x128 ImageSubsystem : 3 +0x12c ImageSubsystemMajorVersion : 5 +0x130 ImageSubsystemMinorVersion : 2 +0x138 ActiveProcessAffinityMask : 0xf +0x140 GdiHandleBuffer : [60] 0 +0x230 PostProcessInitRoutine : (null) +0x238 TlsExpansionBitmap : 0x00000000`77522580 Void +0x240 TlsExpansionBitmapBits : [32] 1 +0x2c0 SessionId : 1 +0x2c8 AppCompatFlags : _ULARGE_INTEGER 0x0 +0x2d0 AppCompatFlagsUser : _ULARGE_INTEGER 0x0 +0x2d8 pShimData : (null) +0x2e0 AppCompatInfo : (null) +0x2e8 CSDVersion : _UNICODE_STRING "Service Pack 1" +0x2f8 ActivationContextData : 0x00000000`00140000 _ACTIVATION_CONTEXT_DATA +0x300 ProcessAssemblyStorageMap : (null) +0x308 SystemDefaultActivationContextData : 0x00000000`00130000 _ACTIVATION_CONTEXT_DATA +0x310 SystemAssemblyStorageMap : (null) +0x318 MinimumStackCommit : 0 +0x320 FlsCallback : 0x00000000`0027fe90 _FLS_CALLBACK_INFO +0x328 FlsListHead : _LIST_ENTRY [ 0x00000000`0027fa70 - 0x00000000`0027fa70 ] +0x338 FlsBitmap : 0x00000000`77522570 Void +0x340 FlsBitmapBits : [4] 3 +0x350 FlsHighIndex : 1 +0x358 WerRegistrationData : (null) +0x360 WerShipAssertPtr : (null) +0x368 pContextData : 0x00000000`00150000 Void +0x370 pImageHeaderHash : (null) +0x378 TracingFlags : 0 +0x378 HeapTracingEnabled : 0y0 +0x378 CritSecTracingEnabled : 0y0 +0x378 SpareTracingBits : 0y000000000000000000000000000000 (0)
我們可以使用dd 0x00000000`7752a6c0查看進程堆的地址
也可以使用!heap -h 命令查看進程堆的地址和分配的大小
而我們可以看到運行結果中分配的地址4A0A90,正好在段4A0000中。
2)堆的相關結構
我們首先了解下面幾個結構_HEAP_ENTRY,_HEAP_SEGMENT,_HEAP。
1._HEAP_ENTRY就是塊首,下面是一個64位系統堆塊的結構,我們在申請得到的地址減去0x10,就可以得到HEAP_ENTRY的首地址。
2._HEAP_SEGMENT是段結構,我們可以這么認為,堆申請內存的大小是以段為單位的,當新建一個堆的時候,系統會默認為這個堆分配一個段叫0號段,通過剛開始的new和malloc分配的空間都是在這個段上分配的,當這個段用完的時候,如果當初創建堆的時候指明了HEAP_GROWABLE這個標志,那么系統會為這個堆在再分配一個段,這個時候新分配的段就稱為1號段了,以下以此類推。每個段的開始初便是HEAP_SEGMENT結構的首地址,由於這個結構也是申請的一塊內存,所以它前面也會有個HEAP_ENTRY結構:
我們使用Windbg查看HEAP_SEGMENT結構如下:
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : Uint4B
+0x014 SegmentFlags : Uint4B
+0x018 SegmentListEntry : _LIST_ENTRY
+0x028 Heap : Ptr64 _HEAP //段所屬的堆
+0x030 BaseAddress : Ptr64 Void //段的基地址
+0x038 NumberOfPages : Uint4B //段的內存頁數
+0x040 FirstEntry : Ptr64 _HEAP_ENTRY //第一個堆塊(HEAP_ENTRY指針,堆塊一般位於HEAP_SEGMENT后面)
+0x048 LastValidEntry : Ptr64 _HEAP_ENTRY //堆塊的邊界值
+0x050 NumberOfUnCommittedPages : Uint4B //尚未提交的內存頁數
+0x054 NumberOfUnCommittedRanges : Uint4B //UnCommittedRanges數組元素數
+0x058 SegmentAllocatorBackTraceIndex : Uint2B
+0x05a Reserved : Uint2B
+0x060 UCRSegmentList : _LIST_ENTRY
3._HEAP結構
HEAP結構則是記錄了這個堆的信息,這個結構可以找到HEAP_SEGMENT鏈表入口,空閑內存鏈表的入口,內存分配粒度等等信息。HEAP的首地址便是堆句柄的值,但是堆句柄的值又是0號段的首地址也是堆句柄,何解?其實很簡單,0號段的HEAP_SEGMENT就在HEAP結構里面,HEAP結構類定義如這樣:
0:001> dt ntdll!_HEAP 4a0000 +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 0 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000000`004a0128 - 0x00000000`004a0128 ] +0x028 Heap : 0x00000000`004a0000 _HEAP +0x030 BaseAddress : 0x00000000`004a0000 Void +0x038 NumberOfPages : 0x10 +0x040 FirstEntry : 0x00000000`004a0a80 _HEAP_ENTRY +0x048 LastValidEntry : 0x00000000`004b0000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 0xe +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 +0x05a Reserved : 0 +0x060 UCRSegmentList : _LIST_ENTRY [ 0x00000000`004a1fe0 - 0x00000000`004a1fe0 ] +0x070 Flags : 0x1004 //堆標志 +0x074 ForceFlags : 4 //強制標志 +0x078 CompatibilityFlags : 0 +0x07c EncodeFlagMask : 0x100000 +0x080 Encoding : _HEAP_ENTRY +0x090 PointerKey : 0x5c50b3ba`3fc7668b +0x098 Interceptor : 0 +0x09c VirtualMemoryThreshold : 0xff00 //最大堆塊大小 +0x0a0 Signature : 0xeeffeeff //HEAP結構的簽名 +0x0a8 SegmentReserve : 0x100000 //段的保留空間大小 +0x0b0 SegmentCommit : 0x2000 //每次提交內存的大小 +0x0b8 DeCommitFreeBlockThreshold : 0x100 //解除提交的單塊閾值 +0x0c0 DeCommitTotalFreeThreshold : 0x1000 //解除提交的總空閑塊閾值 +0x0c8 TotalFreeSize : 0x151 //空閑塊的總大小 +0x0d0 MaximumAllocationSize : 0x000007ff`fffdefff //可分配的最大值 +0x0d8 ProcessHeapsListIndex : 5 //本堆在進程堆列表中的索引 +0x0da HeaderValidateLength : 0x208 //頭結構的驗證長度 +0x0e0 HeaderValidateCopy : (null) +0x0e8 NextAvailableTagIndex : 0 //下一個可用的堆塊標記索引 +0x0ea MaximumTagIndex : 0 //最大的堆塊標記索引 +0x0f0 TagEntries : (null) //指向用於標記堆塊的標記結構 +0x0f8 UCRList : _LIST_ENTRY [ 0x00000000`004a1fd0 - 0x00000000`004a1fd0 ] //UnCommitedRange Segments +0x108 AlignRound : 0x1f +0x110 AlignMask : 0xffffffff`fffffff0 //用於地址對齊的掩碼 +0x118 VirtualAllocdBlocks : _LIST_ENTRY [ 0x00000000`004a0118 - 0x00000000`004a0118 ] +0x128 SegmentList : _LIST_ENTRY [ 0x00000000`004a0018 - 0x00000000`004a0018 ] //段鏈表HEAP_SEGMENT +0x138 AllocatorBackTraceIndex : 0 +0x13c NonDedicatedListLength : 0 //用於記錄回溯信息 +0x140 BlocksIndex : 0x00000000`004a0230 Void +0x148 UCRIndex : (null) +0x150 PseudoTagEntries : (null) +0x158 FreeLists : _LIST_ENTRY [ 0x00000000`004a0ac0 - 0x00000000`004a0ac0 ] //空閑塊鏈表數組 +0x168 LockVariable : 0x00000000`004a0208 _HEAP_LOCK //用於串行化控制的同步對象 +0x170 CommitRoutine : 0x5c50b3ba`3fc7668b long +5c50b3ba3fc7668b +0x178 FrontEndHeap : (null) //用於快速釋放堆塊的"前端堆" +0x180 FrontHeapLockCount : 0 //"前端堆"的鎖定計數 +0x182 FrontEndHeapType : 0 '' //"前端堆"的類型 +0x188 Counters : _HEAP_COUNTERS +0x1f8 TuningParameters : _HEAP_TUNING_PARAMETERS
對比一下上面HEAP_SEGMENT的結構,可以發現HEAP中就包含一個HEAP_SEGMENT結構。
四、定位申請的內存
我們知道我們申請的內存地址為4A0A90,根據前面學習的,4A0A90-0x10 = 4A0A80就是_HEAP_ENTRY的地址,而該地址在段4a0000中
1)我們使用!heap -a 4a0000查該堆的內容
0:001> !heap -a 4a0000 Index Address Name Debugging options enabled 5: 004a0000 Segment at 00000000004a0000 to 00000000004b0000 (00002000 bytes committed) Flags: 00001004 ForceFlags: 00000004 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000100 DeCommit Total Thres: 00001000 Total Free Size: 00000151 Max. Allocation Size: 000007fffffdefff Lock Variable at: 00000000004a0208 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 004a0118 Uncommitted ranges: 004a00f8 004a2000: 0000e000 (57344 bytes) FreeList[ 00 ] at 00000000004a0158: 00000000004a0ac0 . 00000000004a0ac0 00000000004a0ab0: 00030 . 01510 [100] - free Segment00 at 004a0000: Flags: 00000000 Base: 004a0000 First Entry: 004a0a80 Last Entry: 004b0000 Total Pages: 00000010 Total UnCommit: 0000000e Largest UnCommit:00000000 UnCommitted Ranges: (1) Heap entries for Segment00 in Heap 00000000004a0000 address: psize . size flags state (requested size) 00000000004a0000: 00000 . 00a80 [101] - busy (a7f) 00000000004a0a80: 00a80 . 00030 [101] - busy (20) 00000000004a0ab0: 00030 . 01510 [100] 00000000004a1fc0: 01510 . 00040 [111] - busy (3d) 00000000004a2000: 0000e000 - uncommitted bytes.
可以看到內存粒度問16字節
Granularity: 16 bytes
繼續往下看可以發現堆中正好有地址為4a0a80的一項
00000000004a0a80: 00a80 . 00030 [101] - busy (20)
第一項為地址,第二項a80為上一項的堆塊大小,0x30為該堆塊的大小,[101]為是這個內存的標志位,最右邊的1表示內存塊被占用,然后busy(20)表示這塊內存被占用,申請的內存為0x20,加上塊首的大小為0x10,一共是0x30
2)我們知道了_HEAP_ENTRY的地址為4a0a80,我們查看該結構體
發現這里的Size和我們的0x30完全不符!
我們可以看上面的_HEAP結構,Win7下面的_HEAP結構比XP多了兩項
+0x07c EncodeFlagMask : 0x100000
+0x080 Encoding : _HEAP_ENTRY
相對於XP,Vista之后增加了對堆塊的頭結構(HEAP_ENTRY)的編碼。編碼的目的是引入隨機性,增加堆的安全性,防止黑客輕易就可以預測堆的數據結構內容而實施攻擊。在_HEAP結構中新增了如下兩個字段:
其中的EncodeFlagMask用來指示是否啟用編碼功能,Encoding字段是用來編碼的,編碼的方法就是用這個Encoding結構與每個堆塊的頭結構做亦或(XOR)
讀取_HEAP偏移為0x80的Encoding子結構:注意Size字段是從偏移8開始的兩個字節,不是從偏移0開始
我們使用dd查看我們的HEAP_ENTRY信息
做異或解碼:
0:001> ?2b552b39^29542b3a Evaluate expression: 33619971 = 00000000`02010003
低地址的word是Size字段,所以Size字段是0x3,因為是以0x10為內存粒度的,所以字節大小為
0:001> ?3*0x10 Evaluate expression: 48 = 00000000`00000030
也就是0x30,與我們顯示出的正好一致
3)內存中的數據
五、總結
1.在PEB中保存這進程的堆地址和數量。
2.HEAP結構記錄HEAP_SEGMENT的方式采用了鏈表,這樣不再受數組大小的約束,同時將HEAP_SEGMENT字段包含進HEAP,這樣各堆段的起始便統一為HEAP_SEGMENT,不再有xp下0號段與其他段那種區別,可以統一進行管理了。
3.每個HEAP_SEGMENT都有多個堆塊,每個堆塊包含塊首和塊身,塊身為我們申請得到的地址。
下一篇會研究堆溢出。
代碼鏈接:http://pan.baidu.com/s/1bpBm8W3
參考:
<軟件漏洞分析技術>
<漏洞戰爭>