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,或者崩潰。具體的內存組織結構如下圖

image

摘自《軟件調試》

 

與普通堆不同的是,內存塊前面的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結構微軟也有對應文檔介紹

ms220938.Local_-1265171613_fphbs(en-US,VS.80).gif

(摘自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結構是如何組織的。

image

摘自《軟件調試》

 

這個是整個的頁堆結構圖,我們先來說說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鍵值。

堆的結構和堆的調試

或者使用Gflags 工具。

如果在調試器中運行一個程序,而且注冊表中沒有設置GlobalFlags鍵值,那么操作系統會默認啟用htchfchpc三項堆調試功能。

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值決定)。

如果要觸發堆管理器檢查這個模式是否被破壞,HFCHPC也要同時開啟。(具體設置前面有述,也可以用!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;

}

對於第一個堆:

調試狀態:

堆的結構和堆的調試

 

0004為一共分配的大小:4*8=32個字節。其中用戶申請了3個字節,按照粒度對齊到了8個字節,feee是填充的字段。再加上8個字節的HEAP_ENTRY結構,再加上HTC檢測用的8個字節的0xab,再加上8個字節的HEAP_ENTRY_EXTRA結構。共32個字節。

07flag標志:1busy+2(有EXTRA結構)+4(固定模式填充)

00段的序號

我們也看到對於沒有分配的空閑堆塊堆管理器是用feee填充的。

 

非調試狀態:

堆的結構和堆的調試

 

000216個字節,即8個字節的HEAP_ENTRY加上8個字節經過填充的申請的數據。

 

 

6)       頁堆

上述的所有堆的調試支持都有一定的滯后性。只有在堆的函數再次調用的時候才會去驗證堆的完整性(通過調用xxSlowly的函數),所以當我們發現溢出的時候可能已經離溢出的發生地點很遠了,這也主要是因為上述的調試支持並不是專門為了檢測溢出類漏洞而生的。

頁堆是檢測溢出類漏洞的一個很有效的辦法,盡管這是用大量浪費的空間和性能換來的.

堆的結構和堆的調試

         

 

其中的DPH_HEAP_ROOT結構是頁堆的真正管理結構,他相當於HEAP結構的功能。

 

堆的結構和堆的調試

開啟:gflags /p /enable Heap.exe /full

windbg中使用如下命令查看是否開啟頁堆:

堆的結構和堆的調試
堆的結構和堆的調試

 

 

查看頁堆:

可以使用命令 !heap –p ,!heap –p –h address查看,不過貌似在windbg6.10.x  6.11.x中有bug。我的版本是6.11.0001.402,運行時 有點問題。

堆的結構和堆的調試

 

+ 140000是進程堆的地址,每個頁堆都會附帶一個普通的堆,這個普通堆地址是240000

因為我的Windbgbug,下面使用《軟件調試》里的截圖:

堆的結構和堆的調試

 

堆塊結構:

頁堆的堆塊結構跟普通的堆塊是有很大的區別的,一個堆塊至少占用兩個頁面(8K),其中前一個頁面用於存儲用戶的數據,后一個頁面用於檢測溢出。為了能迅速發現溢出,用戶區的數據建立在第一個頁面的結尾處,這樣如果溢出發生,溢出的數據就會寫入到柵欄頁造成異常。

堆的結構和堆的調試

 

(除了上圖的結構外,對於每一個頁堆堆塊,在頁堆的節點池中還會有一個DPH_HEAP_BLOCK結構,即DPH節點結構。)

 

其中DPH_BLOCK_INFORMATION結構的長度是32個字節。

使用**alloc分配堆塊時,返回的是用戶區的地址,這個地址減去32個字節就是DPH_BLOCK_INFORMATION結構:

堆的結構和堆的調試

為了保護這個結構的完整性,在開始和結尾都加上了固定的值。

 

關於頁堆的填充字段可看下圖:

                      占用堆塊                          空閑堆塊

 

 堆的結構和堆的調試

 

我們直接在內存上觀察新分配的堆塊:

堆的結構和堆的調試

 

可以看到32個字節的DPH_BLOCK_INFORMATION后面就是申請的3個字節和用於填充的5個字節(被初始化為D0

??是柵欄區。

堆的結構和堆的調試

 

(頁堆用的句柄是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; }

 堆的結構和堆的調試

 

 

在發生的溢出點出錯。

 

7)      准頁堆

由於完全頁堆浪費大量的內存,故可能在調試大型程序時較慢(not try),故有時可以采用准頁堆的方法,其實個人感覺准頁堆的方式和HTC(堆尾檢查)是很像的,都有一定的滯后性。

開啟方法:

gflags /p /enable Heap.exe

結構:

堆的結構和堆的調試

 

填充模式:

 

占用堆塊

空閑堆塊

 

頁堆

准頁堆

頁堆

准頁堆

頭結構起始簽名

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的起始地址:

堆的結構和堆的調試

 

用戶區數據(e0)后面的8個字節的0xa0即是柵欄字節

堆的結構和堆的調試

 

0x31為實際的大小,即HEAP_ENTRY結構到柵欄數據(0xa0)結束之間的字節數。

32+9+8 = 49


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM