前面很多注入相關的文章中都提到為了保證注入後原始程式能恢複正常的執行流,需要在編譯器中關閉堆棧檢查。為了解決問題,這是個好手段,但是不得不說這是回避問題,不是根本上解決問題。本文旨在解決這個問題。
vs用 __chkesp來實作堆棧檢查。__chkesp顧名思義,檢查esp的值,檢查失敗就抱錯。什麼時候esp會出錯?情況很多,如果排除緩沖區溢出的可能,那還有堆棧失衡的情況。比如不正常的退出函數方式。我經常遇到的不正常退出函數引發_chkesp失敗一般都發生在修改堆棧上儲存的傳回位址,使之指向某個裸函數,然後從裸函數執行退出(或者跳轉)之後。
要繞過__chkesp檢查,首先要知道怎樣的函數調用方式會引起堆棧檢查。調用Windows提供的API後編譯都會安排一段__chkesp,另外在"直接調用位址"傳回後,也會被插入這段代碼。那麼什麼是"直接調用位址"?來看一段代碼:
typedef int (*funcAddAddress)(int,int);
int add(int a,int b)
{
return a+b;
}
int _tmain(int argc, _TCHAR* argv[])
{
funcAddAddress funcAddAddressPtr = (funcAddAddress)add;
add(1,1); //(1)
printf("======\n");
(*funcAddAddressPtr)(1,1); //(2)
return 0;
}
對同一個函數進行調用,(1)是普通調用方式,不會産生__chkesp,而(2)是我所謂的"直接調用位址"方式,函數傳回後會被插入__chkesp。下面用反彙編代碼驗證一下這個說法:
funcAddAddress funcAddAddressPtr = (funcAddAddress)add;
013513EE mov dword ptr [funcAddAddressPtr],offset add (1351091h)
add(1,1);
013513F5 push 1
013513F7 push 1
013513F9 call add (1351091h)
013513FE add esp,8
printf("======\n");
01351401 mov esi,esp
01351403 push offset string "======\n" (135573Ch)
01351408 call dword ptr [__imp__printf (13582B0h)]
0135140E add esp,4
01351411 cmp esi,esp
01351413 call @ILT+310(__RTC_CheckEsp) (135113Bh)
(*funcAddAddressPtr)(1,1);
01351418 mov esi,esp
0135141A push 1
0135141C push 1
0135141E call dword ptr [funcAddAddressPtr]
01351421 add esp,8
01351424 cmp esi,esp
01351426 call @ILT+310(__RTC_CheckEsp) (135113Bh)
return 0;
0135142B xor eax,eax
此處補發一個相關的pdf:
産生__chkesp的函數調用方式
既然知道了編譯器會在什麼時候插入__chkesp代碼,接下來進入本文的正題,__chkesp檢查失敗和繞過__chkesp堆棧檢查。
先看下__chkesp檢查失敗的情況(反面教材):
#include "stdafx.h"
unsigned int retAddress;
void Test();
void NormalFunc()
{
//data[1]: ebp的值; data[4] :函數傳回位址
unsigned int data[1] = {0x0};
unsigned int* ptr = data;
ptr+=3;
//儲存傳回位址
retAddress = *ptr;
*ptr = (unsigned int)Test;
return;
}
void Test()
{
//跳回到main函數體中!
__asm
{
lea eax,[ebp+0x04];
mov eax,[eax]
mov retAddress,eax;
push retAddress;
ret
}
}
typedef void (*DirectCallFunc)();
int main()
{
DirectCallFunc dirCallFunc = NormalFunc;
(*dirCallFunc)();
getchar();
return 0;
}
函數調用前ESI/ESP的值:

函數調用後ESI/ESP的值:
很明顯,因為esi!=esp是以引起__chkesp檢查失敗。接着看看引起失敗的原因:
函數調用前,編譯器儲存了目前棧指針:
(*dirCallFunc)();
00411435 mov esi,esp
00411437 call dword ptr [dirCallFunc]
0041143A cmp esi,esp
0041143C call @ILT+300(__RTC_CheckEsp) (411131h)
getchar();
并在函數中通過PROLOG/EPILOG生成/恢複函數幀架構:
void Test()
{
00411A90 push ebp
00411A91 mov ebp,esp
00411A93 sub esp,0C0h
00411A99 push ebx
00411A9A push esi
00411A9B push edi
00411A9C lea edi,[ebp-0C0h]
00411AA2 mov ecx,30h
00411AA7 mov eax,0CCCCCCCCh
00411AAC rep stos dword ptr es:[edi]
}
00411ABF pop edi
00411AC0 pop esi
00411AC1 pop ebx
00411AC2 add esp,0C0h
00411AC8 cmp ebp,esp
00411ACA call @ILT+300(__RTC_CheckEsp) (411131h)
00411ACF mov esp,ebp
00411AD1 pop ebp
00411AD2 ret
原本main調用NormalFunc,NormalFunc有編譯器生成函數調用架構并在執行完畢後順利的傳回到main函數中,此時堆棧平衡通過__chkesp檢查。但是由于NormalFunc的傳回位址被修改成Test中,節外生枝的跳轉到Test中執行。如果Test函數正常終止,那麼函數調用前後将還是堆棧平衡的,進一步說就是函數調用完成後esp==函數調用前esi的值==函數調用前esp的值,是以程式可以毫無懸念的通過了__chkesp檢查。然而,這裡的Test并不是這樣的普通青年,他隻正常的走過了PROLOG代碼開辟新堆棧,此時esp已發生了變動,然後他2B的通過push/ret的方式,從函數中間越過EPILOG恢複堆棧的代碼傳回到main函數中(也隻能通過這種方式傳回到main函數中)。是以函數調用完成後esp!=函數調用前esi的值==函數調用前esp的值,在__chkesp關卡上被截住了~
現在找到失敗的原因了,那找出相應的解決方法也不難:原本Test由編譯器自動生成函數棧,大不了取消編譯器做這個步驟就行了。這樣雖然進入了Test卻不額外生成函數幀,Test函數就像main函數的一部分似得,另外由于push/ret是一段esp自平衡的操作,是以堆棧還是維持NormalFunc結束時的樣子。
__declspec(naked) void Test()
{
//跳回到main函數體中!
__asm
{
push retAddress;
ret
}
}
一個什麼都沒幹的裸函數(想幹啥自己補全吧),程式仍然從函數中間退出,不過至少能通過__chkesp檢查。
裸函數是簡單粗暴的解決方式,但是裸函數内部不能定義局部變量,完全不好用。這就得提出新的改變方案:1.函數得正常開辟調用堆棧,可以正常使用變量;2.函數不經過編譯器生成的EPILOG的洗禮,從函數中間傳回main函數;3講了這麼多,最重要的,必須能通過_chkesp的檢查,否則,并沒有什麼卵用~
仔細想想,函數不過是越過EPLLOG代碼,然後傳回main函數是以才出錯的麼?大不了在傳回前參考vs的代碼自己來做EPILOG,來抵消進入Test時PROLOG的影響不就行了?
void Test()
{
__asm
{
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
}
//跳回到main函數體中!
__asm
{
push retAddress;
ret
}
}
來看下程式調用前後的結果: