windbg调试堆破坏

堆破坏

所谓的堆破坏,是说没控制好自己的指针,把不属于你分配的那块内存给写覆盖了。这块内存可能是你程序的数据,也可能是堆的管理结构。那么这个会导致怎样的后果呢?可能的情况我们来yy下

  1. 把程序里的计算结果覆盖了,这也许会让你重复看了N次代码,校验了N次计算逻辑也搞不明白为何计算结果还是有问题
  2. 堆管理结构被破坏了,new/delete,或者malloc/free操作失败
  3. 等等等等~

堆破坏较为理想的情况是被修改的数据会马上导致程序crash,最差的情况是你的堆数据莫名其妙在今天被改了,但明天才crash。这个时候在去分析crash,就如我们的警察叔叔现在接手一桩10年前的案子一般----无从下手。老外称之为heap corruption是很贴切的,有时候咱堆数据被意外篡改是无声无息的,你也许没法从界面甚至日志文件中看到它被篡改的一点迹象,当到某一个时刻,这种错误会暴露出来,然而这个时候查看堆信息也许会是毫无头绪。所以对于堆破坏,咱的策略是尽早发现我们的堆被篡改了,最好能够在堆数据被意外篡改的那一时刻诱发一个异常来提醒我们----兄弟,你的堆被腐蚀了。

微软提供了一些方案,来帮助我们诊断堆破坏。一般来说,堆破坏往往都是写数据越界造成的(yy的第二种情况,如果是第一种情况其实还简单,下个内存断点就好),所以微软在堆分配上,给程序员门额外提供了2种堆分配模式--完全页堆(full page heap),准页堆(normal page heap),用来检测堆被写越界的情况。

完全页堆(FULL PAGE HEAP)

检测原理

完全页堆的检测基本思路是通过分配相邻的一个页,并将其设为不可访问属性,然后用户数据块会被分配到内存页的最末端,从而实现越界访问的检测。当我们对堆中分配的内存读写越界后便会访问到那个不可读的页,系统捕获到改次异常后会试图中断执行并将该异常上报给debugger,或者崩溃。具体的内存组织结构如下图

windbg调试堆破坏

摘自《软件调试》

与普通堆不同的是,内存块前面的HEAP_ENTRY结构被DPH_BLOCK_INFORMATION结构取代,这个结构内部记录了页堆模式下这个内存块的一些基本信息。如果用户数据区前面的数据,也就是DPH_BLOCK_INFORMATION结构被破坏了,那么在释放内存块的时候系统会报错,如果编程者对这块内存块读写越界了,当然,这里越界有几种情况:

  1. 读越界,但只是访问了块尾填充部分数据,那么系统不会报错
  2. 写越界,但只篡改了图中块尾填充的部分,那么在堆块释放的时候会报错
  3. 读越界,且超过了块尾填充的部分,访问到了栅栏页,那么系统会立即抛出一个异常并中断执行
  4. 写越界,且超过了块尾填充部分,写到了栅栏页,那么系统会立即抛出一个异常并中断执行

这里需要注意的还是块尾填充不一定存在,块尾填充是因为要满足堆内存的最小分配粒度,如果本身内存块的分配粒度就已经是最小分配粒度的倍数了,那么块尾填充就不存在了,比如堆内存分配粒度是是8 bytes,那么如果申请了14 bytes的话会有2 bytes的大徐小的块尾填充块,如果申请了24bytes,那么就没有块尾填充了,因为24正好是8的倍数。

示例

开启全页堆(用windbg目录下的gflags或者装一个appverifier都可以开启),通过自己写的一个heap.exe来看一下如何使用全页堆检测堆破坏情况heap.exe代码如下:

#include "windows.h"

