天天看點

繞過__chkesp堆棧檢查

    前面很多注入相關的文章中都提到為了保證注入後原始程式能恢複正常的執行流,需要在編譯器中關閉堆棧檢查。為了解決問題,這是個好手段,但是不得不說這是回避問題,不是根本上解決問題。本文旨在解決這個問題。

   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的值:

繞過__chkesp堆棧檢查

函數調用後ESI/ESP的值:

繞過__chkesp堆棧檢查

很明顯,因為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
        }
}      

來看下程式調用前後的結果:

繞過__chkesp堆棧檢查

繼續閱讀