查看原文
其他

win32下的堆管理系统

2018-02-19 风暴烈酒 看雪学院

准备刷漏洞战争中的堆溢出部分,但是对于堆的了解较少,在这里记录一下关于堆的学习记录,有错误请各位大大拍砖。


 

参考:


  • 《软件调试》

  • 《0day安全 软件漏洞分析技术》


win32下的堆管理系统:


1. win32堆管理器


win32堆管理器由NTDLL.dll实现,目的是为用户态的应用程序提供内存服务,从实现角度上来讲,内核态的池管理器和用户态的win32堆管理器默用的是同一套基础代码,它们以运行时的方式存在于ntdll.dll和ntosknrl.exe模块中。


2. CRT堆管理器


  • CRT由C运行时库创建,CRT创建的堆有三种模式,分别是SBH,ODLSBH和System Heap模式,CRT运行时库选择一种模式创建相应的堆。

  • 对于SBH和OLDSBH模式来说,CRT堆会从堆管理器中批发大块的内存,然后分割成小块的内存供程序使用,对于系统模式,CRT堆只是将堆分配请求转发给它基于的win32堆。因此处于系统模式的CRT堆只是对win32堆的简单封装。



win32堆


1. 体系结构


应用程序通过堆管理器提供的接口来分配或者是释放内存,整个堆管理器由堆管理api,前端堆,后端堆组成。在处理内存的分配和释放的时候,前端堆先做处理,前端堆有三种选择:none,预读列表(快表,lookaside-list),LFH(低碎片堆)。


2. 堆的内部结构


和堆相关的数据结构主要有以下几个:

  • _HEAP

  • _HEAP_SEGMENT

  • _HEAP_ENTRY

  • _HEAP_FREE_ENTRY


堆管理器通过_HEAP来记录和维护堆的管理信息,在_HEAP结构中,可能存在一个或者若干个_HEAP_SEGMENT的信息。在我们创建堆时,堆管理器必然会像操作系统申请一块内存空间,那么申请的内存大小和这段内存空间的描述信息就由_HEAP_SEGMENT记录。在每一个_HEAP_SEGMENT结构中,可能存在若干个_HEAP_ENTRY和_HEAP_FREE_ENTRY结构,其中_HEAP_ENTRY和_HEAP_FREE_ENTRY都是对堆块的描述,只不过_HEAP_FREE_ENTRY描述的是空闲的堆快结构,而由_HEAP_ENTRY描述的不一定都是空闲的堆块。


其中_HEAP_ENTRY和_HEAP_FREE_ENTRY的结构体如下:

0:000> dt _HEAP_ENTRY

ntdll!_HEAP_ENTRY

   +0x000 Size             : Uint2B

   +0x002 PreviousSize     : Uint2B

   +0x000 SubSegmentCode   : Ptr32 Void

   +0x004 SmallTagIndex    : UChar

   +0x005 Flags            : UChar

   +0x006 UnusedBytes      : UChar

   +0x007 SegmentIndex     : UChar

 

0:000> dt ntdll!_HEAP_FREE_ENTRY

   +0x000 Size             : Uint2B

   +0x002 PreviousSize     : Uint2B

   +0x000 SubSegmentCode   : Ptr32 Void

   +0x004 SmallTagIndex    : UChar

   +0x005 Flags            : UChar

   +0x006 UnusedBytes      : UChar

   +0x007 SegmentIndex     : UChar

   +0x008 FreeList         : _LIST_ENTRY


  • ps:上面两个结构体中比较坑的是,在xp sp3下,通过windbg查看_HEAP_ENTRY和_HEAP_FREE_ENTRY结构体中的第四个字段为SmallTagIndex,但实际上这个字段应该是安全cookie,算法如下:_HEAP._HEAP_ENTRY.cookie=_HEAP_ENTRY.cookie^((BYTE)&_HEAP_ENTRY/8)

  • 在win7下,直接用_HEAP_ENTRY和_HEAP_FREE_ENTRY查看数据,会存在问题,需要用_HEAP.ENCODING字段进行解密。



_HEAP结构


windows xp下的_HEAP结构