int main()
{
HANDLE heap_handle = HeapCreate( NULL , 1024 , 0 ) ;
char *temp = NULL ; char *buffer = (char*)HeapAlloc(heap_handle , NULL , 128) ;
char *buffer1 = (char*)HeapAlloc(heap_handle , NULL , 121) ;
temp = buffer ; for( int i = 0 ; i < 138 ; ++i )
{
*(temp++) = 'a' ;
} HeapFree(heap_handle, 0 , buffer ) ;
HeapFree(heap_handle, 0 , buffer1 ) ;
HeapDestroy( heap_handle) ;
return 0 ;
}

在第14行向buffer写入138字节,这显然越界了,然后在用windbg启动heap.exe,直接运行,会发现报错如下

0:000> g
(1f50.1f54): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000080 ebx=00000000 ecx=02596000 edx=02596000 esi=00000001 edi=00193374
eip=00191068 esp=0016fdc8 ebp=0016fddc iopl=0         nv up ei ng nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010297
heap!main+0x68:
00191068 c60161          mov     byte ptr [ecx],61h         ds:0023:02596000=??

报了一个内存访问错误,然后看一下调用堆栈

0:000> kb
ChildEBP RetAddr  Args to Child              
0016fddc 0019120f 00000001 023fbfd0 0239df48 heap!main+0x68 [d:\projects\heap\main.cpp @ 14]
0016fe20 765b1114 7ffd3000 0016fe6c 778eb429 heap!__tmainCRTStartup+0x10f [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 582]
0016fe2c 778eb429 7ffd3000 757369d8 00000000 kernel32!BaseThreadInitThunk+0xe
0016fe6c 778eb3fc 00191357 7ffd3000 00000000 ntdll!__RtlUserThreadStart+0x70
0016fe84 00000000 00191357 7ffd3000 00000000 ntdll!_RtlUserThreadStart+0x1b

可以看到是第14行报的错,但是14行的代码运行了那么多次,我们再看一下这个时候变量i的值是多少

0:000> dv i
              i = 0n128

显然,在填充第128字节的时候,我们的temp指针访问到了栅栏页,从而报出了一个内存违规的异常。

这里顺带看一下如果我们分配的内存不是8 bytes的情况(一般堆内存分配粒度是8 bytes,所以申请128 bytes的内存时是不会有块尾填充部分的)

那我们接下来看另外一段代码

我们把第10行的temp = buffer改成temp = buffer1

因为buffer1申请了121 bytes,也就是说它有7 bytes的填充字节

