軟件調試(12) - Win32堆,CRT堆簡介及堆溢出攻擊及系統檢測溢出方案

Win32堆與CRT堆
------------------------------------------------
堆分配函數:
malloc / HeapAlloc/ new


堆的基本運作:
內存管理器(Memory Manager)把一塊較大的內存空間,委托給堆管理器(Heap Manager)來管理,堆管理器將大塊內存分割成不同大小的很多個小塊來滿足應用程序的需要
而實現內存委托的一系列函數,稱為池管理器(Pool Manager)

Windows在創建進程時,加載器函數執行進程用戶態初始化階段會調用RtlCreateHeap函數為新的進程創建第一個堆
稱為默認堆

默認堆:
由LdrpInitializeProcess所調用RtlCreateHeap建立

此時新進程中只有EXE模塊和NTDLL模塊,EXE模塊中的用戶代碼還未執行,創建好的堆句柄會存放在PEB中ProcessHeap字段,

與棧類似也有HeapSegmentReserve和HeapSegmentCommit兩個字段來決定默認堆的保留大小和提交大小


分配私有Win32堆:
用戶透過HeapCreate函數創建自己的堆

函數原型:
HANDLE WINAPI HeapCreate(
_In_ DWORD  flOptions,
_In_ SIZE_T dwInitialSize,
_In_ SIZE_T dwMaximumSize
);
刪除則是調用:
BOOL WINAPI HeapDestroy(
_In_ HANDLE hHeap
);
堆列表:
每個進程的PEB結構以列表方式記錄當前進程的所有堆句柄:
NumberOfHeap 是堆總數
ProcessHeaps 每個堆的句柄,它是一個數組
MaximumNumberOfHeaps 可分配堆總數
如果NumberOfHeap == MaximumNumberOfHeaps 堆管理器則增大MaximumNumberOfHeaps 的值並重新分配ProcessHeaps

分配堆空間:
使用HeapAlloc可以在指定堆中分配空間

函數原形:
LPVOID WINAPI HeapAlloc(
_In_ HANDLE hHeap,
_In_ DWORD  dwFlags,
_In_ SIZE_T dwBytes
);

而其實現只是調用ntdll!RtlAllocateHeap
CRT堆分配函數也是簡單的調用HeapAlloc,如malloc

釋放從堆中分配的內存:

與之對應的是HeapFree->ntdll!RtlFreeHeap函數
在malloc(),calloc(),delete關鍵字底層實現都劃一的透過調用HeapFree(RtlFreeHeap)從堆釋放內存塊

delete函數的匯編:
004011c2 e959010000 jmp HiHeap!free(00401320)

為提高效率,這裡實現只是簡單的跳轉指令

全局堆與局域堆
----------------------------------------
GlobalAlloc與GlobalFree
LocalAlloc與LocalFree
前者在NT系統已經停用,後者則簡單的調用ntdll!RtlAllocateHeap



Win32堆(Heap)的結構和布局:
----------------------------------------
堆管理使用HEAP結構記錄及維護堆的信息

堆由不同大小的堆塊所組成 -> 每個堆塊起始處一定是一個8字節的heap_entry結構後面便是供應用程序用的區域
HEAP_ENTRY結構前兩字節是以分配粒度表示的堆塊大小, 分配粒度通常為8即2^16*8=0X80000=512kb 但需要減去8個字節的基本控制信息HEAP_ENTRY,因此每個堆塊最大可用上限為0X7FFF8

當我們要分配一塊大於512KB的堆塊時,堆管理器會直接調用ZwAllocateVirtualMemory來滿足分配

堆->有很多的段->每個段由很多區塊組成

*注意這是都是邏輯上的分區,而實際都是在堆空間中的玩意

HEAP結構:
它為HEAP的管理結構 用於管理每一個堆的信息
而HeapCreate事實上是返回這個句柄則是指向heap結構的指針
結構如下:
dt ntdll!_HEAP 00140000
ntdll!_HEAP
+0x000 Entry            : _HEAP_ENTRY  //用於存放管理結構的堆塊結構
+0x008 Signature        : 0xeeffeeff   //heap結構簽名,固定值
+0x00c Flags            : 0x1002   //堆標誌
+0x010 ForceFlags       : 0    //強制標誌
+0x014 VirtualMemoryThreshold : 0xfe00  //最大堆塊大小
+0x018 SegmentReserve   : 0x200000   //段的保留空間大小
+0x01c SegmentCommit    : 0x2000   //每次提交內存的大小
+0x020 DeCommitFreeBlockThreshold : 0x200  //解除提交的單塊臨介點
+0x024 DeCommitTotalFreeThreshold : 0x2000  //
+0x028 TotalFreeSize    : 0x146a   //空閒塊總大小,以粒度為單位
+0x02c MaximumAllocationSize : 0x7ffdefff  //可分配最大值
+0x030 ProcessHeapsListIndex : 7   //本堆在進程堆列表中的索引
+0x032 HeaderValidateLength : 0x608   //頭結構長度驗證 實際為0x640
+0x034 HeaderValidateCopy : (null) 
+0x038 NextAvailableTagIndex : 0
+0x03a MaximumTagIndex  : 0    //下一個可用的堆塊標記索引
+0x03c TagEntries       : (null)    //最大的堆塊標記索引號
+0x040 UCRSegments      : (null)    //指向用於標記堆塊的標記結構
+0x044 UnusedUnCommittedRanges : 0x000305b8 _HEAP_UNCOMMMTTED_RANGE 
+0x048 AlignRound       : 0xf
+0x04c AlignMask        : 0xfffffff8   //用於地址對齊的掩碼 
+0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x30050 - 0x30050 ]
+0x058 Segments         : [64] 0x00030640 _HEAP_SEGMENT //段數組
+0x158 u                : __unnamed
+0x168 u2               : __unnamed
+0x16a AllocatorBackTraceIndex : 0
+0x16c NonDedicatedListLength : 3
+0x170 LargeBlocksIndex : (null) 
+0x174 PseudoTagEntries : (null) 
+0x178 FreeLists        : [128] _LIST_ENTRY [ 0xae0048 - 0x321e8 ]
+0x578 LockVariable     : 0x00030608 _HEAP_LOCK
+0x57c CommitRoutine    : (null) 
+0x580 FrontEndHeap     : 0x00030688 
+0x584 FrontHeapLockCount : 0
+0x586 FrontEndHeapType : 0x1 ''
+0x587 LastSegmentIndex : 0x1 ''

HEAP_SEGMENT結構:
每個HEAP有個HEAP_SEGMENT結構數組,同時每個HEAP_SEGMENT有個堆塊HEAP_ENTRY的鏈表
結構如下:

0:001> dt _HEAP_SEGMENT 00030640
ntdll!_HEAP_SEGMENT
+0x000 Entry            : _HEAP_ENTRY  //段中存放結構堆塊
+0x008 Signature        : 0xffeeffee   //段結構的簽名,固定值
+0x00c Flags            : 0    //標誌
+0x010 Heap             : 0x00030000 _HEAP  //段所屬堆
+0x014 LargestUnCommittedRange : 0x8000
+0x018 BaseAddress      : 0x00030000   //段基址(相對於堆)
+0x01c NumberOfPages    : 0x10   //段的內存頁數
+0x020 FirstEntry       : 0x00030680 _HEAP_ENTRY //段中第一個堆塊
+0x024 LastValidEntry   : 0x00040000 _HEAP_ENTRY //堆塊的邊界
+0x028 NumberOfUnCommittedPages : 8   //尚未提交的內存頁數
+0x02c NumberOfUnCommittedRanges : 1   
+0x030 UnCommittedRanges : 0x00030588 _HEAP_UNCOMMMTTED_RANGE
+0x034 AllocatorBackTraceIndex : 0   //UST記錄序號
+0x036 Reserved         : 0
+0x038 LastEntryInSegment : 0x000321e0 _HEAP_ENTRY //最後一個堆塊


HEAP_ENTRY結構:
從HeapAlloc返回的地址 - 8可以獲取HEAP_ENTRY結構信息

0:001> dt _HEAP_ENTRY 0x00030680
ntdll!_HEAP_ENTRY
+0x000 Size             : 0x301 //以粒度表示的塊大小,不包本結構
+0x002 PreviousSize     : 8  //前一個堆塊大小
+0x000 SubSegmentCode   : 0x00080301 //子段代碼
+0x004 SmallTagIndex    : 0x62 'b' //堆塊的標記序號
+0x005 Flags            : 0x1 '' //堆塊的狀態標誌
+0x006 UnusedBytes      : 0x8 '' //用戶數據區後的未用字節數(為滿足對齊而消耗的字節數)
+0x007 SegmentIndex     : 0 '' //所在段序號
總結
尋找一個堆塊,可從 堆結構HEAP->堆段HEAP_SEGMENT->堆塊HEAP_ENTRY遍歷得知

堆的調試支持
--------------------------------------------
堆尾檢查(HTC): 堆塊的未尾附加額外的標記信息(通常為8字節),用於檢查堆塊是否溢出,與棧類似
釋放檢查(HFC): 釋放堆塊時進行檢查,避免錯誤釋放堆塊或重覆釋放
調用時驗證(HVC): 每次調用堆函數時都對整個堆進行驗證及檢查
用戶態棧回溯(UST): 將每次調用堆函數的函數調用信息(CALLING STACK)都記錄在一個數據庫中
調試的頁堆(DPH): 頁堆

創建堆時,堆管理器根據當前進程的全局標誌決定是否啟用堆調試功能
注册表中的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 表鍵下尋找以程序名命名的子鍵,如果存在子鍵就讀取下面的GlobalFlag鍵值

Windbg 打開進程後,輸入!gflag 取得相關信息

釋放檢查(釋放時檢測)
---------------------------------------------.
未啟用HFC:
多次釋放時: 即使多次釋放 , 不調用HeapValidate函數 系統未會即時發現錯誤

啟用HFC
多次釋放時: 即使多次釋放 , 不調用HeapValidate函數 系統會即時發現錯誤
如果在調試器運行程序,在釋放錯誤時,會中斷到調試器,原因是RtlFreeHeap的調用路徑上中RtlpBreakPointHeap會檢查進程是否處於被調試狀態,如果不在被調試狀態,就不會發起斷點異常

用戶態棧回溯(記錄式)
----------------------------------------------
如果當前進程全局標誌包含了UST標誌(0X1000), 那堆管理器會為當前進程分配一塊大的內存區,及建立STACK_TRACE_DATABASE結構管理該內存區,然後使用ntdll!RtlpStackTraceDataBase指向結構,它稱為用戶態棧回溯數據庫(User-Mode Stack Trace Database)

結構如下:
typedef struct _STACK_TRACE_DATABASE
{
  union
 {
   CHAR Resource[56];
   PVOID Lock;   //同步對象
 };
  VOID *AcquireLockRoutine;  //ntdll!RtlEnterCriticalSection+0
  VOID *ReleaseLockRoutine;  //ntdll!RtlLeaveCriticalSection+0
  VOID *OkayToLockRoutine;  //ntdll!NtdllOkayToLockRoutin+0
  UCHAR PreCommitted;  //數據庫提交標誌
  UCHAR DumpInProgress;  //轉儲標誌
  PVOID CommitBase;   //數據庫的基地址
  PVOID CurrentLowerCommitLimit; 
  PVOID CurrentUpperCommitLimit;
  CHAR * NextFreeLowerMemory; //下一個空閒位置低地址
  CHAR * NextFreeUpperMemory; //下一個空閒位置高地址
  ULONG NumberOfEntriesLookedUp; 
  ULONG NumberOfEntriesAdded; //已加入的表項數
  PRTL_STACK_TRACE_ENTRY * EntryIndexArray;
  ULONG NumberOfEntriesDeleted;
  ULONG NumberOfEntriesAvailable;
  ULONG NumberOfEntriesReused;
  ULONG NumberOfAllocationFailures;
  PRTL_STACK_TRACE_ENTRY FreeLists[32];
  ULONG NumberOfBuckets;
  PRTL_STACK_TRACE_ENTRY Buckets[1]; //以下詳解
} STACK_TRACE_DATABASE, *PSTACK_TRACE_DATABASE;

Buckets是個指針數組, 使用哈希鏈表算法記錄每一個記錄
1: 首先計算記錄的hash
2: 再把hash%numberOfBuckets
3: 得到的值為記錄在數組的位置
4: 一個桶位的多個記錄是以鏈表方式存放

