windows用户态堆分析

序言

一直偷懒, 没遇到winheap相关利用场景, 所以一直拖着没有学习winheap相关知识, 结果面试时遇到直接尬住

与linux不同, 由于windows闭源的原因, 要想阅读源码只能通过反编译的方式, 这显然不利于学习, 但好在网络上有一群热爱分享的前辈已经整理好了许多相关的知识

之前已经学习过linux下的glibc_ptmalloc2和kernel_slab内存管理器模型, 学习了许多利用方式, 在windows下会发现, 其实本质上并没有过于巨大的差异, 有前者的基础能很好的帮助理解winheap

Win10Heap

glibc会随着时代在进步, 显然windows更不会原地踏步, win98, winxp, win7, win10的堆内存管理肯定是存在差异的

尽管现在依然有许多win老设备坚挺在一线, 但win10/11才应该算是更贴近时代的, 所以我们从win10开始切入

Angel Boy有一篇slide专门用于讲解win10下heap的利用, 十分适合从零开始的winheap学习

win10下的内存管理器较为复杂, 主要可以被归为两类

  1. NT Heap
    • 默认的内存管理器
  2. SegmentHeap
    • win10新增的内存管理器
    • 部分系统程序以及UWP(Universal Windows Platform)使用

NT_Heap

NT_Heap可以分为两个部分

  1. back_end, 后端分配器是主要的堆管理结构, 负责大多数内存管理操作
  2. front_end, 前端分配器为了提高小块内存分配的速度, 对高频小内存分配进行优化, Windows 使用 LFH(Low Fragmentation Heap) 作为主要的前端分配器

Back-End

让我们从后端开始, 一切开始之前先让我们知道windows堆是如何描述堆和chunk的

结构体声明来自https://www.vergiliusproject.com/

结构描述

_HEAP_ENTRY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//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可能有以下几种状态

  • Inused chunk
  • Freed chunk
  • VirtualAlloc chunk

与linux类似, 也是头部 + User Data, 对于一个最普通的后端chunk其状态如下

1
2
3
4
5
6
7
8
9
10
struct _HEAP_ENTRY{
void * PreviousBlockPrivateData;
Uint2B Size;
Uchar Flags;
Uchar SmallTagIndex;
Uint2B PreviousSize;
Uchar SegmentOffset;
Uchar Unusedbyte;
Uchar UserData[];
}
  • PreviousBlockPrivateData, 与ptmalloc2类似, 基本上存储前一个chunk的数据, 但空闲时不会存储前一个chunk的size
  • Size, 因为chunk是十六字节对齐存储当前chunk的size >> 4
  • Flags, 当前chunk的标志:
    • 0x1表示处于占用状态
    • 0x2表示存在额外描述
    • 0x4表示使用固定模式填充
    • 0x8表示VirtualAlloc
    • 0x10表示为该段最后一个chunk
  • SmallTagIndex, SizeFlags成员三字节数据逐个xor结果, 取出时会进行校验
  • PreviousSize, 上一个chunk的size, 同样是右移4位
  • SegmentOffset, 某些情况用于查找segment
  • Unusedbyte, 两种情况
    • 在inuse的时候, 表示malloc之后剩下的chunk的空间大小, 可以用来判断chunk是来自于Front-End还是Back-End
    • 在freed的时候, 恒为0
  • UserData, 用户使用的区域, 类似的在freed状态下会存储FlinkBlink分别指向前一个和后一个chunk, 但指向的是chunk的用户区域
_HEAP_VIRTUAL_ALLOC_ENTRY

该结构体用于描述通过VirtualAlloc得到的chunk

1
2
3
4
5
6
7
8
9
//0x40 bytes (sizeof)
struct _HEAP_VIRTUAL_ALLOC_ENTRY
{
struct _LIST_ENTRY Entry; //0x0
struct _HEAP_ENTRY_EXTRA ExtraStuff; //0x10
ULONGLONG CommitSize; //0x20
ULONGLONG ReserveSize; //0x28
struct _HEAP_ENTRY BusyBlock; //0x30
};
  • Entry ,就是一个Flink和Blink, 分别指向上一个和下一个VirtualAlloc得到的chunk
  • 中间三个字段, 暂时不做过多理会
  • BusyBlock, 与普通的_HEAP_ENTRY头基本一样, 不同在于这里的Size是没有使用的size, 储存时也没有进行size >> 4的操作, UnusedBytes恒为4
_HEAP

接下来要知道的是这些chunk是被哪个结构管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//0x2c0 bytes (sizeof)
struct _HEAP
{
union
{
struct _HEAP_SEGMENT Segment; //0x0
struct
{
struct _HEAP_ENTRY Entry; //0x0
ULONG SegmentSignature; //0x10
ULONG SegmentFlags; //0x14
struct _LIST_ENTRY SegmentListEntry; //0x18
struct _HEAP* Heap; //0x28
VOID* BaseAddress; //0x30
ULONG NumberOfPages; //0x38
struct _HEAP_ENTRY* FirstEntry; //0x40
struct _HEAP_ENTRY* LastValidEntry; //0x48
ULONG NumberOfUnCommittedPages; //0x50
ULONG NumberOfUnCommittedRanges; //0x54
USHORT SegmentAllocatorBackTraceIndex; //0x58
USHORT Reserved; //0x5a
struct _LIST_ENTRY UCRSegmentList; //0x60
};
};
ULONG Flags; //0x70
ULONG ForceFlags; //0x74
ULONG CompatibilityFlags; //0x78
ULONG EncodeFlagMask; //0x7c
struct _HEAP_ENTRY Encoding; //0x80
ULONG Interceptor; //0x90
ULONG VirtualMemoryThreshold; //0x94
ULONG Signature; //0x98
ULONGLONG SegmentReserve; //0xa0
ULONGLONG SegmentCommit; //0xa8
ULONGLONG DeCommitFreeBlockThreshold; //0xb0
ULONGLONG DeCommitTotalFreeThreshold; //0xb8
ULONGLONG TotalFreeSize; //0xc0
ULONGLONG MaximumAllocationSize; //0xc8
USHORT ProcessHeapsListIndex; //0xd0
USHORT HeaderValidateLength; //0xd2
VOID* HeaderValidateCopy; //0xd8
USHORT NextAvailableTagIndex; //0xe0
USHORT MaximumTagIndex; //0xe2
struct _HEAP_TAG_ENTRY* TagEntries; //0xe8
struct _LIST_ENTRY UCRList; //0xf0
ULONGLONG AlignRound; //0x100
ULONGLONG AlignMask; //0x108
struct _LIST_ENTRY VirtualAllocdBlocks; //0x110
struct _LIST_ENTRY SegmentList; //0x120
USHORT AllocatorBackTraceIndex; //0x130
ULONG NonDedicatedListLength; //0x134
VOID* BlocksIndex; //0x138
VOID* UCRIndex; //0x140
struct _HEAP_PSEUDO_TAG_ENTRY* PseudoTagEntries; //0x148
struct _LIST_ENTRY FreeLists; //0x150
struct _HEAP_LOCK* LockVariable; //0x160
LONG (*CommitRoutine)(VOID* arg1, VOID** arg2, ULONGLONG* arg3); //0x168
union _RTL_RUN_ONCE StackTraceInitVar; //0x170
struct _RTL_HEAP_MEMORY_LIMIT_DATA CommitLimitData; //0x178
VOID* FrontEndHeap; //0x198
USHORT FrontHeapLockCount; //0x1a0
UCHAR FrontEndHeapType; //0x1a2
UCHAR RequestedFrontEndHeapType; //0x1a3
WCHAR* FrontEndHeapUsageData; //0x1a8
USHORT FrontEndHeapMaximumIndex; //0x1b0
volatile UCHAR FrontEndHeapStatusBitmap[129]; //0x1b2
struct _HEAP_COUNTERS Counters; //0x238
struct _HEAP_TUNING_PARAMETERS TuningParameters; //0x2b0
};

字段很多, 同样只关注部分

  • EncodeFlagMask, 用来标志是否要encode该heap中的chunk头, 0x100000表示需要encode
  • Encoding, 用来和chunk头进行xor的cookie
  • VirtualAllocdBlocks, 一个双向链表的dummy head, 存放着FlinkBlink, 将VirtualAllocate出来的chunk链接起来
  • BlocksIndex, 指向一个_HEAP_LIST_LOOKUP结构, 是backend管理的重要结构体
  • FreeList, 一个双向链表的dummy head, 同样存放着FlinkBlink, 将所有的freed chunk给链起来, 可以理解为unsorted bin, 但链表是有序
  • FrontEndHeap, 指向管理Front-End heap的结构体
  • FrontEndHeapUsageData, 一个对应各个大小chunk的数组, 该数组记录各种大小chunk使用的次数, 达到一定数值的时候就会启用Front-End
  • FrontEndHeapStatusBitmap, 标识某个大小的chunk是否有启用LFH
_HEAP_LIST_LOOKUP

(BlocksIndex)_HEAP_LIST_LOOKUP是后端的一个重要结构

1
2
3
4
5
6
7
8
9
10
11
12
13
//0x38 bytes (sizeof)
struct _HEAP_LIST_LOOKUP
{
struct _HEAP_LIST_LOOKUP* ExtendedLookup; //0x0
ULONG ArraySize; //0x8
ULONG ExtraItem; //0xc
ULONG ItemCount; //0x10
ULONG OutOfRangeItems; //0x14
ULONG BaseIndex; //0x18
struct _LIST_ENTRY* ListHead; //0x20
ULONG* ListsInUseUlong; //0x28
struct _LIST_ENTRY** ListHints; //0x30
};
  • ExtendedLookup, 指向下一个ExtendedLookup, 通常下一个会管理更大的chunk
  • ArraySize, 当前结构管理的最大chunk的大小, 同样右移4位, 第一个通常是是0x800 >> 4
  • ItemCount, 目前管理的chunk数目
  • OutOfRangeItems, 超出该结构所管理大小的chunk数目
  • BaseIndex, 该结构所管理的chunk的起始index, 用来从ListHint中找到合适大小的freed chunk用的
  • ListHead, 指向Freelist的dummy head
  • ListsInUseUlong, 用于判断ListHint中是否有合适大小的chunk的bitmap
  • ListHints, 用来指向对应大小的chunk array, 该array以0x10大小为间隔, 存放一个对应size的freed chunk的地址, 用于快速找到合适大小的chunk, 可以类比linux ptmalloc的tcache bin, 只不过chunk的组织仍然通过双向链表维护

到这里就可以贴一张图了

分配

看完结构体, 接下来就开始分析分配的机制, 分配的主要逻辑是在RtlpAllocateHeap

根据分配的大小, 有三种不同的分配路径

  • case1: size <= 0x4000
  • case2: 0x4000 < size <= 0xff000
  • case3: size > 0xff000
case1
  1. 判断该size对应的FrontEndHeapStatusBitmap是否有启用LFH
    • 如果未开启:
      1. 在size对应的FrontEndHeapUsageData += 0x21
      2. 再次判断FrontEndHeapUsageData > 0xff00 || FrontEndHeapUsageData & 0x1f > 0x10是否成立, 成立则启用LFH
  2. 进入BlocksIndex中查看对应的ListHint是否有可用freed chunk
    • 如果存在size刚好合适的
      1. 取走该chunk, 并且查看其Flink所连接的chunk大小是否与之相同, 如果相同, 那么对应LIstHint更为Flink, 否则置零对应ListHint
      2. 将这个chunk从FreeList中unlink, 并返回给使用者(会将header xor回正常状态)
    • 如果没有刚好合适的, 但有更大size的
      1. 优先找最接近所需要size的, 有则按上述相同方法进行取出
      2. 对chunk进行切割, 剩下的再次加入FreeList, 如果可以放入ListHint则放入
      3. 将切割后chunk返回给使用者同时还原header
    • 如果FreeList中没有能够使用的chunk
      1. 尝试ExtendHeap拓展heap空间
      2. 从拓展出空间的chunk中寻找, 同前两种情况
case2

除了没有LFH相关操作, 其余与case1一致

case3

0xff000其实也就是_HEAP结构的VirtualMemoryThreshold << 4

根据这个字段的名称可以知道, 这个大小就是使用VirtualAlloc的临界大小

  1. 直接使用ZwAllocateVirtualMemory得到类似mmap出的一大块内存
  2. 将该内存块插入_HEAP-> VirtualAllocBlocks链表
  3. 并返回给用户使用

释放

释放时只分为两种情况

  • case1: size <= 0xff000
  • case2: size > 0xff000
case1
  1. 首先检查alignment对齐, 利用unused byte判断chunk状态

  2. 如果该size未开启LFH, 对应的FrontEndHeapUsageData 减 1

  3. 判断前后的chunk是否处于空闲状态, 是的话进行合并, 合并时采用unlink, 与之前类似

  4. 合并完后, 更新合并后chunk的size, 以及后一个chunk的prevsize, 然后查看此时chunk是不是需要在freelist最前跟最后, 是就插入, 否则就从ListHint中插入, 并且update ListHint

    插入时也会对Freelist进行检查, 但是此检查不会触发abort, 原因在于没有做unlink写入

case2
  1. 检查该chunk的linked list并从_HEAP->VirtualAllocdBlocks中移除
  2. 接着使用RtlpSecMemFreeVirtualMemory将chunk整个munmap掉

Back-Exploitation

主要就是unlink

其实原理和linux下的unlink几乎一致, 有前者基础的话能够很好地理解相关知识

需要注意两点

  1. decode header进行check的时候, 需要保证其正确性, 比如找到previous freed chunk, 进行decode以及check的操作的时候

  2. chunk的Flink和Blink直接指向数据区域而不是chunk header, 不用伪造fake chunk的步骤

    设置

    1
    2
    *(Flink + 0x8) = chunk_addr
    *(Blink) = chunk_addr

整体利用思路

  1. 在已知linux下unlink attack的基础上, 以完全相同的方式,对windows heap进行unlink attack, 可以实现将一个指针指向本身的效果
  2. 利用这个指向自身的指针, 可以控制周围的可能的指针, 达到任意地址读写的效果
  3. 不管如何利用, 总是需要先leak得到各类信息
    • 代码地址
      • PEB --> text
    • 共享库地址
      • text --> IAT --> xxx.dll --> xxx.dll
      • _HEAP->LockVariable.Lock --> ntdll.dll
      • CrticalSection->DebugInfo --> ntdll.dll
    • 栈地址
      • Kernel32.dll --> kernelbase.dll --> KERNELBASE!BasepFilterInfo --> stack address
      • kernel32.dll --> ntdll.dll --> ntdll!PebLdr --> PEB --> TEB --> stack address

Front-End

结构描述

这部分的结构体在vergiliusproject找不到

_LFH_HEAP

在_HEAP中有一个字段FrontEndHeap, 指向一个_LFH_HEAP结构体, 该结构体就是前端最重要的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_SRWLOCK
+0x008 SubSegmentZones : _LIST_ENTRY
+0x018 Heap : Ptr64 Void
+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
+0x4a8 SegmentInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0x8b0 AffinitizedInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0xcb8 SegmentAllocator : Ptr64 _SEGMENT_HEAP
+0xcc0 LocalData : [1] _HEAP_LOCAL_DATA

关注几个重要字段

  • Heap, 指向其对应的_HEAP结构体
  • Buckets, 一个存放129个_HEAP_BUCKET结构体的数组, 用来寻找配置大小对应到Block大小的阵列结构
  • SegmentInfoArrays, 一个存放129个_HEAP_LOCAL_SEGMENT_INFO结构体指针的数组, 不同大小对应到不同的_HEAP_LOCAL_SEGMENT_INFO结构体, 主要管理对应到的_HEAP_SUBSEGMENT的信息
  • LocalData, 一个_HEAP_LOCAL_DATA结构体
_HEAP_BUCKET
1
2
3
4
5
6
ntdll!_HEAP_BUCKET
+0x000 BlockUnits : Uint2B
+0x002 SizeIndex : UChar
+0x003 UseAffinity : Pos 0, 1 Bit
+0x003 DebugFlags : Pos 1, 2 Bits
+0x003 Flags : UChar

关注两个部分:

  • BlockUnits, 要分配出去的一个block的大小, 存放size >> 4
  • SizeIndex, 使用者需要的大小, 存放size >> 4
_HEAP_LOCAL_SEGMENT_INFO
1
2
3
4
5
6
7
8
9
10
ntdll!_HEAP_LOCAL_SEGMENT_INFO
+0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA
+0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT
+0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT
+0x090 SListHeader : _SLIST_HEADER
+0x0a0 Counters : _HEAP_BUCKET_COUNTERS
+0x0a8 LastOpSequence : Uint4B
+0x0ac BucketIndex : Uint2B
+0x0ae LastUsed : Uint2B
+0x0b0 NoThrashCount : Uint2B

