安全路透社
当前位置:安全路透社 > 网络转载 > 正文

浅析Windows下堆的结构

*本文原创作者:hellowuzekai

## 简介

Windows下的堆主要有两种,进程的默认堆和自己创建的私有堆。在程序启动时,系统在刚刚创建的进程虚拟地址空间中创建一个进程的默认堆,而且程序也可以通过 HeapCreate 函数来调用 ntdll 中的RtlCreateHeap 来创建自己的私有堆,所以一个进程中可以存在多个堆。

虽说这两种堆名称不同,但是其本质是相同的,区别的只是返回的句柄不同,私有堆虽然名字是私有,但并不是只能在创建它的线程中使用,如果得到它的句柄,在其他线程中也可使用。

## 堆的信息

堆的相关信息可以在/PEB(进程环境块)中看到

.process 查看 $peb 地址

```

0:000> .process

Implicit process is now 7ffd8000

```

dt _PEB addr 查看 $peb 的结构信息

```

0:000> dt _PEB 7ffd8000

ntdll!_PEB

...

   +0x018 ProcessHeap      : 0x00140000 Void

...   

   +0x078 HeapSegmentReserve : 0x100000

   +0x07c HeapSegmentCommit : 0x2000

   +0x080 HeapDeCommitTotalFreeThreshold : 0x10000

   +0x084 HeapDeCommitFreeBlockThreshold : 0x1000

   +0x088 NumberOfHeaps    : 4

   +0x08c MaximumNumberOfHeaps : 0x10

   +0x090 ProcessHeaps     : 0x7c99cfc0  -> 0x00140000 Void

...

```

我们需要注意的是上面几个偏移位置的信息

0×18 默认堆的地址

0×78 默认堆的默认大小

0x7c 默认堆的初始提交大小

0×80 与堆释放有关的阈值

0×84 与堆释放有关的阈值

0×88 程序中堆的数量

0x8c 程序中最大的堆的数量

0×90 存储所有堆地址的数组

只有当本次释放时

1. 本次释放的堆块大小超过了_PEB中的HeapDeCommitFreeBlockThreshold字段的值。

2. 空闲空间的总大小超过了_PEB中的eapDeCommitTotalFreeThreshold字段的值。

堆管理器才会将该内存交还给内存管理器,否则继续由堆管理器管理

查看一下所有堆的地址

```

0:000> dd 7c99cfc0

7c99cfc0  00140000 00240000 00250000 00380000

7c99cfd0  00000000 00000000 00000000 00000000

```

可以看到进程中四个堆的地址,同样使用!heap -h来看一下

```

0:000> !heap -h

Index   Address  Name      Debugging options enabled

  1:   00140000 

    Segment at 00140000 to 00240000 (00003000 bytes committed)

  2:   00240000 

    Segment at 00240000 to 00250000 (00006000 bytes committed)

  3:   00250000 

    Segment at 00250000 to 00260000 (00003000 bytes committed)

  4:   00380000 

    Segment at 00380000 to 00390000 (00003000 bytes committed)

```

## 堆的结构

上面我们通过 PEB 查看了进程中堆的一些信息。

在 Windows 的堆中管理着许多的堆段 (Segment),在堆创建时同时创建第一个堆段,称为 0 号段,之后如果一个段不够,如果指明了 HEAP_GROWABLE 标志,会创建其他的堆段,但是最多有 64 个堆段,而这一个个堆段,正是由堆块 ( 类似于 linux 的 Chunk) 构成。

#### 堆

现在选其中一个堆,我们来看一下堆的详细结构

```

0:000> dt _HEAP 140000

ntdll!_HEAP

   +0x000 Entry            : _HEAP_ENTRY

   +0x008 Signature        : 0xeeffeeff

   +0x00c Flags            : 0x50000062

   +0x010 ForceFlags       : 0x40000060

   +0x014 VirtualMemoryThreshold : 0xfe00

   +0x018 SegmentReserve   : 0x100000

   +0x01c SegmentCommit    : 0x2000

   +0x020 DeCommitFreeBlockThreshold : 0x200

   +0x024 DeCommitTotalFreeThreshold : 0x2000

   +0x028 TotalFreeSize    : 0xa6

   +0x02c MaximumAllocationSize : 0x7ffdefff

   +0x030 ProcessHeapsListIndex : 1

   +0x032 HeaderValidateLength : 0x608

   +0x034 HeaderValidateCopy : (null) 

   +0x038 NextAvailableTagIndex : 0

   +0x03a MaximumTagIndex  : 0

   +0x03c TagEntries       : (null) 

   +0x040 UCRSegments      : (null) 

   +0x044 UnusedUnCommittedRanges : 0x00140598 _HEAP_UNCOMMMTTED_RANGE

   +0x048 AlignRound       : 0x17

   +0x04c AlignMask        : 0xfffffff8

   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x140050 - 0x140050 ]

   +0x058 Segments         : [64] 0x00140640 _HEAP_SEGMENT

   +0x158 u                : __unnamed

   +0x168 u2               : __unnamed

   +0x16a AllocatorBackTraceIndex : 0

   +0x16c NonDedicatedListLength : 1

   +0x170 LargeBlocksIndex : (null) 

   +0x174 PseudoTagEntries : (null) 

   +0x178 FreeLists        : [128] _LIST_ENTRY [ 0x142ad8 - 0x142ad8 ]

   +0x578 LockVariable     : 0x00140608 _HEAP_LOCK

   +0x57c CommitRoutine    : (null) 

   +0x580 FrontEndHeap     : 0x00140688 Void

   +0x584 FrontHeapLockCount : 0

   +0x586 FrontEndHeapType : 0x1 ''

   +0x587 LastSegmentIndex : 0 ''

```

注意这几个偏移位置

0×14 最大分配内存,超过此大小就交由内存管理器分配

0x2c 最大申请大小

0×50 管理由内存管理器分配内存的链表

0×58 该堆中堆段数组

0×178 管理 128 个空闲堆块的双向链表头指针

0×580 指向前端分配器

在 0×14 偏移处的值的单位是 8byte,也就是最大申请大小为 0xfe00 * 8 = 508kB,由于申请内存时堆块的头部需要占用 8 字节,所以最大申请大小 = 最大分配大小 – 8B

看一下偏移 0×178 处

```

0:000> dd 140000+178

00140178  00142ad8 00142ad8 00140180 00140180

00140188  00140188 00140188 00140190 00140190

00140198  00140198 00140198 001401a0 001401a0

001401a8  001401a8 001401a8 001401b0 001401b0

001401b8  001401b8 001401b8 001401c0 001401c0

001401c8  001401c8 001401c8 001401d0 001401d0

001401d8  001401d8 001401d8 001401e0 001401e0

001401e8  001401e8 001401e8 001401f0 001401f0

0:000> dt _LIST_ENTRY 142ad8

ntdll!_LIST_ENTRY

 [ 0x140178 - 0x140178 ]

   +0x000 Flink            : 0x00140178 _LIST_ENTRY [ 0x142ad8 - 0x142ad8 ]

   +0x004 Blink            : 0x00140178 _LIST_ENTRY [ 0x142ad8 - 0x142ad8 ]

```

可以看到 HEAP 结构维护的是  LIST_ENTRY 指针,该指针指向的正是双向链表的头指针结构

#### 堆段

同样看一下 0×58 处的 Segments

```

0:000> dd 0x140000+58

00140058  00140640 00000000 00000000 00000000

00140068  00000000 00000000 00000000 00000000

0:000> dt _HEAP_SEGMENT 0x140640

ntdll!_HEAP_SEGMENT

   +0x000 Entry            : _HEAP_ENTRY

   +0x008 Signature        : 0xffeeffee

   +0x00c Flags            : 0

   +0x010 Heap             : 0x00140000 _HEAP

   +0x014 LargestUnCommittedRange : 0xfd000

   +0x018 BaseAddress      : 0x00140000 Void

   +0x01c NumberOfPages    : 0x100

   +0x020 FirstEntry       : 0x00140680 _HEAP_ENTRY

   +0x024 LastValidEntry   : 0x00240000 _HEAP_ENTRY

   +0x028 NumberOfUnCommittedPages : 0xfd

   +0x02c NumberOfUnCommittedRanges : 1

   +0x030 UnCommittedRanges : 0x00140588 _HEAP_UNCOMMMTTED_RANGE

   +0x034 AllocatorBackTraceIndex : 0

   +0x036 Reserved         : 0

   +0x038 LastEntryInSegment : 0x00142ad0 _HEAP_ENTRY

```

注意这几个偏移位置

0×00 存储该堆段所有堆块

0×10 记录 _HEAP 结构的地址

0×18 维护该段的基址

0×20 第一个堆块地址

