一個使用錯誤 calling convention 卻沒有 crash 的故事⋯
使用錯的 calling convention 進行 function call 往往會在 callee function return 後讓 caller 的 stack pointer 指向錯誤的位址,此時 caller 再透過 stack pointer 去讀寫資料就會產生錯誤;可能讀到錯誤值、寫到錯誤位置,若是使用的資料型別是 pointer 時,更是容易使用到非法位址,引發 access violation 。從高階語言來看,這類的資料讀取往往透過 local variables 使用, return statement 去產生。
但如果在 callee 返回後,caller 都沒有透過 stack pointer 去存取資料,是不是可以神不知、鬼不覺地隱藏這個錯誤呢?正甚者,當 caller function 返回時,stack pointer 還能被還原到正確值呢?
回顧一下在 x86 Windows 系統上常見的 calling convention ,在沒有進行 FPO 最佳化時大都帶有 function prologue 和 epilogue ,像是下面 callModuleFunc() 為例,它的 prologue 和 epilogue 如下:
| // main.exe 使用 module.dll 的程式碼 |
| 0:000> uf main!callModuleFunc |
| main!callModuleFunc [...\code\main\mainentry.cpp @ 12]: |
| |
| 12 001f1040 55 push ebp |
| 12 001f1041 8bec mov ebp,esp |
| 12 001f1043 83ec08 sub esp,8 |
| |
| main!callModuleFunc+0x72 [...\code\main\mainentry.cpp @ 30]: |
| |
| 30 001f10b2 8be5 mov esp,ebp |
| 30 001f10b4 5d pop ebp |
| 30 001f10b5 c3 ret |
prologue 將 stack pointer 暫存到 ebp (L.6),而 epilogue 會把 ebp 寫回 esp (L.11),透過暫放在 ebp 的方式可以達到 esp 平衡。同時也解答了我們剛剛的問題:在特定條件下,esp 可以透過 ebp 還原,以此讓 caller 有機會從 mismatched calling convention 問題中全身而退的。
那實務我們怎麼從上 C++ 實現這樣的場景呢?回想一下剛剛的描述,我們需要做到:
- esp 暫存在某處,並在 mismatched calling convention 發生後有機會從該暫存處讀回
- 發生 mismatched calling convention 後不再依賴 esp 去讀取資料
有了這個想法,一個簡單的 sample code 就可以架構出來了:
- main.exe 動態載入 module.dll ,並且從中獲取 moduleFunc() 的位置
- moduleFunc() 是 __stdcall ,但在 main.exe 中我們故意以 __cdecl 方式呼叫
- main.exe 透過 callModuleFunc() 去完成上面的動作,而且故意將 FPO 關閉
- mismatched calling convention 發生後不進行 local variables 讀取,避免可能的 esp 使用
- 試試看一個額外的 bonus: 在 mismatched calling convention 發生後進行一個正確的 FreeLibrary()
實現上述需求的程式碼如下:
| #pragma optimize( "", off ) |
| void callModuleFunc( int v ) |
| { |
| HMODULE hModule{ ::LoadLibraryW( L"module.dll" ) }; |
| if ( nullptr == hModule ) { |
| printf( "LoadLibrary() failed: 0x%08x\n", ::GetLastError() ); |
| return; |
| } |
| |
| using ModuleFunc = int (*)( int ); |
| auto fpModuleFunc = reinterpret_cast( |
| ::GetProcAddress( hModule, "moduleFunc" ) ); |
| if ( nullptr == fpModuleFunc ) { |
| printf( "GetProcAddress() failed: 0x%08x\n", ::GetLastError() ); |
| } |
| else { |
| fpModuleFunc( v ); |
| } |
| |
| ::FreeLibrary( hModule ); |
| } |
| #pragma optimize( "", on ) |
開始在 Visual C++ 的實踐看看,但很快就發現只有在 release build 中才能逃離 runtime error 魔掌,在 debug build 則會有如下的錯誤視窗彈出。
錯誤內容很好理解,就是 esp 檢查出錯。Visual C++ debug build 中會開啟許多安全檢查,其中
Run-Time Error Checks 的 stack frame 預設開啟 ,反組譯一下,可以看到 L.9 處 VC 插入的代碼 RTC_CheckEsp() 會對 esp 進行檢查,該行在 mov esp,ebp 前,也就因此沒有機會將 ebp 寫回 esp 了。
| // Visual C++ 的 stack frame check |
| 0:000> uf main!callModuleFunc |
| main!callModuleFunc+0xc0 [...\code\main\mainentry.cpp @ 30]: |
| 30 00dd17d0 5f pop edi |
| 30 00dd17d1 5e pop esi |
| 30 00dd17d2 5b pop ebx |
| 30 00dd17d3 81c4d8000000 add esp,0D8h |
| 30 00dd17d9 3bec cmp ebp,esp |
| 30 00dd17db e84cf9ffff call main!ILT+295(__RTC_CheckEsp) (00dd112c) |
| 30 00dd17e0 8be5 mov esp,ebp |
| 30 00dd17e2 5d pop ebp |
| 30 00dd17e3 c3 ret |
不要勾選與 stack frame 相關的選項便可使用 Debug build 驗證這次的實驗。
完整的代碼可以在 github 上找到,git clone 後請往 CrashTonightOh/HiddenMismatchedCallingConvention 資料夾走去,不要忘了透過 debugger 觀察一下 esp 。
git clone https://github.com/WCChou/CrashTonightOh.git
留言
張貼留言