windows 堆分析


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 heapsegment heap通常會在winapp或者某些特殊的進程(核心進程)中會使用到。

而在NT heap中又分為前端管理和后端管理兩套不同的堆分配管理策略。

而windows程序的堆又分為兩種:

第一種叫做processheap,它包括兩個部分,一個是default heap,其地址信息回存放於_PEB中,在調用malloc等函數的時候會用到。第二個是crtheap,但是其本質一樣是default,封裝了一些別的信息,存放於crt_heap中。

第二種叫做private heap,也就是我們通過HeapCreate創建的堆。

 

NT堆

大體流程

958f1cf6c271a0c79a8018d5343b6971.png

大體流程就是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)

f041f06ba545689720376f485f2a4be4.png

可以看到,所有的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還不一樣,貼個圖:

6e88708d4519c753d1925cdfc4a13103.png

這里看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:

2d13cf26a92cd86b2dcb8855ed54efa9.png

管理機制

在之前的后端管理邏輯中已經對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或者其他形式的修改函數指針的方式。

 

實驗地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM