軟件調試(10) - 棧和函數調用

用戶態及內核態棧
--------------------------------------------
TSS會記錄了不同優先級所使用的棧的基本信息
TSS+0x4 到 TSS+0x28的24字節是用來記錄棧的段信息和棧指針ss:esp

每個Win32線程都有兩個棧:
一是用戶態棧,記錄在_TEB->_NT_TIB
一是內核態棧,記錄在_KTHREAD

他們記著線程棧的詳細資料

用戶態棧- 1MB
x86內核態棧-12KB
x64內核態棧-24KB

考慮到gui線程在調用gdi服務時通常需要更大的棧段空間,所以在一個線程被轉為GUI線程後,內核態棧一般會被替換成一個更新的新棧
--------------------------------------------
內核棧的創建:
PspCreateThread是Windows內核態用於創建線程的一個重要內部函數
不論是PspCreateSystemThread或NtCreateThread

由PspCreateThread創建內核態棧,大小總是默認大小的內核態棧
但Windows線程在第一次調用Win32子系統服務時,將其轉變為GUI線程->轉化後系統的PsConvertToGuiThread函數會為該線程重新建立一個棧,然後使用KeSwitchKernelStack切換到新的棧,新的棧是可以改變大小的,稱為大內核態棧(Large Kernel Stack)

棧大小的最大值記錄在MmLargeStackSize


在一個線程被轉變為GUI線程 其KTHREAD結構的LargeStack會改為1
同時其Win32Thread字段會由0變為非0
棧段不會立即增大,在需要增大時調用MmGrowKernelStack函數來增長棧

-------------------------------------------------------
用戶態棧的創建
初始線程:
CreateProcess->NtCreateProcess->BaseCreateStack(調用NtCreateThread前)初始化用戶態棧

初始線程外:
CreateThread / CreateRemoteThread->BaseCreateStack(調用NtCreateThread前)初始化用戶態棧

PE中IMAGE_OPTIONAL_HEADER->SizeOfStackReserve/>SizeOfStackCommit代表初始線程保留及提交大小,當CreateThread為0時,系統也會使用他們來決定大小

在鏈接器時寫入

FramePointerOmission FPO
----------------------------------------------------------
在FPO下的程序,不會使用ebp作為棧幀指針,而轉用ESP


調用約定:

C調用約定 : 參數從右到左順序入棧, 調用者清棧, 關鍵字 __cdecl
標準調用約定: 參數從右到左順序入棧, 函數內清棧, 關鍵字 __stdcall
快速調用約定: 參數從右至左順序入棧,參數從ecx和edx來傳入前兩個(左起)長度不超過32位的參數, 關鍵字 __fastcall
this調用約定: this指針放入ECX傳給被調用方法,然後調用 函數內清棧 關鍵字__thiscall

x64調用約定:
一般只使用派生自fastcall的調用約定: 參數從右到左順序入棧,RCX,RDX,R8,R9傳參, 函數內清棧, 關鍵字 __stdcall

函數返回值:
-----------------------------------------------------------
如果EAX不夠裝入函數返回值,但它小於8字節 則將高位的傳入EDX


棧空間的自動增長:
-----------------------------------------------------------
VC為棧設置默認大小參數為1mb,初始提交為4kb(8kb,其中4kb為保護頁)

保護頁屬性為PAGE_GUARD,當訪問該頁時,cpu會立刻產生頁錯誤異常,並開始執行系統的內存管理函數
流程:
1: 先清除該頁面屬性
2: 調用MiCheckForUserStackOverflow系統函數, 它會從當前線程的TEB讀取用戶態棧的基本信息並檢查導致異常的地址
3: 系統先判斷是否有剩餘內存頁面可以被分配為保護頁面,如果有則分配(ZwAllocateVirtualMemory)
4: 原先的棧空間就增大了

*剩餘空間是指尚未提交到物理內存的空間,以上例子為1mb-4kb


棧溢出及棧保護機制:
-----------------------------------------------------------
當棧空間又再一次被用完,保護頁面再被訪問,重覆以上步驟

直到當棧保護頁面距離保留空間的最後一個頁面 只剩下一個頁面時(意味再提交到物理內存 就會用盡全部保留的棧空間)

此時MiCheckForUserStackOverflow函數會提交倒數第2個頁面,但不再設置PAGE_GUARD屬性,因為最後一個頁面永遠保留不可訪問

所以這時棧增長到它的最大極限,為了讓應用程序知道棧即將用完,函數會返回status_stack_overflow


棧下溢
----------------------------------------------------------
以下一個棧下溢例子
void main(){
  char sz[20]; 
  sz[2000]=0;
}
其異常匯編代碼:
StkUFlow!main+0x6:
00401006 c685b0c07000000 mov byte ptr [ebp+0x7bc],0x0

ebp+0x7bc = ebp-0x14+2000

而棧空間+2000已超越當前棧的下界 , 產生棧下溢

緩沖區溢出
------------------------------------------------------------
緩沖區是程序用來存儲數據結構的連續內存區域
一旦分配完成後,其基礎地址及大小便固定下來
當使用它時,超出了當時定下的區域,則稱為緩沖區溢出

情境假設: 緩沖區地址是分配在棧空間



調試版本的防止緩沖區溢出
------------------------------------------------------------
1: 分配局部變量時,自動多分配8byte用作屏障字段, 在填充他時這些會被填充為0xCC
2: 產生變量描述表,用來記錄局部變量的詳細信息
3: 函數返回前調用_RTC_CheckStackVars函數,根據變量描述表逐一檢查其中包含的每個變量,如果發現變量前後的0xCC發生變化,則報告檢查失敗

發布版本的防止緩沖區溢出
------------------------------------------------------------
基於Cookie:
Cookie值永遠保存在[ebp-0x4] 即函數第一個局部變量
而他會在函數剛進入和準備離開時進行校驗, 當他被修改,證明緩沖區溢出有可能已經漫延至參數,棧幀及返回地址等

檢查時系統使用_security_check_Cookie函數, 由於使用fastcall, 所以會把唯一參數以ecx傳入

原型為:
void __declspec(naked) __fastcall __security_check_cookie(_In_ uintptr_t _StackCookie);
實現:
void __declspec(naked) __fastcall __security_check_cookie(_In_ uintptr_t _StackCookie){
__asm{
cmp ecx, __security__Cookie
jne fail
rep ret

fail:
jmp __report_gsfailure
}
}

Comments

Popular posts from this blog

Android Kernel Development - Kernel compilation and Hello World

How does Nested-Virtualization works?

Understanding ACPI and Device Tree