0:000> dt  ntdll!_HEAP 003b0000                

   +0x000 Entry            : _HEAP_ENTRY

   +0x008 Signature        : 0xeeffeeff ;heap结构签名

   +0x00c Flags            : 0x50001060 ;堆属性

   +0x010 ForceFlags       : 0x40000060 ;

   +0x014 VirtualMemoryThreshold : 0xfe00;最大堆块大小

   +0x018 SegmentReserve   : 0x100000

   +0x01c SegmentCommit    : 0x2000 

   +0x020 DeCommitFreeBlockThreshold : 0x200

   +0x024 DeCommitTotalFreeThreshold : 0x2000

   +0x028 TotalFreeSize    : 0x130;空闲块总大小,以粒度为单位

   +0x02c MaximumAllocationSize : 0x7ffdefff;堆可分配的最大内存

   +0x030 ProcessHeapsListIndex : 6;堆在peb.processheaps数组中的索引+1

   +0x032 HeaderValidateLength : 0x608

   +0x034 HeaderValidateCopy : (null)

   +0x038 NextAvailableTagIndex : 0

   +0x03a MaximumTagIndex  : 0

   +0x03c TagEntries       : (null)

   +0x040 UCRSegments      : (null)

   +0x044 UnusedUnCommittedRanges : 0x003b0598 _HEAP_UNCOMMMTTED_RANGE

   +0x048 AlignRound       : 0x17

   +0x04c AlignMask        : 0xfffffff8

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

   +0x058 Segments         : [64] 0x003b0640 _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 [ 0x3b0688 - 0x3b0688 ];空闲块

   +0x578 LockVariable     : 0x003b0608 _HEAP_LOCK

   +0x57c CommitRoutine    : (null)

   +0x580 FrontEndHeap     : (null)

   +0x584 FrontHeapLockCount : 0

   +0x586 FrontEndHeapType : 0 ''

   +0x587 LastSegmentIndex : 0 ''


在这个结构体中,FreeLists是一个由128个LIST_ENTRY结构体组成的结构体数组,在调用HeapCreate时,Freelists[0]指向尾块,尾块也就是堆刚创建出时最大的一个空闲块,数组中的其他元素指向自己。


3.创建进程时操作系统为进程创建的默认堆:


ntdll!KiUserApcDispather->ntdll!_LdrpInitialize->ntdll!LdrplInitializeProcess->ntdll!RtlCreateHeap,创建好的堆的句柄回保存在PEB结构的ProcessHeap字段中,PEB中关于堆的字段如下:

0:000> dt ntdll!_PEB

    +0x018 ProcessHeap      : Ptr32 Void //堆句柄

    +0x078 HeapSegmentReserve : Uint4B   //默认堆的保留大小:1MB

    +0x07c HeapSegmentCommit : Uint4B    //默认堆的提交大小:8kb

    +0x088 NumberOfHeaps    : Uint4B     //进程中堆的总数

    +0x08c MaximumNumberOfHeaps : Uint4B //ProcessHeaps数组中数组元素的个数

    +0x090 ProcessHeaps     : Ptr32 Ptr32 Void//这个数组用来保存进程中创建的堆的句柄


4.私有堆:


创建:


可以通过HeapCreate这个api来创建属于进程的私有堆,这个api实际上回去调用RtlCreateHeap函数,创建完毕之后会将创建好的堆句柄保存到peb结构中。HANDLE WINAPI HeapCreate(DWORD  flOptions,SIZE_T dwInitialSize, SIZE_T dwMaximumSize)这个api中,flOptions下面三个可选项:

  • HEAP_CREATE_ENABLE_EXECUTE 允许执行堆中的代码,也就是这时创建出的堆具有可执行属性。

  • HEAP_GENERATE_EXCEPTIONS 使用异常来报告HeapAlloc和HeapReAlloc的错误而不是通过返回NULL。

  • HEAP_NO_SERIALIZE 当堆函数访问这个堆时,不需要进行串行化控制。这么做可以提高操作堆的速度,但是在多线程同时操作堆时,可能出现问题。当创建堆时指定这个参数时,不能启用低碎片堆(LFH)。