#### 堆块

查看堆块的结构

```

0:000> dt _HEAP_ENTRY 0x140640

ntdll!_HEAP_ENTRY

   +0x000 Size             : 8

   +0x002 PreviousSize     : 0xc8

   +0x000 SubSegmentCode   : 0x00c80008 Void

   +0x004 SmallTagIndex    : 0 ''

   +0x005 Flags            : 0x1 ''

   +0x006 UnusedBytes      : 0 ''

   +0x007 SegmentIndex     : 0 ''

```

注意这几个偏移位置

0×00 本堆块的大小

0×02 前一堆块的大小 

0×05 标志位

0×06 由于对齐原因多分配的字节数

0×07 所属的 segment 号

Flags 的标志位有下面几种情形

0×01 该块处于占用状态

0×02 该块存在额外描述

0×04 使用固定模式填充堆块

0×08 虚拟分配

0×10 该段最后一个堆块

我们可以看到堆块的结构相比较于Linux下的Chunk结构来说,首先 prev_size 和 size 的位置调换了一下,并且该块的状态并不在下一块的 size 位中保存,而是在本块的 Flags 位保存,这对我们的漏洞利用提出了新的要求。

#### 实例观测私有堆

编译下面的代码并使用 WinDBG 调试

```

#include<Windows.h>

int main(int argc, char* argv[])  

{  

  

       HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 1024);  

  

       void * p = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 1012);  

  

       bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);  

  

       return 0;  

  

```

首先在 main 函数下断点,运行

```

0:000> bp main

*** WARNING: Unable to verify checksum for heap.exe

0:000> bl

 0 e 00401010     0001 (0001)  0:**** heap!main

0:000> g

Breakpoint 0 hit

eax=00380c20 ebx=7ffdf000 ecx=00000001 edx=00380cc8 esi=007e6436 edi=0242f554

eip=00401010 esp=0012ff84 ebp=0012ffc0 iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

heap!main:

00401010 55              push    ebp

```

单步调试到 HeapCreate 函数返回,通过 eax 返回值观察堆结构,也可 !heap 查看

```

0:000> p

eax=003a0000 ebx=7ffdf000 ecx=7c946090 edx=7c99b380 esi=0012ff28 edi=0012ff80

eip=00401043 esp=0012ff28 ebp=0012ff80 iopl=0         nv up ei pl zr na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

heap!main+0x33:

00401043 8bf4            mov     esi,esp

```

这里返回的 eax,即hHeap句柄值为3a0000

```

0:000> dt _HEAP 3a0000

ntdll!_HEAP

   +0x000 Entry            : _HEAP_ENTRY

   +0x008 Signature        : 0xeeffeeff

   +0x00c Flags            : 0x50001061

   +0x010 ForceFlags       : 0x40000061

   +0x014 VirtualMemoryThreshold : 0xfe00

   +0x018 SegmentReserve   : 0x100000

   +0x01c SegmentCommit    : 0x2000

   +0x020 DeCommitFreeBlockThreshold : 0x200

   +0x024 DeCommitTotalFreeThreshold : 0x2000

   +0x028 TotalFreeSize    : 0x137

   +0x02c MaximumAllocationSize : 0x7ffdefff

   +0x030 ProcessHeapsListIndex : 5

   +0x032 HeaderValidateLength : 0x608

   +0x034 HeaderValidateCopy : (null) 

   +0x038 NextAvailableTagIndex : 0

   +0x03a MaximumTagIndex  : 0

   +0x03c TagEntries       : (null) 

   +0x040 UCRSegments      : (null) 

   +0x044 UnusedUnCommittedRanges : 0x003a0588 _HEAP_UNCOMMMTTED_RANGE

   +0x048 AlignRound       : 0x17

   +0x04c AlignMask        : 0xfffffff8

   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x3a0050 - 0x3a0050 ]

   +0x058 Segments         : [64] 0x003a0608 _HEAP_SEGMENT

   +0x158 u                : __unnamed

   +0x168 u2               : __unnamed

   +0x16a AllocatorBackTraceIndex : 0

   +0x16c NonDedicatedListLength : 1

   +0x170 LargeBlocksIndex : (null) 

   +0x174 PseudoTagEntries : (null) 

   +0x178 FreeLists        : [128] _LIST_ENTRY [ 0x3a0650 - 0x3a0650 ]

   +0x578 LockVariable     : (null) 

   +0x57c CommitRoutine    : (null) 

   +0x580 FrontEndHeap     : (null) 

   +0x584 FrontHeapLockCount : 0

   +0x586 FrontEndHeapType : 0 ''

   +0x587 LastSegmentIndex : 0 ''

```

继续单步调试到 HeapAlloc 函数返回,得到这次申请的堆块地址为 0x3a6500

```

0:000> p

eax=003a0650 ebx=7ffdf000 ecx=7c9301bb edx=000003f4 esi=0012ff28 edi=0012ff80

eip=00401060 esp=0012ff28 ebp=0012ff80 iopl=0         nv up ei pl zr na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

heap!main+0x50:

00401060 8bf4            mov     esi,esp

```

由于返回的地址是加上堆块头的地址,所以查看堆块结构时减去 8byte

```

0:000> dt _HEAP_ENTRY 3a0650-8

ntdll!_HEAP_ENTRY

   +0x000 Size             : 0x82

   +0x002 PreviousSize     : 8

   +0x000 SubSegmentCode   : 0x00080082 Void

   +0x004 SmallTagIndex    : 0xa7 ''

   +0x005 Flags            : 0x7 ''

   +0x006 UnusedBytes      : 0x1c ''

   +0x007 SegmentIndex     : 0 ''

```

可以了解到此堆块大小为 0×82*8 byte,属于堆段 0,并且多分配了 0x1c 字节,Flags位表示该块占用,有额外描述并且被 ‘baadf00d’ 填充

```

0:000> dd 3a0650-8

003a0648  00080082 001c07a7 baadf00d baadf00d

003a0658  baadf00d baadf00d baadf00d baadf00d

003a0668  baadf00d baadf00d baadf00d baadf00d

003a0678  baadf00d baadf00d baadf00d baadf00d

003a0688  baadf00d baadf00d baadf00d baadf00d

003a0698  baadf00d baadf00d baadf00d baadf00d

003a06a8  baadf00d baadf00d baadf00d baadf00d

003a06b8  baadf00d baadf00d baadf00d baadf00d

```

继续单步执行到 HeapFree 函数返回,再次观察该堆块

```

0:000> dd 3a0650-8

003a0648  00080137 001c14a7 003a0178 003a0178

003a0658  feeefeee feeefeee feeefeee feeefeee

003a0668  feeefeee feeefeee feeefeee feeefeee

003a0678  feeefeee feeefeee feeefeee feeefeee

003a0688  feeefeee feeefeee feeefeee feeefeee

003a0698  feeefeee feeefeee feeefeee feeefeee

003a06a8  feeefeee feeefeee feeefeee feeefeee

003a06b8  feeefeee feeefeee feeefeee feeefeee

```

注意到填充发生了变化,对于已释放的堆块,结构体为 Heap_Free_Entry,相较于 Heap_Entry 多了两个空闲链表的指针

```

0:000> dt _HEAP_FREE_ENTRY 3a0650-8

ntdll!_HEAP_FREE_ENTRY

   +0x000 Size             : 0x137

   +0x002 PreviousSize     : 8

   +0x000 SubSegmentCode   : 0x00080137 Void

   +0x004 SmallTagIndex    : 0xa7 ''

   +0x005 Flags            : 0x14 ''

   +0x006 UnusedBytes      : 0x1c ''

   +0x007 SegmentIndex     : 0 ''

   +0x008 FreeList         : _LIST_ENTRY [ 0x3a0178 - 0x3a0178 ]

```

这里的堆块由于 free 后合并,所以 size 变成了合并后的值

## 堆的管理

在 Windows 中堆的申请回收使用了两种分配器,分别叫做前端分配器和后端分配器,当进程发起申请堆的请求时,首先由前端分配器处理,如果处理不了的话在交由后端分配器处理,在这点上前端分配器有点类似于 Linux 下的 FastBin,后端分配器类似于 UnsortedBin,SmallBin,LargeBin 组成的 Bin 数组

Windows 提供了两种前端分配器,分别为旁视列表(LAL)和低碎片(LF)前端分配器,其中前者在 Vista 之后的版本中不再使用

## 小结

这篇文章主要分析了 Windows 下不同于 Linux 的堆的结构,而 Windows 下堆的申请回收类似于 Linux,详情可以查看我的Dance In Heap系列文章。

*本文原创作者:hellowuzekai 

未经允许不得转载:安全路透社 » 浅析Windows下堆的结构

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册