快速定位windows堆溢出
windows堆溢出的排查的困難
windows的堆溢出,具有一定的滯后性,溢出后有時不會立即崩潰,可能運行一段時間后再崩潰,此時查看調用堆棧,也不是溢出的第一現場,此時的調用堆棧意義也不大,看不出什么貓膩,所以堆溢出,對開發人員排查具有一定的難度。本文帶你尋找定位堆溢出的第一現場。
頁堆結構
1、頁堆結構
圖1畫出了頁堆的結構,其中的地址是以x86系統中的一個典型頁堆。左側的矩形是頁堆的主題部分,右側是附屬的普通堆,創建每個頁堆時,堆管理器都會創建一個附屬的普通堆。
頁堆上空間大多是以內存頁來組織的,第一個內存頁(起始4KB)用來偽裝普通堆的HEAP結構,大多數空間被填充為0xeeeeeeee,只有少數字段(Flags和ForceFlags)是有效的,這個內存頁的屬性是只讀的。因此可用來檢測應用程序意外寫HEAP結構的錯誤。
第二個內存頁的開始處是一個DPH_HEAP_ROOT結構,該結構包含了DPH的各種信息和鏈表,是描述和管理頁堆的重要資料。它的第一個字段是這個結構的簽名(signature),固定為0xffeeddcc,與普通的堆結構的簽名0xeeffeeff不同。它的NormalHeap字段記錄著附屬普通堆的句柄。
DPH_HEAP_ROOT結構之后的一段空間用來存儲堆塊節點,稱為堆塊節點池(node pool)。為了防止堆塊的管理信息被覆蓋,除了在堆塊的用戶數據區前面儲存堆塊信息,頁堆還會在節點池中為每個堆塊記錄一個DPH_HEAP_BLOCK結構,簡稱為DPH節點結構。多個節點是以鏈表的形式鏈接在一起的。
DPH_HEAP_BLOCK結構的pNodePoolListHead字段記錄這個鏈表的開頭,pNodePoolListTail字段記錄鏈表的結尾。
它的第一個節點描述的是DPH_HEAP_ROOT結構和節點池本身所占用的空間。節點池的典型大小是4個內存頁(16KB)減去DPH_HEAP_ROOT結構的大小。
節點池后的一個內存頁用來存放同步用的關鍵區對象,即_RTL_CRITICAL_SECTION結構,這個結構之外的空間被填充為0,DPH_HEAP_BLOCK結構的HeapCritSect字段記錄著關鍵區對象的地址。

2、堆塊結構
與普通堆塊相比,頁堆的堆塊結構有很大的不同,每個堆塊至少占用兩個內存頁(1個內存頁4KB),在用于存放用戶數據的內存頁后面,堆管理器總會多分配一個內存頁,這個內存頁是專門用來檢測溢出的,我們稱其為柵欄頁(fense page)。
柵欄頁的頁屬性被設置為不可訪問(PAGE_NOACCESS),因此,一旦用戶數據區發生溢出并觸及柵欄頁,便會引發異常,如果程序在調試,那么調試器便會立刻收到異常,使調試人員可以在第一現場發現問題,并迅速定位到導致溢出的代碼.
數據區在第一個內存頁的結尾,第二個內存頁緊鄰在數據區的后面,圖2顯示了這樣的一個頁堆堆塊(DPH_HEAP_BLOCK)的數據布局。

(圖2 堆塊結構 摘自《軟件調試》)
堆溢出調查方法一:使用頁堆調查堆溢出
頁堆的工作機制
在堆塊周圍加一個不可訪問的保護頁,用來將各個堆塊區分開;如果保護頁被覆蓋,將盡可能在接近覆蓋問題發生的地方檢測到問題并中斷給調試器。
頁堆有2種模式
普通頁堆(normal page heap)和完全頁堆(full page heap),兩者主要差別在于保護方式(普通頁堆在堆前后用固定填充數據保護,完全頁堆用一個不可訪問的頁保護)。
最常見的元數據被覆蓋問題是在使用堆時沒有考慮到邊界問題,導致的溢出問題。下面是溢出可能影響堆結構的示意圖:

(圖3 堆溢出圖示)
測試代碼:
//堆溢出示例//現象:輸入字符長度小于等于10,程序正常運行;超過10個字符,程序會崩潰#include #include #include #include "dump.h"WCHAR* pszCopy = NULL;bool DupString(WCHAR* psz) { bool bRet = false; if (psz != NULL) { pszCopy = (WCHAR*)HeapAlloc(GetProcessHeap(), 0, 10 * sizeof(WCHAR)); //10個寬字符內存 if (pszCopy) { wcscpy(pszCopy, psz); //隱患:字符超過10個就會產生溢出 wprintf(L"Copy of string: %s", pszCopy); HeapFree(GetProcessHeap(), 0, pszCopy); bRet = true; } } return bRet;}void __cdecl wmain(int argc, WCHAR* args[]) { SetUnhandledExceptionFilter(ExceptionFilter);//異常回調,生成dump文件 if (2 == argc) { wprintf(L"Press any key to start"); _getch(); DupString(args[1]); } else { wprintf(L"Please enter a string"); }}
step 1:啟動完全頁堆
gflag啟用頁堆,執行命令后,其實是修改的注冊表。
命令:gflags.exe /p /enable app.exe /full

(圖4 對測試程序開啟頁堆)

(圖 5 gflags啟用頁堆后,gflags修改的注冊表)
step 2:WinDbg調試程序
1) 確定頁堆是否啟用

(圖 6 windbg查看頁堆是否啟動)
2) 觸發堆溢出
windbg打開可執行程序、輸入參數:xiaosangTestForOverflow,這個參數會拷貝到動態分配的緩沖區,用來溢出緩沖區。
使用命令.reload /f testHeapOverflow.exe加載符號,然后在調試的程序中按下任意鍵(代碼中getchar),觸發緩沖區溢出。

(圖 7 windbg打開程序,并傳遞參數)

(圖 8加載符號,并運行)

(圖 9 堆溢出報錯)
step 3:關閉完全頁堆
gflags -p /disable testHeapOverflow.exe

(圖 10 關閉完全頁堆)
堆溢出調查方法二:應用程序驗證器
step 1:應用程序驗證器設置
選擇File->add application,然后右擊Basics下的Heaps,選擇full,然后保存。
設置后,實際修改是注冊表(下圖可以看到設置后注冊表的變化)。

(圖 11 使用應用程序驗證器完全頁堆)

(圖 12 使用應用程序驗證器完全頁堆后的注冊表變化)
step 2:WinDbg調試結果
運行應用程序

(圖 13 打開測試程序,并傳遞參數)
應用程序崩潰之后,再次輸入go,獲取校驗停止碼,顯示是13。

(圖 14 查看校驗停止碼)
查詢校驗停止的原因,并根據指導進行查找原因。

(圖 15 打開堆校驗停止窗口)

(圖 16 查看如何排查校驗停止碼13的方法)
根據校驗停止碼的原因,在windbbg輸入!heap –p –a accessAddr。

(圖 17 查看堆溢出地址的調用堆棧,觀看溢出內存)