windows和linux堆管理機制雖然呈現給用戶的效果是一樣的,大體思路也是差不太多,但是底層實現邏輯大相徑庭,很多地方和glibc的ptmalloc差別很大。網上資料零零散散,而且都是通過逆向手段分析,所以每個版本資料還多少有些差異,在這里對windows堆管理機制做個歸納,學習一下。
接口
在glibc中,通常我們調用的分配函數就是malloc、calloc、realloc,但是這三個函數本質都差不多,本體還是malloc函數的邏輯。
在windows中,堆的分配函數就比較多了這里我們逐一介紹一下。
函數原型 | 參數 | 說明 |
---|---|---|
HeapAlloc(HANDLE hHeap, DWORD dwFlags, size_t dwSize) | hHeap為進程堆開始位置,flag就是標志,size就是大小。 | 內存是指定位置開始分配,且分配的內存不可移動。對應的釋放函數是HeapFree |
GlobalAlloc(UINT uFlags, size_t dwBytes) | uflag標志信息:GMEM_FIXED分配固定內存,返回一個指針;GMEM_MOVABLE分配活動內存,返回內存對象句柄,這個句柄可以利用GlobalLock轉化為指針。 | 從全局堆中分配內存,相應的釋放函數是GlobalFree |
LocalAlloc(UINT uFlags,size_t dwBytes) | 參數同GlobalAlloc | 對應的釋放函數為LocalFree |
VirtualAlloc(LPVOID lpAddress, size_t dwSize,DWORD flAllocationType,DWORD flProtect) | 意義和參數名一樣 | 對應的釋放函數為VirtualFree |
malloc | 和linux一樣 | free |
HeapCreate(DWORD flOptions , DWORD dwInitialSize , DWORD dwMaxmumSize); | flOptions:堆的可選屬性。這些標記影響以后對這個堆的函數操作,函數有:HeapAlloc , HeapFree , HeapReAlloc , HeapSize .wInitialSize:堆的初始大小,單位為Bytes。這個值決定了分配給堆的初始物理空間大小。這個值將向上舍入知道下個page boundary(頁界)。若需得到主機的頁大小,使用GetSystemInfo 函數。dwMaxmumSize:如果該參數是一個非零的值,它指定了這個堆的最大大小,單位為Bytes。 | 用來創造一塊堆區域 |
從接口信息可以看出來,windows和linux堆的一個很大的不同點就是windows的堆有很多,linux的話都在一個區域里。
另外,globalalloc和localalloc在現代的win32以后的版本中沒有區別,這兩個函數剛開始是在16位windows中使用有區別的。在win32中每個程序都有一個自己的缺省堆,所以全局堆和局部堆在win32中都指向這個缺省堆,這倆沒區別,甚至釋放函數都可以混着用。等效於heapAlloc(GetProcessHeap(),flag,size)
malloc函數雖然不像其他的函數那樣指明了堆區,但是實際上windows中malloc函數在初始化的時候自己HeapCreate了一段堆內存區域供他使用。每個模塊的malloc都有自己的堆區域,所以不能一個dllfree掉另一個dll的堆指針。
概覽
windows堆管理機制較之於linux比較復雜,管理機制也分好幾套。
UWP即Windows通用應用平台,Windows 10中的Universal Windows Platform簡稱。UWP不同於傳統pc上的exe應用,可以在所有Windows10設備上運行。UWP應用程序進程至少包括三個堆:(1) 默認堆(2) 用於向進程的會話Csrss.exe實例傳遞大參數的共享堆。這是由CsrClientConnectToServer函數創建的,該函數在Ntdll.dll完成的進程初始化早期執行。(3) 由Microsoft C運行庫創建的堆。該堆是由C/C++內存分配函數(如Maloc、Free、等)內部使用的堆。
在Windows10和服務器2016之前,只有一種堆類型,我們稱之為NT堆。Windows 10引入了一種稱為段堆(segment heap)的新堆類型。這兩種堆類型包括公共元素,但結構和實現方式不同。默認情況下,所有UWP應用程序和某些系統進程都使用段堆,而所有其他進程都使用NT堆。這可以在注冊表中更改。
大部分場合默認使用的堆都是NT heap
,segment heap
通常會在winapp或者某些特殊的進程(核心進程)中會使用到。
而在NT heap中又分為前端管理和后端管理兩套不同的堆分配管理策略。
而windows程序的堆又分為兩種:
第一種叫做processheap,它包括兩個部分,一個是default heap,其地址信息回存放於_PEB中,在調用malloc等函數的時候會用到。第二個是crtheap,但是其本質一樣是default,封裝了一些別的信息,存放於crt_heap中。
第二種叫做private heap,也就是我們通過HeapCreate創建的堆。
NT堆
大體流程
大體流程就是windows app調用msvcrt140.dll函數中的形如malloc、free等函數后,會調用kernel32.dll中的堆管理api,接着調用ntdll中的管理機制。
這里的管理機制中,LFH就是前端管理的核心,那么整個流程具體來說就是如下的邏輯:
(1) 小於或等於16368字節,使用LFH分配器。這與NT堆的邏輯類似。如果LFH還沒有啟動,那么將使用可變大小(VS)分配器。(2) 對於小於或等於128 KB的大小(不由LFH提供服務),使用VS分配器。VS和LFH分配器都使用后端根據需要創建所需的堆子段。(3) 大於128 KB且小於或等於508 KB的分配由堆后端直接提供服務。(4) 大於508kb的分配直接調用內存管理器(VirtualAlloc),因為這些分配非常大,因此使用默認的64kb分配粒度(並舍入到最接近的頁面大小)就足夠了。
如果LFH沒有啟用,那么就直接調用后端堆管理機制。
啟用LFH后,第一次申請或者LFH內部空間不夠時會從后端堆中申請一段大空間來使用。
如果LFH搞定了申請,那么直接由LFH返回,不調用后端。
可以看出前端分配器就有點類似於linux中的fastbin。
這里要說明一下,在之前的windows版本中,前端分配器並不是LFH,而是look aside表,也就是0day一書中提到的快表,但是windows10中已經不適用lookaside了。
數據結構
由前面的內容可以看出來windows有很多的堆,從linux的管理機制中,我們知道每個堆都由一個重要的數據結構malloc_state來管理,這些個mallocstate就稱之為arena,主線程叫main_arena,別的叫thread_arena,這些個數據結構由指針鏈接形成鏈表。
那么在windows的堆管理機制中,同樣也需要類似於arena這樣的結構體。但是不同於linux,每個這樣的堆管理結構體是存放於每個堆段的頭部,並不是在某些dll的數據段中。
這個數據結構就稱之為_HEAP,長這個樣子:
+0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x008 SegmentSignature : Uint4B //用來判斷NT還是Segment +0x00c SegmentFlags : Uint4B +0x010 SegmentListEntry : _LIST_ENTRY +0x018 Heap : Ptr32 _HEAP +0x01c BaseAddress : Ptr32 Void +0x020 NumberOfPages : Uint4B +0x024 FirstEntry : Ptr32 _HEAP_ENTRY +0x028 LastValidEntry : Ptr32 _HEAP_ENTRY +0x02c NumberOfUnCommittedPages : Uint4B +0x030 NumberOfUnCommittedRanges : Uint4B +0x034 SegmentAllocatorBackTraceIndex : Uint2B +0x036 Reserved : Uint2B +0x038 UCRSegmentList : _LIST_ENTRY +0x040 Flags : Uint4B +0x044 ForceFlags : Uint4B +0x048 CompatibilityFlags : Uint4B +0x04c EncodeFlagMask : Uint4B //用來表示是否encode header +0x050 Encoding : _HEAP_ENTRY //用來encode的cookie +0x058 Interceptor : Uint4B +0x05c VirtualMemoryThreshold : Uint4B +0x060 Signature : Uint4B +0x064 SegmentReserve : Uint4B +0x068 SegmentCommit : Uint4B +0x06c DeCommitFreeBlockThreshold : Uint4B +0x070 DeCommitTotalFreeThreshold : Uint4B +0x074 TotalFreeSize : Uint4B +0x078 MaximumAllocationSize : Uint4B +0x07c ProcessHeapsListIndex : Uint2B +0x07e HeaderValidateLength : Uint2B +0x080 HeaderValidateCopy : Ptr32 Void +0x084 NextAvailableTagIndex : Uint2B +0x086 MaximumTagIndex : Uint2B +0x088 TagEntries : Ptr32 _HEAP_TAG_ENTRY +0x08c UCRList : _LIST_ENTRY +0x094 AlignRound : Uint4B +0x098 AlignMask : Uint4B +0x09c VirtualAllocdBlocks : _LIST_ENTRY +0x0a4 SegmentList : _LIST_ENTRY +0x0ac AllocatorBackTraceIndex : Uint2B +0x0b0 NonDedicatedListLength : Uint4B +0x0b4 BlocksIndex : Ptr32 Void //用來管理后端的chunk列表 +0x0b8 UCRIndex : Ptr32 Void +0x0bc PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY +0x0c0 FreeLists : _LIST_ENTRY //用來管理后端所有的freechunk的鏈表 +0x0c8 LockVariable : Ptr32 _HEAP_LOCK +0x0cc CommitRoutine : Ptr32 long +0x0d0 StackTraceInitVar : _RTL_RUN_ONCE +0x0d4 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA +0x0e4 FrontEndHeap : Ptr32 Void //指向前端堆的結構 +0x0e8 FrontHeapLockCount : Uint2B +0x0ea FrontEndHeapType : UChar +0x0eb RequestedFrontEndHeapType : UChar +0x0ec FrontEndHeapUsageData : Ptr32 Wchar //指向前端管理chunk的列表 +0x0f0 FrontEndHeapMaximumIndex : Uint2B +0x0f2 FrontEndHeapStatusBitmap : [257] UChar +0x1f4 Counters : _HEAP_COUNTERS +0x250 TuningParameters : _HEAP_TUNING_PARAMETERS
其中比較重要的字段意義都寫在了注釋中。
在linux中,堆是由一個個chunk構成的,在windows中也一樣,也是由一個個堆塊構成。
這樣一個堆塊的結構,稱之為_HEAP_ENTRY。這個結構比較奇怪,似乎有好幾種實現方式?以為同樣偏移有不同的意思。
ntdll!_HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : Ptr64 Void +0x008 Size : Uint2B +0x00a Flags : UChar +0x00b SmallTagIndex : UChar +0x008 SubSegmentCode : Uint4B +0x00c PreviousSize : Uint2B +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f UnusedBytes : UChar +0x008 CompactHeader : Uint8B +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY +0x000 Reserved : Ptr64 Void +0x008 FunctionIndex : Uint2B +0x00a ContextValue : Uint2B +0x008 InterceptorValue : Uint4B +0x00c UnusedBytesLength : Uint2B +0x00e EntryOffset : UChar +0x00f ExtendedBlockSignature : UChar +0x000 ReservedForAlignment : Ptr64 Void +0x008 Code1 : Uint4B +0x00c Code2 : Uint2B +0x00e Code3 : UChar +0x00f Code4 : UChar +0x00c Code234 : Uint4B +0x008 AgregateCode : Uint8B
在老外逆出來的c版本中是這樣的:
//0x10 bytes (sizeof)struct _HEAP_ENTRY{ union { struct _HEAP_UNPACKED_ENTRY UnpackedEntry; //0x0 struct { VOID* PreviousBlockPrivateData; //0x0 union { struct { USHORT Size; //0x8 UCHAR Flags; //0xa UCHAR SmallTagIndex; //0xb }; struct { ULONG SubSegmentCode; //0x8 USHORT PreviousSize; //0xc union { UCHAR SegmentOffset; //0xe UCHAR LFHFlags; //0xe }; UCHAR UnusedBytes; //0xf }; ULONGLONG CompactHeader; //0x8 }; }; struct _HEAP_EXTENDED_ENTRY ExtendedEntry; //0x0 struct { VOID* Reserved; //0x0 union { struct { USHORT FunctionIndex; //0x8 USHORT ContextValue; //0xa }; ULONG InterceptorValue; //0x8 }; USHORT UnusedBytesLength; //0xc UCHAR EntryOffset; //0xe UCHAR ExtendedBlockSignature; //0xf }; struct { VOID* ReservedForAlignment; //0x0 union { struct { ULONG Code1; //0x8 union { struct { USHORT Code2; //0xc UCHAR Code3; //0xe UCHAR Code4; //0xf }; ULONG Code234; //0xc }; }; ULONGLONG AgregateCode; //0x8 }; }; };};
這里的話主要是因為一個chunk(也就是_HEAP_ENTRY,這么叫方便些)有不同的狀態,所以就union一下。
那么具體來說,一個chunk有三種狀態:使用(allocated)、釋放(free)、虛擬(virtual alloc)(mmap出來的chunk)。
使用狀態(inuse):
偏移&名稱 | 大小 | 意義 |
---|---|---|
0x0: PreviousBlockPrivateData | 8bytes | 前一個chunk的數據,由於需要0x10對其所以算在頭部 |
0x8: Size | 2bytes | 本chunk的大小,這里的大小是 real_size >> 4 |
0xa: Flag | 1byte | 表示當前chunk是否inuse |
0xb: smallTagIndex | 1byte | 前三個byte(size和flag)做xor后的值,驗證作用 |
0xc: PreviousSIze | 2bytes | 表示前一個chunk的size,同樣也是右移4位后的值 |
0xe: SegmentOffset | 1byte | 某些情況下用來找segment |
0xf: UnusedBytes | 1byte | 記錄malloc后所剩的chunk大小,可用來判斷是前端或者后端 |
0x10: userdata | (Size << 4) -0x10 | 用戶數據區域 |
釋放狀態(unused):
偏移&名稱 | 大小 | 意義 |
---|---|---|
0x0: PreviousBlockPrivateData | 8bytes | 前一個chunk的數據,由於需要0x10對其所以算在頭部 |
0x8: Size | 2bytes | 本chunk的大小,這里的大小是 real_size >> 4 |
0xa: Flag | 1byte | 表示當前chunk是否inuse |
0xb: smallTagIndex | 1byte | 前三個byte(size和flag)做xor后的值,驗證作用 |
0xc: PreviousSIze | 2bytes | 表示前一個chunk的size,同樣也是右移4位后的值 |
0xe: SegmentOffset | 1byte | 某些情況下用來找segment |
0xf: UnusedBytes | 1byte | 恆為0 |
0x10:flink | 8bytes | 指向list中后一個chunk |
0x18: blink | 8bytes | 指向list中前一個chunk |
virtualAlloc狀態:
偏移&名稱 | 大小 | 意義 |
---|---|---|
0x0: flink | 8bytes | 雙向鏈表指針 |
0x8: blink | 8bytes | 雙向鏈表指針 |
0x10: size | 2bytes | 這里的size是unusedsize,且沒有進行移位 |
0x12: flag | 1byte | |
0x13: smallTagIndex | 1byte | |
0x14: PreviousSIze | 2bytes | |
0x16: SegmentOffset | 1byte | |
0x17:UnusedBytes | 1byte | 恆為4 |
這里的virtualalloc的chunk狀態可能有些勘誤,因為網上關於這里的資料比較少。
這里要說明一下,關於chunk頭部的驗證:
在之前的_HEAP結構體中有一個encoding字段,這個cookie就是為了加密頭部來用的,具體來說就是xor一下,所以在對chunk進行操作的時候會驗證其有效性。同時SmallTagIndex也會對flag和size做一個驗證。
free_list
這個是在_HEAP中的一個指針,指向的是free的chunk的鏈表,雙向有序鏈表。在一個chunk被釋放后,會插入到這個list中(類似於unsortedbin)
BlocksIndex
這個指針的結構是_HEAP_LIST_LOOKUP。
這一結構體長這個樣子:
//0x38 bytes (sizeof)struct _HEAP_LIST_LOOKUP{ struct _HEAP_LIST_LOOKUP* ExtendedLookup; //0x0 指向下一個lookup,通常chunk會更大 ULONG ArraySize; //0x8 管理的最大chunk大小(右移4位后) ULONG ExtraItem; //0xc ULONG ItemCount; //0x10 當前管理的chunk數 ULONG OutOfRangeItems; //0x14 超出該結構體管理的chunk數量 ULONG BaseIndex; //0x18 該結構管理的chunk的起始index(listhint中) struct _LIST_ENTRY* ListHead; //0x20 指向freelist的head ULONG* ListsInUseUlong; //0x28 判斷listhint中是否有合適大小的chunk, 是一個bitmap struct _LIST_ENTRY** ListHints; //0x30 用來指向對應大小chunk的array,0x10遞增};
這兩個鏈表之間的關系如下圖(摘自angelboy的slide)
可以看到,所有的chunk都是存儲在freelist中,而blockindex用來定位這些在freelist中的chunk的位置,快速找到合適大小的chunk。
NT后端管理機制
管理機制無非就是申請和釋放的邏輯。
申請
申請時,分為三種情況:
1.Size<=0x40002.0x4000<size<=0xff0003.Size>0xff000
第一種情況,當size<=0x4000時:1.查看size對應到的FrontEndHeapStatusBitmap使否有啟用LFH如果有的話會對對應到的FrontEndHeapUsageData加上0x21,並且檢查值是否超過0xff00或者 &0x1f 后超過0x10 : 超過則啟用LFH。
2.接下來首先查看對應的ListHint中是否有chunk,有則優先分配(先看快表)如果有大小合適的chunk在ListHint上則移除ListHint,並且查看chunk的Flink⼤⼩是否size與此chunk相同(注意FreeLists按大小排序):為空則清空,否則將LintHint填上Flink。最后unlink該chunk,把此chunk從linkedlist中移除返回給user,並將header xor回去(返回時header被encode)
3: 若沒有大小合適的chunk: 則從比較⼤的ListHint中找,有找到比較大的chunk后,同樣查看下⼀塊chunk的size是不是一樣大小,有則填上,並且unlink該chunk, 從freelist移除。最后將chunk做切割,剩下的⼤⼩重新加入Freelist,如果可以放進ListHint就會放進去,將切割好的chunk返回給使用者(chunk header同樣encode)
4.如果FreeList中沒有可以操作的chunk,則嘗試ExtendHeap來加大heap空間,再從extend出來的heap取chunk,接着像上面一樣分割返回(chunk header encode),剩下的放回ListHint
第二種情況,當0x4000<size<=0xff000
基本和第一種情況差不多,但是沒有LFH操作。
第三種情況,當size大於0xff000
直接使⽤ZwAllocateVirtualMemory,類似直接mmap一大塊空間,並且會插入到_HEAP->VirtualAllocdBlocks這個linked list中(這個linked list用來串接該HeapVirtualAllocate出來的區段)
釋放
分兩種情況,大於小於0xff000分別討論
size<=0xff000
1:首先檢查alignment,利⽤unusedbyte判斷該chunk狀態如果是非LFH模式下,會對對應到的FrontEndHeapUsageData減12:接下來會判斷前后的chunk是否為freed,是的話就合並此時會把可以合並的chunk unlink,並從ListHint移除(移除⽅式與前⾯相同,查看下一個chunk是不是相同⼤⼩,是則補上ListHint)3:合並之后,update size&prevsize,然后查看是不是最前跟最后,是就插入,否則就從ListHint中插入,並且update ListHint,插入 時也會對linked list進行檢查(此檢查不會abort,其原因主要是因為不做unlink寫入)
具體的流程可以參考angelboy的slide。
size > 0xff000
檢查該chunk的linkedlist並從_HEAP->VirtualAllocdBlocks移除接着使⽤RtlpSecMemFreeVirtualMemory將chunk整個munmap掉
NT前端管理機制
也就是之前一直提到的LFH(low fragment heap),在win10主要使用,只有在非調試狀態下才會啟用,根據之前的內容也不難推測,是用來管理大小小於0x4000的chunk的。
要想觸發LFH,需要分配18個相同大小的堆塊,他們可以不連續。
如何查看LFH是否開啟呢?在windbg中,可以通過dt _HEAP [Heap Address]
查看heap結構體,在偏移0x0d6處FrontEndHeapType字段可以揭示是否開啟了LFH,如果為0則說明后端堆在管理,為1就是lookaside策略,2就說明是LFH。
另一種方式可以查看一個chunk是否屬於LFH管理,通過!heap -x [Chunk Address]
來查看
數據結構
相關的重要的數據結構為_LFH_HEAP,在 _HEAP結構中,frontEndHeap指針指向這一結構。
這個結構的話不同版本windows還不一樣,貼個圖:
這里看win10的就可以
0:001> dt _LFH_HEAPntdll!_LFH_HEAP +0x000 Lock : _RTL_SRWLOCK +0x008 SubSegmentZones : _LIST_ENTRY +0x018 Heap : Ptr64 Void //指向對應的_HEAP +0x020 NextSegmentInfoArrayAddress : Ptr64 Void +0x028 FirstUncommittedAddress : Ptr64 Void +0x030 ReservedAddressLimit : Ptr64 Void +0x038 SegmentCreate : Uint4B +0x03c SegmentDelete : Uint4B +0x040 MinimumCacheDepth : Uint4B +0x044 CacheShiftThreshold : Uint4B +0x048 SizeInCache : Uint8B +0x050 RunInfo : _HEAP_BUCKET_RUN_INFO +0x060 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY +0x2a0 MemoryPolicies : _HEAP_LFH_MEM_POLICIES +0x2a4 Buckets : [129] _HEAP_BUCKET //用來尋找配置⼤⼩對應到 Block ⼤⼩的陣列 +0x4a8 SegmentInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO //不同大小對應到不同的segmentinfo結構,主要管理對應的subsegment的信息 +0x8b0 AffinitizedInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO +0xcb8 SegmentAllocator : Ptr64 _SEGMENT_HEAP +0xcc0 LocalData : [1] _HEAP_LOCAL_DATA //其中有個地址指向LFH本身,用來找回LFH
可以看到這結構體類似於_HEAP,包含了很多指針信息,這其中又有兩個結構體需要分析一下。
_HEAP_BUCKET
ntdll!_HEAP_BUCKET +0x000 BlockUnits : Uint2B //分配block大小>>4 +0x002 SizeIndex : UChar //使用大小>>4 +0x003 UseAffinity : Pos 0, 1 Bit +0x003 DebugFlags : Pos 1, 2 Bits +0x003 Flags : UChar
_HEAP_LOCAL_SEGMENT_INFO
ntdll!_HEAP_LOCAL_SEGMENT_INFO +0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA//對應 _LFH_HEAP->LocalData ,便於從 SegmentInfo 找回 _LFH_HEAP +0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT//對應已分配的Subsegment,用於管理userblock記錄剩余多少chunk、最大分配書等等 +0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT//_HEAP_SUBSEGMENT array//存放對應此SegmentInfo且還有可以分配chunk給user的Subsegment//當ActiveSubsegment⽤完時,將從這里填充,並置換掉ActiveSubsegment +0x090 SListHeader : _SLIST_HEADER +0x0a0 Counters : _HEAP_BUCKET_COUNTERS +0x0a8 LastOpSequence : Uint4B +0x0ac BucketIndex : Uint2B +0x0ae LastUsed : Uint2B +0x0b0 NoThrashCount : Uint2B
其中,cachedItems比較重要,其結構體為_HEAP_SUBSEGMENT:
ntdll!_HEAP_SUBSEGMENT +0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO//指向對應的_HEAP_LOCAL_SEGMENT_INFO +0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER //記錄要分配出去的chunk所在位置,開頭存儲一些metadata來管理這些chunk +0x010 DelayFreeList : _SLIST_HEADER +0x020 AggregateExchg : _INTERLOCK_SEQ //用來管理對應的userblock中還有多少freedchunk,LFH以此來判斷是否從此userblock中分配 +0x024 BlockSize : Uint2B //此userblock中每個chunk的大小 +0x026 Flags : Uint2B +0x028 BlockCount : Uint2B //此userblock中chunk的總數 +0x02a SizeIndex : UChar //該userblock對應的sizeindex +0x02b AffinityIndex : UChar +0x024 Alignment : [2] Uint4B +0x02c Lock : Uint4B +0x030 SFreeListEntry : _SINGLE_LIST_ENTRY
_INTERLOCK_SEQ
ntdll!_INTERLOCK_SEQ +0x000 Depth : Uint2B //該userblock剩余freechunk的數量 +0x002 Hint : Pos 0, 15 Bits +0x002 Lock : Pos 15, 1 Bit +0x002 Hint16 : Uint2B +0x000 Exchg : Int4B
_HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER +0x000 SFreeListEntry : _SINGLE_LIST_ENTRY +0x000 SubSegment : Ptr64 _HEAP_SUBSEGMENT //指回對應的_HEAP_SUBSEGMENT +0x008 Reserved : Ptr64 Void +0x010 SizeIndexAndPadding : Uint4B +0x010 SizeIndex : UChar +0x011 GuardPagePresent : UChar +0x012 PaddingBytes : Uint2B +0x014 Signature : Uint4B +0x018 EncodedOffsets : _HEAP_USERDATA_OFFSETS //用於檢查Userdata的頭部字段 +0x020 BusyBitmap : _RTL_BITMAP_EX //bitmap,用來記錄使用的chunk +0x030 BitmapData : [1] Uint8B
其中的EncodingOffset字段就是個驗證,在USERBLOCK初始化時會生成這個數值作為驗證用,其數值具體來說是以下四個值的xor:
(sizeof(userblock header)) | (blockunit*0x10 << 16)LFHkeyUserblock addrLFH_HEAP addr
在_HEAP_USERDATA_HEADER之后就是一系列的chunks。
在LFH中,chunk雖然還是chunk,但是頭部信息和之前學的chunk不一樣
偏移&名稱 | 大小 | 意義 |
---|---|---|
0x0: PreviousBlockPrivateData | 8bytes | 前一個chunk的數據,由於需要0x10對其所以算在頭部 |
0x8: SubSegmentCode | 4bytes | encode過的metadata,用來推回userblock的位置 |
0xc: PreviousSIze | 2bytes | 該chunk在userblock中的index |
0xe: SegmentOffset | 1byte | |
0xf: UnusedByte | 1byte | 恆為0x80,用來判斷是否為LFH的freechunk |
0x10: UserData |
其中,SubSegmentCode的值為這四個值的xor:
_HEAP addressLFHkeyChunk address >> 4((chunk address) - (UserBlock address)) << 12
搞了這么多結構體,頭疼眼暈,好在angelboy大佬給出了LFHheap的overview:
管理機制
在之前的后端管理邏輯中已經對LFH這一概念有所提及。
申請
LFH涉及到初始化工作,具體來說就是查看size對應到的FrontEndHeapStatusBitmap使否有啟用LFH如果有的話會對對應到的FrontEndHeapUsageData加上0x21,並且檢查值是否超過0xff00或者 &0x1f 后超過0x10 : 超過則啟用LFH。也就是在FrontEndHeapUsageData[x] & 0x1F > 0x10
的時候,置位_HEAP->CompatibilityFlag |= 0x20000000
,下一次Allocate
就會對LFH進行初始化:
-
首先會ExtendFrontENdUsageData,也就是將這個數值增大,然后增加更大的_HEAP->BlocksIndex,因為這里_HEAP->BlocksIndex可以理解為一個_HEAP_LIST_LOOKUP結構的單向鏈表(參考上面Back-End的解釋),且默認初始情況下只存在一個管理比較小的(0x0 ~ 0x80)的chunk的_HEAP_LIST_LOOKUP,所以這里會擴展到(0x80 ~ 0x400),即在鏈表尾追加一個管理更大chunk的_HEAP_LIST_LOOKUP結構體結點。
在 FrontEndHeapUsageData 寫上對應的index,此時 enable LFH 范圍變為 (idx: 0-0x400)FrontEndHeapUsageData中分為兩部分:對應用於判斷LFH是否需要初始化的map以及已經enable LFH的chunk size (例如enable malloc 0x50大小的chunk,則寫入0x50>>4=5) 原BlocksIndex進行擴展,即新建一個BlocksIndex,寫入原BlocksIndex->ExtendedLookup,進行擴展
-
建立並初始化_HEAP->FrontEndHeap(通過mmap),即初始化_LFH_HEAP的一些metadata。
-
建立並初始化_LFH_HEAP->SegmentInfoArrays[x],在SegmentInfoArrays[BucketIndex]處填上對應的_HEAP_LOCAL_SEGMENT_INFO結構體指針。
在初始化后,從LFH分配內存的邏輯為:
1.先看ActiveSubSegment中是否有可以分配的chunk,這個是否有的判斷標准就是ActiveSubSegment->AggregateExchg->depth
2.如果沒有就從CachedItem中找,找到的話會把ActiveSubSegment換成CachedItem中的SubSegment
到了這一步時,LFH分配器就找到了UserBlock,UserBlock中有很多的chunk可以供用戶使用,LFH選取chunk的標准如下:
1.首先從RtlpLowFragHeapRandomData中下標為x處取一個值,這個名字很長的數組是一個長度為256byte的元素大小范圍為0-0x7f的隨機數數組,每次取,x都會自增1,如果x超過了256,那么x = rand()%256.
2.最終獲取的index為RtlLowFragHeapRandomData[x]*maxidx >> 7,檢查bitmap是否為0,如果沖突了的話就往后找最近的
3.檢查(unused byte & 0x3f)!=0(表示chunk是free的)
4.最后設置index(chunk頭部中的previoussize)和unusedbyte返回給用戶。
釋放
1.將unused位改成0x80
2.根據頭部中的字段找到userblock,然后找會Subsegment,根據index設置bitmap
3.更新ActiveSubSegment->AggregateExchg
4.如果釋放的chunk不屬於當前的ActiveSubSegment就看一下能不能放到cachedItems中,可以就放進去。
利用方式
地址問題
先不考慮如何利用的事,首先關注最基本的問題,要泄漏什么地址?地址在哪?
假設我們有了任意內存地址讀寫,那么我們就需要泄漏一些關鍵的函數地址,比如說system,以及攻擊的目標點,比如棧地址。
不同於linux,windows有一堆dll函數庫。
這里,根據angelboy的slide,需要泄漏的地址為kernelbase以及stackaddress,這兩個地址在kernel32.dll。
那么如何泄漏ntdll呢?_HEAP_LOCK相關的信息會指向ntdll,具體來說,就是_HEAP->LockVariable.Lock以及CriticalSection->DebugInfo
在ntdll!PebLdr中,_PEB_LDR_DATA可以找到所有dll的位置。
同樣可以從IAT表中找到kernel32,不過需要先泄漏binary的地址。
在KERNELBASE!BasepFilterInfo中,會有大概率包含stack的指針,這個主要是因為內存沒有初始化。
如果這個上面沒有想要的地址,可以從PEB向后算一個page,通常會是TEB上,這上面也會有stack的地址信息。
攻擊的話,angelboy提出的方式就是泄漏地址,然后攻擊棧寫rop或者shellcode。
后端利用方式
unlink
和linux中的unlink很像(都是雙向鏈表的節點移除),但是繞過條件和linux不同,因為頭部的信息不同,需要對一些encode的字段構造一下。還有就是flink和blink指向的是userdata部分。
具體構造就是p -> fd = &p-8, p->bk = &p.
前端利用方式
angelboy同樣是只是草草的介紹了下如果有了uaf的話,如何繞過隨機在LFHuserblock中分配到指定chunk的方式,具體來說就是填滿其他的,下一次肯定就會落到目標點。那么有了uaf之后呢,劫持哪些指針劫持到哪里並沒有說明。所以這里的話還需要后續調試的時候整理。
具體怎么攻擊才叫合理?哪些攻擊面呢?
由於Angelboy給的利用方式太少,而且比較籠統局限,所以我又參考了別的資料,想找到一些類似於linux堆利用手法的攻擊方式。
然而現實打了我一巴掌,根據冠城大佬的ppt,在windows中,想通過攻擊堆的頭部或者其他字段來進行getshell幾乎不可能,因為windows堆的防御機制十分嚴格。堆中比較合理的攻擊手法似乎就只有unlink或者其他形式的修改函數指針的方式。