每個記錄屬於RTL_STACK_TRACE_ENTRY結構, 結構如下:
typedef struct _RTL_STACK_TRACE_ENTRY
{
   union
   {
     PRTL_STACK_TRACE_ENTRY HashChain; //同一個桶中的下一個記錄節點
     PRTL_STACK_TRACE_ENTRY FreeChain;
    };
   ULONG TraceCount;    //本回溯發生次數
   WORD Index;    //記錄的索引號
   WORD Depth;    //棧回溯的深度
   VOID * BackTrace[32];   ///從棧幀得到的函數返回地址數組
} RTL_STACK_TRACE_ENTRY, *PRTL_STACK_TRACE_ENTRY;
建立好UST數據庫後,當堆塊分配函數被調用時,堆管理器便會將當前的棧回溯信息記錄到UST數據庫中
過程如下:
1: 堆分配函數調用RtlLogStackBackTrace發起記錄請求
2: 判斷ntdll!RtlpStackTraceDataBase指針是否為NULL,不為NULL則調用RtlCaptureStackBackTrace
3: RtlCaptureStackBackTrace調用RtlWalkFrameChain遍歷各個棧幀並將每個棧幀的函數返回地以數組形式返回
4: RtlLogStackBackTrace將得到的信息放入RTL_STACK_TRACE_ENTRY中,然後根據新數據的哈希值搜索是否已記錄過這樣的回溯記錄,如果搜索到->返回索引值,如果沒找到->調用RtlpExtendStackTraceDataBase將新記錄加入數據庫,返回索引值
5: 堆分配函數RtlDebugAllocateHeap將RtlLogStackBackTrace函數返回的索引號放入堆塊未尾一個名為HEAP_ENTRY_EXTRA的數據結構中,這個結構是分配堆塊時就分配好的,長度為8字節:2字節為ust索引號, 2字節為堆塊標記號,4字節為用戶設置的數值

*注册表中的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\heapmfc.exe表鍵下直接修改stackTraceDatabaseSizeInMb修改UST數據庫的大小


堆溢出
-----------------------------------------------
- 每個堆有很多段,每個段有很多堆塊,每個堆塊有兩部份一部份為真正可用空間,一部份為控制數據,透過刪改控制數據,如下一個堆塊節點,指向自定義的堆塊可以強制插入自定義的堆塊

其中一種攻擊方式:
1:定義字符串(它會存放到堆)並獲取該地址
2:分配兩個堆塊
3:把後者Free掉(因為對於下次分配,系統會先遍歷空閒堆塊鏈表)
4:使用第一個堆塊對第二個閒置堆塊進行溢出攻擊(攻擊部份為定義字符串的地址)
5:分配第二,三個堆塊(系統遍歷空閒鏈表,把剛剛free掉的堆塊重新分配起來,然後在下次再分配的時侯如free掉的堆塊的flink地址為可用,就會被當作成功分配)
6:當對第三個堆塊內容寫入時,實際上是對第一步定義的字符串的堆進行寫入,達到目的

調用時驗證(HVC)(每次調用堆函數都進行檢測,不作添加額外標誌)
-----------------------------------------------
假設:我們使用gflags啟用HVC, 發生堆溢出時,我們再使用HeapAlloc函數由於他會觸發驗證功能(因為啟用了hvc)
如釋放類似,他用調用RtlpValidateHeap來進程驗證,發現堆塊前後信息進程檢查,如一些預設好的校驗值是否被修改等操作,假如空閒堆塊被改為
最終導致,堆分配失敗/中斷到調試器

堆尾檢查(HTC) (為堆塊添加額外標誌,每次分配或釋放時進行檢測,檢測是否溢出)
------------------------------------------------
ntdll!CheckHeapFillPattern 為 abababab
一但啟用堆尾檢查,那堆管理器在分配堆塊時會附加8字節的CheckHeapFillPattern, (如果要觸發檢測,就要啟用HFC及HPC)
在下次分配或釋放時會調用RtlpValidateHeapEntry查證是否校驗堆的一致性

頁堆(完全HPC) (立即知道堆溢出)
------------------------------------------------
重點:
1. 頁堆有別於普通的堆, 頁堆的堆塊至少占用兩個內存頁
2. 一個內存頁是存放用戶數據,另一個是存放檢測頁,後者專門用來檢測溢出,像棧溢出的檢測類似,但頁面屬性為PAGE_NOACCESS
3. 每次分配頁堆是,堆管理器還會分配一個正常堆

根頁堆由以下部份組成(類似於HEAP):
1. DPH_HEAP_ROOT結構
2. DPH_HEAP_BLOCK節點結構(一段空間存放堆塊節點)
3. 同步用的關鍵區(Critical Section)對象
4. 滿足分配粒度填充為0