关注:

  • LocalData, 一个_HEAP_LOCAL_DATA结构体指针, 指向_LFH_HEAP->LocalData, 方便从_HEAP_LOCAL_SEGMENT_INFO找回_LFH_HEAP

  • BucketIndex, 对应到的BucketIndex, 也就是_LFH_HEAP->SegmentInfoArrays数组中对应的下标

  • ActiveSubsegment, 非常重要的成员, 一个_HEAP_SUBSEGMENT结构体指针

    目的在于管理UserBlocks, 记录剩余等多chunk和该UserBlocks最大分配数等信息

  • CachedItems, 存放16个_HEAP_SUBSEGMENT结构体指针的数组, 存放对应到该_HEAP_LOCAL_SEGMENT_INFO且还有可以分配chunk给用户的_HEAP_SUBSEGMENT指针

    可以理解为一个内存池, 当ActiveSubsegment没有可用chunk的时候, 即用完的时候, 就从CachedItems选择填充, 替换ActiveSubsegment

_HEAP_LOCAL_DATA
1
2
3
4
5
6
ntdll!_HEAP_LOCAL_DATA
+0x000 DeletedSubSegments : _SLIST_HEADER
+0x010 CrtZone : Ptr64 _LFH_BLOCK_ZONE
+0x018 LowFragHeap : Ptr64 _LFH_HEAP
+0x020 Sequence : Uint4B
+0x024 DeleteRateThreshold : Uint4B

其中LowFragHeap指回_LFH_HEAP结构本身的位置, 通常用来找回LFH

_HEAP_SUBSEGMENT
1
2
3
4
5
6
7
8
9
10
11
12
13
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER
+0x010 DelayFreeList : _SLIST_HEADER
+0x020 AggregateExchg : _INTERLOCK_SEQ
+0x024 BlockSize : Uint2B
+0x026 Flags : Uint2B
+0x028 BlockCount : Uint2B
+0x02a SizeIndex : UChar
+0x02b AffinityIndex : UChar
+0x024 Alignment : [2] Uint4B
+0x02c Lock : Uint4B
+0x030 SFreeListEntry : _SINGLE_LIST_ENTRY
  • LocalInfo, 一个指回到对应_HEAP_LOCAL_SEGMENT_INFO结构体位置的指针

  • UserBlocks, 一个指向_HEAP_USERDATA_HEADER结构的指针, 也就是指向LFH chunk的内存分配池

    该内存分配池包括一个_HEAP_USERDATA_HEADER, 存放一些metatdata

    后面紧跟着要分配出去的所有chunk

  • AggregateExchg, 一个_INTERLOCK_SEQ结构, 储存对应的UserBlocks的状态信息

    LFH通过该结构判断是否从该UserBlock分配, 同时具有Lock的作用

  • BlockSize, 该UserBlocks中每个chunk的大小

  • BlockCount, 该UserBlocks中chunk的总个数

  • SizeIndex, 该UserBlocks对应的index

_INTERLOCK_SEQ
1
2
3
4
5
6
ntdll!_INTERLOCK_SEQ
+0x000 Depth : Uint2B
+0x002 Hint : Pos 0, 15 Bits
+0x002 Lock : Pos 15, 1 Bit
+0x002 Hint16 : Uint2B
+0x000 Exchg
  • Depth, 用来管理对应到的UserBlocks还有多少freed chunk, LFH会用这个判断是否还从该UserBlock进行分配
  • Lock, 即提供锁的作用, 其实只占用第4 byte的最后一个bit
_HEAP_USERDATA_HEADER
1
2
3
4
5
6
7
8
9
10
11
12
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr64 _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
+0x020 BusyBitmap : _RTL_BITMAP_EX
+0x030 BitmapData : [1] Uint8B

关注:

  • SubSegment, 指回对应的_HEAP_SUBSEGMENT结构
  • EncodedOffsets, 一个_HEAP_USERDATA_OFFSETS结构, 用来验证chunk header是否被改过
  • BusyBitmap, 记录该UserBlocks哪些chunk被使用
_HEAP_ENTRY

前端管理的_HEAP_ENTRY与之前在后端的_HEAP_ENTRY存在差异

1
2
3
4
5
6
7
8
struct _HEAP_ENTRY{
void * PreviousBlockPrivateData;
Uint4B SubSegmentCode;
Uint2B PreviousSize;
Uchar SegmentOffset;
Uchar Unusedbyte;
Uchar UserData[];
}

size, FlagsSmallTagIndex变成了SubSegmentCode

相应含义也发生了变化

  • SubSegmentCode, encode过的metadata, 用来推回UserBlocks的位置。
  • PreviousSize, 该chunk在UserBlock中的index, 实际上是第0xD个byte
  • UnusedBytes, 用来判断该LFH chunk状态
    • Freed, 恒为0x80
    • Inused, UnusedBytes & 0x80 != 0