在windbg中可以使用!heap -h指令来查看进程中的所有堆。!heap -h的查询结果也就是peb.ProcessHeaps中的值。


销毁:

HeapDestory->ntdll!RtlDestroyHeap->NtFreeVirtualMemory。
ntdll!RtlDestroyHeap会将PEB堆列表中的堆要销毁堆的堆句柄移除掉,然后通过NtFreeVirtualMemory函数向内存管理器归还内存。

应用程序不应该也不许销毁默认堆,当进程退出和销毁进程对象时,系统都会调用MnCleanProcessSpace函数。


堆内存的申请和释放


通过HeapAlloc这个api可以从堆中分配空间出来,LPVOID WINAPI HeapAlloc(HANDLE hHeap,DWORD  dwFlags,SIZE_T dwBytes),dwFlags有下面几个选项:

  • HEAP_GENERATE_EXCEPTIONS 系统通过返回异常而不是返回NULL来报告错误

  • HEAP_NO_SERIALIZE 不对堆进行串行话控制。

  • HEAP_ZERO_MEMORY 分配的内存将被初始化为0。

ps:指定这些值中的任何值将覆盖使用HeapCreate创建堆时指定的对应值 。

 

也可以使用malloc,calloc或者是new运算符来从堆中分配内存,上面提到CRT堆有三种模式,大多数情况下,运行时库都会选择系统模式,此时CRT堆建立在win32堆之上。

 

通过HeapFree这个api可以从释放HeapAlloc申请处的内存空间,free或者是delete最终都会跳到这个api上来释放内存。


winxp 堆内存释放和合并-空闲双向链表:


实验代码:

#include <stdio.h>

#include <windows.h>

main()

{

    HLOCAL h1,h2,h3,h4,h5,h6;

    HANDLE hp;

    hp = HeapCreate(0,0x1000,0x10000);

    getchar();

    __asm int 3

 

    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);

    h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);

    h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);

    h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

    h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);

    h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);

 

    HeapFree(hp,0,h1); //free to freelist[2]

    HeapFree(hp,0,h3); //free to freelist[2]

    HeapFree(hp,0,h5); //free to freelist[4]

 

    HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to freelist[8]

    return 0;

}


执行程序之后用windbg附加,然后f5,断到int 3,在windbg中查看空闲表

0:000>dd 0x003a0000+0x178

003a0178  003a0688 003a0688 003a0180 003a0180

003a0188  003a0188 003a0188 003a0190 003a0190

003a0198  003a0198 003a0198 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0


Heap.FreeLists[0]的flink和blink分别都指向0x003a0688.通过!heap -a  003a0000查到的结果,我们来看分配到的堆块结构,在windbg中输入
!heap -x 003a0640或者是dt ntdll!_HEAP_ENTRY来查看堆结构:


0:000> !heap -x 003a0640

Entry    User      Heap      Segment      Size  PrevSize  Unused    Flags

-----------------------------------------------------------------------------

003a0640  003a0648  003a0000  003a0640        40      640        0  busy

 

0:000> dt ntdll!_HEAP_ENTRY 003a0640

  +0x000 Size            : 8

  +0x002 PreviousSize    : 0xc8

  +0x000 SubSegmentCode  : 0x00c80008

  +0x004 SmallTagIndex    : 0 ''

  +0x005 Flags            : 0x1 ''

  +0x006 UnusedBytes      : 0 ''

  +0x007 SegmentIndex    : 0 '


其中_HEAP_ENTRY.Size为堆块的大小/堆块分配粒度,堆块分配粒度一般都为8,可以用!heap -v 堆基址来查看


然后单步执行完 h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3),观察空闲块的变化


0:000> dd 0x003a0000+0x178

003a0178  003a0698 003a0698 003a0180 003a0180

003a0188  003a0188 003a0188 003a0190 003a0190

003a0198  003a0198 003a0198 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0


此时Heap.FreeLists[0]的flink和blink分别都指向0x003a0698,在windbg中输入!heap -x 0x003a0698或者是dt ntdll!_HEAP_FREE_ENTRY 0x003a0698-8来查看堆结构:

