当前位置: 首页 > 技术干货 > windows 堆分析

windows 堆分析

发表于:2021-09-03 14:27 作者: s3cunDa 阅读数(2560人)

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: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部
        0x8: Size2bytes本chunk的大小,这里的大小是 real_size >> 4
        0xa: Flag1byte表示当前chunk是否inuse
        0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用
        0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值
        0xe: SegmentOffset1byte某些情况下用来找segment
        0xf: UnusedBytes1byte记录malloc后所剩的chunk大小,可用来判断是前端或者后端
        0x10: userdata(Size << 4) -0x10用户数据区域

        释放状态(unused):

        偏移&名称大小意义
        0x0: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部
        0x8: Size2bytes本chunk的大小,这里的大小是 real_size >> 4
        0xa: Flag1byte表示当前chunk是否inuse
        0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用
        0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值
        0xe: SegmentOffset1byte某些情况下用来找segment
        0xf: UnusedBytes1byte恒为0
        0x10:flink8bytes指向list中后一个chunk
        0x18: blink8bytes指向list中前一个chunk

        virtualAlloc状态:

        偏移&名称大小意义
        0x0: flink8bytes双向链表指针
        0x8: blink8bytes双向链表指针
        0x10: size2bytes这里的size是unusedsize,且没有进行移位
        0x12: flag1byte
        0x13: smallTagIndex1byte
        0x14: PreviousSIze2bytes
        0x16: SegmentOffset1byte
        0x17:UnusedBytes1byte恒为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: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部
                          0x8: SubSegmentCode4bytesencode过的metadata,用来推回userblock的位置
                          0xc: PreviousSIze2bytes该chunk在userblock中的index
                          0xe: SegmentOffset1byte
                          0xf: UnusedByte1byte恒为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或者其他形式的修改函数指针的方式。