頁堆堆塊由以下部份組成:
1: 空閒數據,由於頁堆要保證用戶數據區是在堆塊中的最後位置,因此會填充為0
2: 固定長度的DPH_BLOCK_INFORMATION結構,大小為32字節
3: 用戶數據區(可用區域) 如分配空間正好是分配粒度的倍數比如16字節,那第三部份就不存在
4: 堆塊尾填充,填充為D0
5: 不可訪問頁(用於檢測溢出)

DPH_BLOCK_INFORMATION結構如下:
typedef struct _DPH_BLOCK_INFORMATION
{
ULONG StartStamp;   //起始簽名
PVOID Heap;   //堆的根結構地址
ULONG RequestedSize;  //請求大小
ULONG ActualSize;   //實際大小 
union
{
LIST_ENTRY FreeQueue;   //釋放後的鏈表結構
SINGLE_LIST_ENTRY FreePushList;
WORD TraceIndex;   //序號
};
PVOID StackTrace;    //分配過程的棧回溯
ULONG EndStamp;    //結束簽名
} DPH_BLOCK_INFORMATION, *PDPH_BLOCK_INFORMATION; 
總結:
尋找一個頁堆塊可透過遍歷根頁堆->DPH_HEAP_BLOCK中找到



准頁堆(HPC)
------------------------------------------------
由於頁堆的內存消耗太大
基本上跟頁堆大致相同,但在分配堆塊時是在頁堆附屬的正常堆中分配, 而且頁堆主結構中不再為節點分配節點結構
結構首先是HEAP_ENTRY,DPH_BLOCK_INFORMATION,用戶數據區,檢測字節(8/16BYTE)固定為0xA0,補位字節為滿足分配粒度,HEAP_ENTRY_EXTRA結構(如啟用了UST)



CRT堆
------------------------------------------------
分為三種模式:
1. SBH模式
2. 舊SBH模式
3. 系統模式(System Heap)

CRT堆對於前兩種模式:
CRT堆會直接使用虛擬內存分配api從內存管理器批發大的內存塊,然後分割成小的堆塊滿足應用程序的需要

系統模式:
CRT堆會把WIN32堆進行一次封裝及附加功能


CRT分配函數:
malloc
calloc

系統模式下會直接調用HeapAlloc

CRT堆在所有模式下都會調用_heap_alloc(調試版本為_heap_alloc_base)
_heap_alloc會根據CRT堆的正作模式來調用相應的分配函數->Win32/SBH分配函數
但不論是哪一種,都需要對原來的CRT堆塊進行多一層封裝
如WIN32:
HEAP_ENTRY+CRT調試堆塊管理結構(_CrtMemBlockHeade結構)+用戶數據(可用空間)+CRT調試堆塊檢測字節+補位字節+WIN32堆塊檢測字節+WIN32的HEAP_ENTRY_EXTRA


CRT調試堆塊管理結構如下:
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext; //下一個CRT堆塊
struct _CrtMemBlockHeader * pBlockHeaderPrev; //前一個CRT堆塊
char *     pszFileName;    //源文件名
int        nLine;     //源代碼行號
size_t     nUserRequestdDataSize;  //用戶區字節數
int        nBlockUse;     //塊的類型
long       lRequest;    //塊序號
unsigned char gap[nNoMansLandSIze];  //不可著陸區(防止其他堆塊溢出被修改)
}
CRT初始化時,會初始化CRT堆(調用_heap_init()),他的運作如下:
1. 調用heapCreate創建普通win32堆
2. 調用_heap_select選擇使用三種模式之一,並記錄結果到__active_heap
3. 如果是SBH/舊SBH 則調用__sbh_heap_init/__old_sbh_new_region來初始化SBH

_heap_select依賴以下3點
1: 操作系統版本號如果是WIN2000或更高則返回__SYSTEM_HEAP(1)
2: __MSVCRT_HEAP_SELECT和__GLOBAL_HEAP_SELECTED 有設置便返回設置
3: _GetLinkerVersion 鏈接器版本號

#define pdData(pblock) ((unsigned char*)((_CrtMemBlockHeader*)pblock+1))// 獲取CRT堆塊用戶數據區
#define pHdr(pdData) (((_CrtMemBlockHeader *)pdData)-1)   // 獲取CRT堆塊管理結構


參考:Windows軟件調試

Comments

Popular posts from this blog

Android Kernel Development - Kernel compilation and Hello World

How does Nested-Virtualization works?

Understanding ACPI and Device Tree