0:000> !heap -x 003a0698

Entry    User      Heap      Segment      Size  PrevSize  Unused    Flags

-----------------------------------------------------------------------------

003a0690  003a0698  003a0000  003a0640      970        10        0  free last

 

0:000> dt ntdll!_HEAP_FREE_ENTRY 003a0698-8

  +0x000 Size            : 0x12e

  +0x002 PreviousSize    : 2

  +0x000 SubSegmentCode  : 0x0002012e

  +0x004 SmallTagIndex    : 0 ''

  +0x005 Flags            : 0x10 ''

  +0x006 UnusedBytes      : 0 ''

  +0x007 SegmentIndex    : 0 ''

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


在_HEAP_FREE_ENTRY中多了一个LIST_ENTRY成员,而这个成员的flink和blink都只想freelist[0]。单步执行完HeapFree(hp,0,h1);在windbg中观察下内存的变化:

0:000> dd 003a0000+0x178

003a0178  003a0708 003a0708 003a0180 003a0180

003a0188  003a0688 003a0688 003a0190 003a0190

003a0198  003a0198 003a0198 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0

003a01b8  003a01b8 003a01b8 003a01c0 003a01c0

003a01c8  003a01c8 003a01c8 003a01d0 003a01d0

003a01d8  003a01d8 003a01d8 003a01e0 003a01e0


可以看到Heap.FreeLists[2]成员的flink和blink分别都指向了0x003a0688,而在此之前,Heap.FreeLists[2]的两个成员都指向自己。单步执行完HeapFree(hp,0,h5)观察下内存结构

0:000> dd 003a0000+0x178

003a0178  003a0708 003a0708 003a0180 003a0180

003a0188  003a0688 003a06a8 003a0190 003a0190

003a0198  003a06c8 003a06c8 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0

003a01b8  003a01b8 003a01b8 003a01c0 003a01c0


FreeList[0].flink和blink都指向最后一个空闲块003a0708,FreeList[2].flink指向第一次释放的堆块,FreeList[2].blink指向第二次释放的堆块。在windbg中查看下这两个空闲块的结构

0:000> dt ntdll!_HEAP_FREE_ENTRY 003a0688-8

  +0x000 Size            : 2

  +0x002 PreviousSize    : 8

  +0x000 SubSegmentCode  : 0x00080002

  +0x004 SmallTagIndex    : 0xcb ''

  +0x005 Flags            : 0 ''

  +0x006 UnusedBytes      : 0xd ''

  +0x007 SegmentIndex    : 0 ''

  +0x008 FreeList        : _LIST_ENTRY [ 0x3a06a8 - 0x3a0188 ]

0:000> dt ntdll!_HEAP_FREE_ENTRY 003a06a8-8

  +0x000 Size            : 2

  +0x002 PreviousSize    : 2

  +0x000 SubSegmentCode  : 0x00020002

  +0x004 SmallTagIndex    : 0xcf ''

  +0x005 Flags            : 0 ''

  +0x006 UnusedBytes      : 0xa ''

  +0x007 SegmentIndex    : 0 ''

  +0x008 FreeList        : _LIST_ENTRY [ 0x3a0188 - 0x3a0688 ]


观察空闲块和Freelist表结构,我们可以得出结论:每个空闲块的前向指针指向前一个空闲块,第一个空闲块的前向指针指向相应的freelist头,每一个空闲块的后向指针指向下一个空闲块,最后一个空闲块的后向指针指向相应的freelist头,而freelist头的前向指针指向最后一个空闲块,freelist头的后向指针指向第一个空闲块,这样就构成了一个双循环链表。

 

单步执行完HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to freelist[8],观察下空闲块的结构:

0:000> dd 003a0000+0x178

003a0178  003a0708 003a0708 003a0180 003a0180

003a0188  003a0688 003a0688 003a0190 003a0190

003a0198  003a0198 003a0198 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0

003a01b8  003a06a8 003a06a8 003a01c0 003a01c0

003a01c8  003a01c8 003a01c8 003a01d0 003a01d0

003a01d8  003a01d8 003a01d8 003a01e0 003a01e0