0:000> g
(1ba0.1ba4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000080 ebx=00000000 ecx=024c8000 edx=024c8000 esi=00000001 edi=00033374
eip=00031068 esp=002cfb80 ebp=002cfb94 iopl=0         nv up ei ng nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010297
heap!main+0x68:
00031068 c60161          mov     byte ptr [ecx],61h         ds:0023:024c8000=??
0:000> dv i
              i = 0n128

可以看到变量i还是128,也就是说我们还是在访问到第128字节后才引发访问异常,而不是我们期望的121字节后就引发异常。

这里也就是说如果我们的代码中对申请的堆内存写越界了,写数据覆盖块尾填充部分的时候并不会引发异常!

但是,这并不代表我们的写越界问题不会被发现。块尾填充部分是会被填充上固定数据的,系统在适合的时机(比如销毁堆的时候)会校验块尾填充块,如果发现块尾填充块数据有变,那么便会报一个verifier异常,比如我们把代码中的for循环次数改为124

    for( int i = 0 ; i < 124 ; ++i )

那么windbg会中断在第19行

    HeapDestroy( heap_handle) ;

提示内容如下
=======================================
VERIFIER STOP 0000000F: pid 0x1E3C: Corrupted suffix pattern for heap block.

025A1000 : Heap handle used in the call.
    025A7F80 : Heap block involved in the operation.
    00000079 : Size of the heap block.
    025A7FF9 : Corruption address.

=======================================
This verifier stop is not continuable. Process will be terminated 
when you use the `go' debugger command.

=======================================

(1e3c.143c): Break instruction exception - code 80000003 (first chance)
eax=6c75e994 ebx=6c75cf58 ecx=00000002 edx=002bf461 esi=00000000 edi=000001ff
eip=6c753c38 esp=002bf6b4 ebp=002bf8b8 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
vrfcore!VerifierStopMessageEx+0x543:
6c753c38 cc              int     3

提示说的很清楚了,appverifier指出了堆和具体的内存块,我们这个时候查看buffer1的值是0x025a7f80 ,正好就是出问题的堆块,出问题的地址是0x025a7ff79,正好就是buffer1内存块的边界,错误原因是Corrupted suffix pattern for heap block,也就是说咱块尾填充部分(suffix pattern for heap block)被破坏(corrupted)了

结论:只要写越界,系统都能够检测出来,只不过如果写越界写到了栅栏页会理解触发异常中断,而写越界只写了块尾填充部分,那么系统在适当时机(比如堆被销毁,或者这块内存被重新分配等时机)会对块尾填充部分做完整性检测,如果发现被破坏了,就会报错。当然,你可以根据错误号(蓝色字体部分)信息去appverifier的帮助文档中查找更详细的错误说明。

结构详解

这次咱来倒叙,先从最基本的内存堆块结构DPH_BLOCK_INFORMATION开始介绍,DPH_BLOCK_INFORMATION结构微软也有对应文档介绍

windbg调试堆破坏

(摘自MSDN)

其中prefix start magic和prefix end magic是校验块,用来检测DPH_BLOCK_INFORMATION是否被破坏,这些检测部分属于DPH_BLOCK_INFORMATION结构。我们先来用windbg探究下DPH_BLOCK_INFORMATION这个最基本的结构.再一次,我们打开windbg调试heap.exe.运行到第10行,这个时候变量的值是

0:000> dv heap_handle
    heap_handle = 0x024a0000
0:000> dv buffer
         buffer = 0x024a5f80 "???"
0:000> dv buffer1
        buffer1 = 0x024a7f80 "???"

这里可以看到一个很有趣的现象,buffer1和buffer的地址正好相差8K,也就是两个页的大小.这当然是因为页堆的原因啦,其实这两块内存分配是相邻着的,虚拟内存结构如下图所示

buffer内存块(4K) 栅栏页(4K) buffer1内存块(4K) 栅栏页(4K)

由于buffer和buffer1分配的大小是一样的(buffer1加上尾部填充块和buffer的大小相同),所以这两块内存正好相差8K

而DPH_BLOCK_INFORMATION就在我们申请的内存块指针的前0x20字节处,用dt命令看的结果如下:

0:000> dt _DPH_BLOCK_INFORMATION 0x024a5f80-0x20
verifier!_DPH_BLOCK_INFORMATION
   +0x000 StartStamp       : 0xabcdbbbb
   +0x004 Heap             : 0x024a1000 Void
   +0x008 RequestedSize    : 0x80
   +0x00c ActualSize       : 0x1000
   +0x010 Internal         : _DPH_BLOCK_INTERNAL_INFORMATION
   +0x018 StackTrace       : 0x003d9854 Void
   +0x01c EndStamp         : 0xdcbabbbb

0x024a5f80-0x20就是DPH_BLOCK_INFORMATION结构的地址。DPH_BLOCK_INFORMATION结构在已分配和已释放的状态下,StartStamp和EndStamp(也就是MSDN图中的prefix start magic和prefix end magic)是不同的,显然dt输出的结果看来,这个内存块是已分配状态。StackTrace记录了分配这个内存块时的调用栈,可以用dds来看一下这个内存块被分配时候的调用栈

0:000> dds 0x003d9854 
003d9854  00000000
003d9858  00004001
003d985c  00090000
003d9860  5b3b8e89 verifier!AVrfDebugPageHeapAllocate+0x229
003d9864  776d5c4e ntdll!RtlDebugAllocateHeap+0x30
003d9868  77697e5e ntdll!RtlpAllocateHeap+0xc4
003d986c  776634df ntdll!RtlAllocateHeap+0x23a
003d9870  003b1030 heap!main+0x30 [d:\projects\heap\main.cpp @ 8]
003d9874  003b120c heap!__tmainCRTStartup+0x10f [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 582]
003d9878  76451114 kernel32!BaseThreadInitThunk+0xe
003d987c  7766b429 ntdll!__RtlUserThreadStart+0x70
003d9880  7766b3fc ntdll!_RtlUserThreadStart+0x1b

输出结果我们可以看到这个内存块是在main.cpp,也就是我们的示例代码的第8行分配的,第8行是char *buffer = (char*)HeapAlloc(heap_handle , NULL , 128) 正好就是分配buffer内存的那条语句。这个结构的其它字段,顾名思义,ActualSize指明了实际分配字节数,0x1000 bytes也就是4K大小,Internal这个字段保存了个内部结构,用windbg也看不出这个结构信息。

当然为了防止内存块前面的数据被冲刷掉,除了DPH_BLOCK_INFORMATION外,系统还通过DPH_HEAP_BLOCK保存了所分配内存块的信息,

通过!heap –p –h [address] 可以查看到页堆的信息

0:000> !heap -p -h 0x024a0000                            //heap_handle的值
    _DPH_HEAP_ROOT @ 24a1000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
        024a1f6c : 024a5f80 00000080 - 024a5000 00002000
        024a1f38 : 024a7f80 00000079 - 024a7000 00002000

可以看到,buffer内存块对应的DPH_HEAP_BLOCK结构地址是024a1f6c

0:000> dt _DPH_HEAP_BLOCK 024a1f6c
verifier!_DPH_HEAP_BLOCK
   +0x000 NextFullPageHeapDelayedNode : 0x024a1020 _DPH_HEAP_BLOCK
   +0x004 DelayQueueEntry  : _DPH_DELAY_FREE_QUEUE_ENTRY
   +0x000 LookasideEntry   : _LIST_ENTRY [ 0x24a1020 - 0x0 ]
   +0x000 UnusedListEntry  : _LIST_ENTRY [ 0x24a1020 - 0x0 ]
   +0x000 VirtualListEntry : _LIST_ENTRY [ 0x24a1020 - 0x0 ]
   +0x000 FreeListEntry    : _LIST_ENTRY [ 0x24a1020 - 0x0 ]
   +0x000 TableLinks       : _RTL_BALANCED_LINKS
   +0x010 pUserAllocation  : 0x024a5f80  "???"
   +0x014 pVirtualBlock    : 0x024a5000  "???"
   +0x018 nVirtualBlockSize : 0x2000
   +0x01c Flags            : _DPH_HEAP_BLOCK_FLAGS
   +0x020 nUserRequestedSize : 0x80
   +0x024 AdjacencyEntry   : _LIST_ENTRY [ 0x24a1f5c - 0x24a1fc4 ]
   +0x02c ThreadId         : 0x3f4
   +0x030 StackTrace       : 0x003d9854 Void

从dt的数据看来,这个结构大小为0x34,buffer和buffer1的DPH_HEAP_BLOCK结构首地址正好也是相差0x34,说明这两个结构是紧挨着的,下一步在让我们来看看DPH_HEAP_BLOCK结构是如何组织的。

windbg调试堆破坏

摘自《软件调试》

这个是整个的页堆结构图,我们先来说说DPH_HEAP_BLOCK的组织吧,在图中0x16d00000是页堆的首地址,也就是页堆的句柄,我们调试器中,页堆首地址则是0x024a0000,为了数据统一,我还是拿0x024a0000作为堆句柄来讲解。我们的DPH_HEAP_BLOCK其实就在堆块节点池里边,我们可以近似把这个节点池看成一个大型的DPH_HEAP_BLOCK数组,但有个地方在软件调试中没有提到,就是在win7下,运行时这些DPH_HEAP_BLOCK结构都是以二叉平衡数的结构来组织的,这个树的结构的入口正是在TableLinks字段内,这么做的原因也大概是因为能够在分配时更快的索。我们再看看DPH_HEAP_ROOT结构,这个结构储存了整个页堆的必要信息,它就相当于普通堆的_HEAP结构。

0:000> dt _dph_heap_root 24a1000
verifier!_DPH_HEAP_ROOT
   +0x000 Signature        : 0xffeeddcc
   +0x004 HeapFlags        : 0x1002
   +0x008 HeapCritSect     : 0x024a16cc _RTL_CRITICAL_SECTION
   +0x00c NodesCount       : 0x2c
   +0x010 VirtualStorageList : _LIST_ENTRY [ 0x24a1fa0 - 0x24a1fa0 ]
   +0x018 VirtualStorageCount : 1
   +0x01c PoolReservedLimit : 0x024a5000 Void
   +0x020 BusyNodesTable   : _RTL_AVL_TABLE
   +0x058 NodeToAllocate   : (null) 
   +0x05c nBusyAllocations : 2
   +0x060 nBusyAllocationBytesCommitted : 0x4000
   +0x064 pFreeAllocationListHead : (null) 
   +0x068 FullPageHeapDelayedListTail : (null) 
   +0x06c DelayFreeQueueHead : (null) 
   +0x070 DelayFreeQueueTail : (null) 
   +0x074 DelayFreeCount   : 0
   +0x078 LookasideList    : _LIST_ENTRY [ 0x24a1078 - 0x24a1078 ]
   +0x080 LookasideCount   : 0
   +0x084 UnusedNodeList   : _LIST_ENTRY [ 0x24a1ed0 - 0x24a16e4 ]
   +0x08c UnusedNodeCount  : 0x28
   +0x090 nBusyAllocationBytesAccessible : 0x2000
   +0x094 GeneralizedFreeList : _LIST_ENTRY [ 0x24a1f04 - 0x24a1f04 ]
   +0x09c FreeCount        : 1
   +0x0a0 PoolCommitLimit  : 0x024a2000 Void
   +0x0a4 NextHeap         : _LIST_ENTRY [ 0x5b3e9a58 - 0x23a10a4 ]
   +0x0ac ExtraFlags       : 3
   +0x0b0 Seed             : 0xfed6f13a
   +0x0b4 NormalHeap       : 0x027d0000 Void
   +0x0b8 CreateStackTrace : 0x003d9824 _RTL_TRACE_BLOCK
   +0x0bc ThreadInHeap     : (null) 
   +0x0c0 BusyListHead     : _LIST_ENTRY [ 0x24a10c0 - 0x24a10c0 ]
   +0x0c8 SpecializedFreeList : [64] _LIST_ENTRY [ 0x24a10c8 - 0x24a10c8 ]
   +0x2c8 DelayFreeListLookup : [257] (null) 
   +0x6cc HeapCritSectionStorage : _RTL_CRITICAL_SECTION

这里边维护了很多运行时信息,比如说DPH_BLOCK_INFORMATION中的那个二叉树入口其实就是保存在BusyNodesTable 字段,这里面记录了所有被分配了的内存块所对应的DPH_BLOCK_INFORMATION。当然,这里面一些信息软件调试里面都有介绍,很多看名字也能够猜到大概意思,看名字猜不到啥意思的字段,其实我也猜不到。。。-_-|||在创建页堆后,所有内存分配都分配在页堆中,通过分配的地址也能看得出来(我们分配的内存都是024a打头),而非普通页堆中,普通页堆也仅仅只是保存一些系统内部使用的数据。一般来说,堆块节点池加上DPH_HEAP_ROOT结构大小正好是4个内存页,也就是16K。

优缺点

缺点:消耗大量虚拟内存,每块内存的分配粒度是2个页(8K),

优点:能够立即捕获越界读写操作,通过调用栈就可以追溯到问题源头。能够快速定位问题代码。

五、        堆的调试支持

HTC   HFC   HPC   HVC   UST    DPH

1)       全局标志:

可以查看下图注册表路径中该程序名子健下的GlobalFlag键值。

windbg调试堆破坏

或者使用Gflags 工具。

如果在调试器中运行一个程序,而且注册表中没有设置GlobalFlags键值,那么操作系统会默认启用htc,hfc,hpc三项堆调试功能。

在windbg中可以使用  !gflag 命令查看。如果是附加到一个已经运行的进程,则系统会默认设置为0。

2)       释放检查:

为了防止两次释放一个堆而产生错误。

如果启用了堆调试功能,RtlAllocateHeap会调用RtlAllocateHeapSlowly函数执行真正的堆分配功能。同理RtlFreeHeap也会调用RtlFreeHeapSlowly函数执行堆的释放。这会导致执行速度的下降。

3)       栈回溯数据库(UST)

记录分配堆块时的函数调用,即栈回溯的记录。

管理结构:STACK_TRACE_DATABASE

使用全局变量ntdll!RtlpStackTraceDatabase指向这个内存区域。

具体过程 略

堆分配函数把RtlLogStackBackTrace返回的回溯记录的索引号存入堆结尾的HEAP_ENTRY_EXTRA数据结构中。

HEAP_ENTRY_EXTRA结构: 两个字节为UST记录,两个字节为堆块标记号,其余四个字节用于存储用户设置的数值。如果没有设置则为0。

4)       调用时验证(HVC)

为了检查堆中的异常情况,在堆管理器的堆函数每次被调用时都对堆进行检查。

RtlpValidateHeap函数是验证堆的函数,他会对堆进行全面检查。这个函数是由上述XXslowly函数间接调用的。

如果是在调试器中运行,则会触发断点异常(INT 3),然后切换到调试器。

如果不是,则不会触发断点异常,但仍会检测到错误。

HVC可以防止堆溢出的破坏。

5)       堆尾检查(HTC)

主要是为了检测堆溢出,但是存在滞后性。

方法是在堆块的用户申请的区域后加上8个字节的0xAB。(这个数值由ntdll!CheckHeapFillPattern值决定)。

如果要触发堆管理器检查这个模式是否被破坏,HFC和HPC也要同时开启。(具体设置前面有述,也可以用!gflag +htc +hfc +hpc)

实际观察堆的结构(调试状态和非调试状态):

Code:

#include <windows.h>

int main()

{

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

HANDLE hp;

//hp = HeapCreate(0,0x1000,0x10000);// 不可扩展的堆

hp = HeapCreate(0,0,0);

__asm int 3;

//h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

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);

HeapFree(hp,0,h3);

HeapFree(hp,0,h5);

HeapFree(hp,0,h4);

return 0;

}

对于第一个堆:

调试状态:

windbg调试堆破坏

0004为一共分配的大小:4*8=32个字节。其中用户申请了3个字节,按照粒度对齐到了8个字节,feee是填充的字段。再加上8个字节的HEAP_ENTRY结构,再加上HTC检测用的8个字节的0xab,再加上8个字节的HEAP_ENTRY_EXTRA结构。共32个字节。

07为flag标志:1(busy)+2(有EXTRA结构)+4(固定模式填充)

00段的序号

我们也看到对于没有分配的空闲堆块堆管理器是用feee填充的。

非调试状态:

windbg调试堆破坏

0002为16个字节,即8个字节的HEAP_ENTRY加上8个字节经过填充的申请的数据。

6)       页堆

上述的所有堆的调试支持都有一定的滞后性。只有在堆的函数再次调用的时候才会去验证堆的完整性(通过调用xxSlowly的函数),所以当我们发现溢出的时候可能已经离溢出的发生地点很远了,这也主要是因为上述的调试支持并不是专门为了检测溢出类漏洞而生的。

页堆是检测溢出类漏洞的一个很有效的办法,尽管这是用大量浪费的空间和性能换来的.

windbg调试堆破坏

其中的DPH_HEAP_ROOT结构是页堆的真正管理结构,他相当于HEAP结构的功能。

windbg调试堆破坏

开启:gflags /p /enable Heap.exe /full

在windbg中使用如下命令查看是否开启页堆:

windbg调试堆破坏
windbg调试堆破坏

查看页堆:

可以使用命令 !heap –p ,!heap –p –h address查看,不过貌似在windbg6.10.x 到 6.11.x中有bug。我的版本是6.11.0001.402,运行时 有点问题。

windbg调试堆破坏

+ 140000是进程堆的地址,每个页堆都会附带一个普通的堆,这个普通堆地址是240000

因为我的Windbg的bug,下面使用《软件调试》里的截图:

windbg调试堆破坏

堆块结构:

页堆的堆块结构跟普通的堆块是有很大的区别的,一个堆块至少占用两个页面(8K),其中前一个页面用于存储用户的数据,后一个页面用于检测溢出。为了能迅速发现溢出,用户区的数据建立在第一个页面的结尾处,这样如果溢出发生,溢出的数据就会写入到栅栏页造成异常。

windbg调试堆破坏

(除了上图的结构外,对于每一个页堆堆块,在页堆的节点池中还会有一个DPH_HEAP_BLOCK结构,即DPH节点结构。)

其中DPH_BLOCK_INFORMATION结构的长度是32个字节。

使用**alloc分配堆块时,返回的是用户区的地址,这个地址减去32个字节就是DPH_BLOCK_INFORMATION结构:

windbg调试堆破坏

为了保护这个结构的完整性,在开始和结尾都加上了固定的值。

关于页堆的填充字段可看下图:

占用堆块                          空闲堆块

windbg调试堆破坏

我们直接在内存上观察新分配的堆块:

windbg调试堆破坏

可以看到32个字节的DPH_BLOCK_INFORMATION后面就是申请的3个字节和用于填充的5个字节(被初始化为D0)

??是栅栏区。

windbg调试堆破坏

(页堆用的句柄是016e1000,这个是DPH_HEAP_ROOT结构,可见当这个堆被当作页堆理解时,这个堆的句柄是DPH_HEAP_ROOT结构而不是上一页(4K)伪装的HEAP结构)

 

检测溢出:

Code:

#include <windows.h>

int main()

{

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

HANDLE hp;

//hp = HeapCreate(0,0x1000,0x10000);// 不可扩展的堆

//__asm int 3;

hp = HeapCreate(0,0,0);

//__asm int 3;

//h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

char *p;

__asm int 3;

p= (char *)HeapAlloc(hp,0,9);

for(int i=0;i<20; i++)

*(p+i)=i; }

windbg调试堆破坏

在发生的溢出点出错。

7)      准页堆

由于完全页堆浪费大量的内存,故可能在调试大型程序时较慢(not try),故有时可以采用准页堆的方法,其实个人感觉准页堆的方式和HTC(堆尾检查)是很像的,都有一定的滞后性。

开启方法:

gflags /p /enable Heap.exe

结构:

windbg调试堆破坏

填充模式:

占用堆块

空闲堆块

页堆

准页堆

页堆

准页堆

头结构起始签名

ABCDBBBB

ABCDAAAA

ABCDAAA9

ABCDBBBA

头结构结束签名

DCBABBBB

DCBABBBB

DCBAAAA9

DCBABBBA

用户区

C0

E0

F0

F0

栅栏字节

N/A

A0

N/A

N/A

补齐字节

D0

00

N/A

N/A

如果分配(调用HeapAlloc)时指定了参数HEAP_ZERO_MEMORY,那么用户区会被填充为0

准页堆是从上述的那个页堆的附属的普通堆上分配堆块的。

返回的句柄是用户区的地址,减去40个字节(32个字节的HEAP_BLOCK_INFORMATION再加上8个字节的HEAP_ENTRY),即是HEAP_ENTRY的起始地址:

windbg调试堆破坏

用户区数据(e0)后面的8个字节的0xa0即是栅栏字节

windbg调试堆破坏

0x31为实际的大小,即HEAP_ENTRY结构到栅栏数据(0xa0)结束之间的字节数。

32+9+8 = 49

上一篇:Java 中的几种线程池,你之前用对了吗


下一篇:操作系统笔记系列 一 Linux