在认识了这些结构体之后就可以看这两张图片

补充

_HEAP_USERDATA_HEADER->EncodedOffsetsUserBlocks初始化的时候设置, 其算法为下面四个值进行xor:

  • (sizeof(\_HEAP_USERDATA_HEADER)) | ((_HEAP_BUCKET->BlockUnits) * 0x10 << 16)
  • LFHkey
  • UserBlocks的地址
  • _LFH_HEAP的地址

所有UserBlocks里的chunk header在初始化的时候都会经过xor, 其算法为下面各个值得xor:

  • _HEAP的地址
  • LFHkey
  • chunk本身的地址address >> 4
  • ((chunk address) - (UserBLocks address)) << 12

初始化

FrontEndHeapUsageData[x] & 0x1F > 0x10时, 置位_HEAP->CompatibilityFlag |= 0x20000000, 下一次allocate(也就是第18次)就会启用LFH并初始化

  1. 首先会ExtendFrontEndUsageData及增加更大的_HEAP->BlocksIndex

    _HEAP->BlocksIndex可以理解为一个_HEAP_LIST_LOOKUP结构的单向链表, 且默认初始情况下只存在一个管理(max 0x800)的chunk的_HEAP_LIST_LOOKUP

    所以这里会扩展到(0x800, 0x4000), 即在链表尾追加一个管理更大chunk的_HEAP_LIST_LOOKUP结构体结点

  2. 建立并初始化_HEAP->FrontEndHeap(通过mmap), 即初始化_LFH_HEAP的一些metadata

  3. 建立并初始化_LFH_HEAP->SegmentInfoArrays[x], 在SegmentInfoArrays[BucketIndex]处填上对应的_HEAP_LOCAL_SEGMENT_INFO结构体指针

再之后申请同样大小的chunk就会开始使用LFH

  1. 分配UserBlocks并进行初始化, 即设置对应大小的chunk。
  2. 然后再设置对应_HEAP_LOCAL_SEGMENT_INFO->ActiveSubsegment
  3. 随机地从UserBlocks中返回一个chunk

分配

主要函数逻辑在RtlpLowFragHeapAllocFromContext

  1. 先看看ActiveSubsegment中有没有空闲的chunk, 也就是通过ActiveSubsegment->AggregateExchg.depth判断

    • 如果没有则从CacheedItems中找, 找到有存在空闲chunk的Subsegment就替换掉当前的ActiveSubsegment
    • 如果有则继续往下
  2. 取得RtlpLowFragHeapRandomData[x]上的值, 取值是依次循环取的, x为1 byte大小的值, 即下一次x = (x + 1) % 256

    由于RtlpLowFragHeapRandomData是一个存放256个随机数的数列(范围为0x0 ~ 0x7F), 所以这里相当于在取随机数

  3. 计算相应的UserBlocks里chunk的index, 通过RtlpLowFragHeapRandomData[x] * max_index >> 7(max_index是能取到的最大的index)

    • 如果发生了collision, 即该index对应的chunk是busy的, 那么往后取最近的

      细节上, 就是检查index对应到的bitmap是否为0, 如果是0就返回对应的bitmap, 否则选取最近的下一个

    • 如果没有发生, 则继续往下

  4. 检查chunk->UnusedBytes & 0x3F != 0, 因为满足此式表示chunk是free状态的, 否则状态非法

    该过程中还会设置对应的bitmap, 以及更新ActiveSubsegment->AggregateExchg.depth等相关信息

  5. 最后设置index(即chunk->PreviousSize)以及chunk->UnusedBytes, 并把chunk返回给用户

释放

  1. 首先更新chunk->UnusedBytes = 0x80
  2. 找到该chunk对应的在UserBlocks中的index,并且置UserBlocks->BusyBitmap对应的bit为0
  3. 更新ActiveSubsegment->AggregateExchg
  4. 如果该chunk不属于当前的ActiveSubsegment则看能不能放进CachedItems中去, 如果可以就放进去

LFH-Exploitation

假如拥有UAF的漏洞可以利用, 但是因为LFH分配的随机性, 无法预测下一个分配出的chunk是在哪个位置, 也就是说现在我们free的chunk, 下一次malloc不一定拿得到

那么此时可以通过拿空UserBlocks的方式(也就是使这个UserBlocks的chunk全部处于busy), 再free掉目标chunk, 这样下一次malloc就必然会拿到目标chunk(因为只有一个可用)

再之后利用这个特性进一步利用