003a01e8  003a01e8 003a01e8 003a01f0 003a01f0


此时三个空闲块从freelist[2],freelist[4]中被摘下,合并之后链入freelist[8]。


winxp 堆内存释放-快速单向链表:


快表的结构和空闲表的结构相似,表中元素为单链表表头,组织结构如下:

       快表

|---------------|

| lookaside[0]  |

| lookaside[1]  |-> [8bytes] -> [8bytes]

| lookaside[2]  |-> [16bytes]

| ..............|..................

| lookaside[126]|

| lookaside[127]|->[1016bytes]

|---------------|


实验代码:

#include <stdio.h>

 

#include <windows.h>

#include <windows.h>

main()

{

  HLOCAL h1,h2,h3,h4;

  HANDLE hp;

  hp = HeapCreate(0,0x0,0x0);

    getchar();

  __asm int 3;

 

    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

    h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

    h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);

    h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);

 

  HeapFree(hp,0,h1);free heap1 to lookaside[1];

  HeapFree(hp,0,h2);free heap2 to lookaside[1]

  HeapFree(hp,0,h3);free heap3 to lookaside[2];

  HeapFree(hp,0,h4);free heap4 to lookaside[3];

    h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);//alloc block from lookaside[2]

  return 0;

}


执行程序之后用windbg附加,f5,断到int3,然后单步到HeapFree(hp,0,h1)执行结束,此时观察下内存结构:

0:000> dd 003a0000+0x178

003a0178  003a1ee8 003a1ee8 003a0180 003a0180

003a0188  003a0188 003a0188 003a0190 003a0190

003a0198  003a0198 003a0198 003a01a0 003a01a0

003a01a8  003a01a8 003a01a8 003a01b0 003a01b0


此时我们观察下空闲块,并没有发现有是释放过的空闲块。在内存中查看下快表:

0:000> dd 003a0000+0x688 l50

003a0688  00000000 00000000 01000004 00000000

003a0698  00000000 00000000 00000000 00000000

003a06a8  00000000 00000004 00000000 00000000

003a06b8  00000000 00000000 01000004 00000000

003a06c8  00000000 00000000 00000000 00000000

003a06d8  00000000 00000000 00000000 00000000

003a06e8  003a1e90 00010001 01000004 00000002

003a06f8  00000002 00000001 00000000 00000000

003a0708  00000000 00000000 00000000 00000000

003a0718  00000000 00000000 01000004 00000001

003a0728  00000001 00000000 00000000 00000000

003a0738  00000000 00000000 00000000 00000000


发现在快表结构中多出了第一次我们申请到的地址0x003a1e90,在windbg中查询结果如下:


0:000> !heap -x 003a1e90

Entry     User      Heap      Segment       Size  PrevSize  Unused    Flags

-----------------------------------------------------------------------------

003a1e88  003a1e90  003a0000  003a0640        10      1808         8  busy


和空闲块有所不同的是,堆块的flag为busy。

单步执行完HeapFree(hp,0,h4);free heap4 to lookaside[3];之后,观察下内存中的块表信息


0:000> dd 003a0000+0x688 l50

003a0688  00000000 00000000 01000004 00000000

003a0698  00000000 00000000 00000000 00000000

003a06a8  00000000 00000004 00000000 00000000

003a06b8  00000000 00000000 01000004 00000000

003a06c8  00000000 00000000 00000000 00000000

003a06d8  00000000 00000000 00000000 00000000

003a06e8  003a1ea0 00020002 01000004 00000002

003a06f8  00000002 00000002 00000000 00000000

003a0708  00000000 00000000 00000000 00000000

003a0718  003a1eb0 00010001 01000004 00000001

003a0728  00000001 00000001 00000000 00000000

003a0738  00000000 00000000 00000000 00000000

003a0748  003a1ec8 00010001 01000004 00000001

003a0758  00000001 00000001 00000000 00000000

003a0768  00000000 00000000 00000000 00000000

003a0778  00000000 00000000 01000004 00000000






本文由看雪论坛 风暴烈酒 原创

转载请注明来自看雪社区


热门阅读


点击阅读原文/read,

更多干货等着你~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存