x86對抗棧回溯檢測

1.原理
函數調用 CALL 指令可拆分爲兩步操作:
1)、
將調用者的下一條指令(EIP)的地址壓棧
2)、跳轉至將要調用的函數地址中(相對偏移或絕對地址)

那麼在執行到子函數首地址位置時,返回地址(即調用函數中調用位置下一條指令的地址)就已經存在於堆棧中了,並且是 ESP 指向地址的值。下面通過棧幀的概念,瞭解編譯器在接下來對堆棧進行的操作。
簡言之,棧幀就是利用
EBP(棧幀指針,請注意不是 ESP)寄存器訪問棧內部局部變量、參數、函數返回地址等的手段。程序運行中,ESP 寄存器的值隨時變化,訪問棧中函數的局部變量、參數時,若以 ESP 值爲基準編寫程序會十分困難,並且也很難使 CPU 引用到正確的地址。
所以,調用某函數時,先要把用作基準點(函數起始地址)的
ESP 值保存到 EBP,並維持在函數內部。這樣,無論 ESP 的值如何變化,以 EBP 的值爲基準能夠安全訪問到相關函數的局部變量、參數、返回地址,這就是 EBP 寄存器作爲棧幀指針的作用。

在函數體代碼的任何位置,EBP 寄存器指向的地址始終存儲屬於它的調用函數的 EBP 的值,根據這個原理可逐級向調用函數、調用函數的調用函數進行遍歷,向上回溯。

這樣有什麼用呢?在將屬於調用函數的 EBP 的值壓棧之前,ESP 指向的地址存儲的是由 CALL 指令壓棧的調用函數中調用位置的下一條指令的地址(原 EIP)。那麼根據這個邏輯,可以通過上面回溯的各級 EBP 的值,並根據 EBP+sizeof(ULONG_PTR) 獲取到函數調用者函數體中的地址(當前函數的返回地址)。有了每級調用的函數體中的地址,那麼獲取該函數的詳細信息及函數符號就變得容易了。

 

2.對抗思路
分配內存地址作爲基地址的內存空間,並將以當前
ESP 爲基地址的一段棧內存片段的數據拷貝到了新分配的內存空間的高內存區域中,修改 ESP EBP 寄存器的值爲新緩衝區中對應的兩個寄存器指針應該指向的位置,相當於對堆棧片段進行了平移。

平移時首先根據 ESP EBP 寄存器指向的內存地址定位需要拷貝的數據範圍。在這裏可能會向 EBP 指向的地址上面多拷貝一部分數據,以將參數和返回地址等數據一併拷貝到新分配的緩衝區中。拷貝完成之後,將 ESP EBP 寄存器指向新緩衝區中對應的位置。

這時開始程序對堆棧的操作將會在新分配的內存緩衝區中進行。在 ShellCode 代碼執行即將完成時,應會再將 ESP EBP 的值還原回原來真正棧裏的地址,避免彈棧時進入上面未知的內存區域導致程序異常。

 

3.驗證
 

爲了驗證這個判斷是否有效和真實,接下來需要實現上面猜想中描述的操作,看看調試器或檢測系統是否能夠成功地進行棧回溯。

下面的代碼片段實現了分配新的緩衝區,並拷貝從 ESP 指針指向位置到 調用函數的 EBP 在棧中存儲位置加上調用函數的返回地址的存儲位置這個範圍的棧片段,到新分配的緩衝區中最高位置區域,爲低內存預留了 0x100000 字節的空間。

void simplesubfunc() {
    printf("a simple sub function!\n");
}

void buildmystack() {
    ULONG_PTR stackbase, stacklimit;
    ULONG_PTR p_ebp, pp_ebp = 0, p_esp, delta;
    ULONG_PTR p_new_esp = 0, pp_delta;

    PVOID p_new_stack = NULL;

    __asm pushad;
    __asm pushfd;
    __asm push 0;
    __asm push 0;
    __asm push 0;
    __asm push 0;

    // 獲取棧的基本信息
    __asm mov   eax,        fs:[0x04] ; 取 StackBase 域的值
    __asm mov   stackbase,  eax       ;
    __asm mov   ebx,        fs:[0x08] ; 取 StackLimit 域的值
    __asm mov   stacklimit, ebx       ;
    __asm mov   p_ebp,      ebp       ;
    __asm mov   p_esp,      esp       ;

    stackbase -= 2 * sizeof(ULONG_PTR);
    delta = p_ebp - p_esp;

    // 獲取調用者的 EBP 在棧中的位置
    if (p_esp > stacklimit &&
        p_esp < stackbase  &&
        p_ebp > stacklimit &&
        p_ebp < stackbase) {
        pp_ebp = *(ULONG_PTR *)p_ebp;
    }

    // 搭建新的棧空間並移動棧指針
    if (pp_ebp > stacklimit &&
        pp_ebp < stackbase) {
        pp_delta = pp_ebp - p_esp;
        p_new_stack = malloc(pp_delta + 0x100000 + 2 * sizeof(ULONG_PTR));
        p_new_esp = (ULONG_PTR)p_new_stack + 0x100000;
        memcpy((PVOID)p_new_esp, (PVOID)p_esp, pp_delta + 2 * sizeof(ULONG_PTR));
        __asm mov   eax,   p_new_esp  ;
        __asm mov   esp,   eax        ;
        __asm mov   ebx,   eax        ;
        __asm add   eax,   delta      ; 計算當前 ebp 應指向的位置
        __asm mov   ebp,   eax        ;
        __asm add   ebx,   pp_delta   ;
        __asm mov   [eax], ebx        ; 修正調用者 ebp 在棧中位置
    }

    // 執行正式函數體代碼
    simplesubfunc();

    // 恢復棧指針到原棧中的位置並釋放內存
    if (p_new_stack) {
        __asm mov   esp,   p_esp      ;
        __asm mov   ebp,   p_ebp      ;
        __asm mov   eax,   ebp        ;
        __asm mov   ebx,   pp_ebp     ;
        __asm mov   [eax], ebx        ;
        free(p_new_stack);
    }

    __asm pop  eax;
    __asm pop  eax;
    __asm pop  eax;
    __asm pop  eax;
    __asm popfd;
    __asm popad;
}

void helloworld() {
    buildmystack();
    printf("hello world!\n");
}

int main(int argc, char* argv[]) {
    helloworld();
    return 0;
}

在函數 simplesubfunc() 處下斷點,用 windbg 啓動執行,命中斷點後通過 kv 指令觀察調用棧,發現調用序列中已經不能回溯到上級各層的調用了。

(5644.3e20): Break instruction exception - code 80000003 (first chance)
eax=016e40d0 ebx=012fe000 ecx=00000000 edx=000000e4 esi=013b1d40 edi=013b1d40
eip=013b1129 esp=016e3fec ebp=016e4038 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
*** WARNING: Unable to verify checksum for HookDemo.exe
HookDemo!simplesubfunc+0x9:
013b1129 cc              int     3
0:000> kv
ChildEBP RetAddr  Args to Child              
016e4038 00000000 00000206 013b1d40 013b1d40 HookDemo!simplesubfunc+0x9 (FPO: [Non-Fpo])
0:000> !teb
TEB at 01027000
    ExceptionList:        012ffdc8
    StackBase:            01300000
    StackLimit:           012fe000
    SubSystemTib:         00000000
    FiberData:            00001e00
    ArbitraryUserPointer: 00000000
    Self:                 01027000
    EnvironmentPointer:   00000000
    ClientId:             00005644 . 00003e20
    RpcHandle:            00000000
    Tls Storage:          0102702c
    PEB Address:          01024000
    LastErrorValue:       0
    LastStatusValue:      c0000139
    Count Owned Locks:    0
    HardErrorMode:        0

 對比 TEB StackBase StackLimit 域的值和命中斷點時 CPU 寄存器狀態中 ESP EBP 指向的值,發現 ESP EBP 已經不在線程棧的範圍中了。但是程序的向下執行並沒有受到任何影響:

a simple sub function!
hello world!
請按任意鍵繼續. . .

這就說明,這個判斷至少到目前爲止是正確的。

 

4.應對

棧回溯時以 TEB 的成員 StackBase StackLimit 的值作爲限制範圍,而棧頂和棧底指針一開始就不在範圍之中,那麼棧回溯循環過程會在遍歷第一個棧幀時就跳出遍歷。

那麼可不可以在棧回溯的時候,去掉通過這兩個成員的值進行的限制呢?

這樣考慮和推測,當然要想到任何一種可能出現的不正常的情況。ShellCode 中構造的新的棧片段中,最上級調用的棧區域可能並未賦給正確的值,包括原 EBP 或原 EIP 的值,比如這兩個域在 ShellCode 代碼中被臨時地給簡單地置爲 0x00000000 了。那麼放開 StackBase StackLimit 的限制而直接地通過調用序列向上回溯,如果未處理好的話,很可能會在檢測模塊中發生非法訪問等異常情況。

那麼如果對原 EBP 或原 EIP 判斷得好的話,比如對內存地址的有效性進行謹慎的判斷,那麼放開限制是否就可以了?

根據前面表達過的意思,你不能清楚地知道在 ShellCode 中對原 EBP 或原 EIP 的值改成什麼樣了,如果是非法的地址還算是比較好判斷的。但是如果是正常的屬於堆棧地址呢?這裏的正常的意思是,原 EBP 或原 EIP 的值確實是 EBP 或原 EIP 的值,但不是應該出現在這裏的,而是諸如應該出現在下級調用中的 EBP 或原 EIP 的值這樣的。如此一來,將會導致無限循環遍歷等問題。

要是樣本的 ShellCode 更進一步,竊取其他線程的堆棧部分數據覆蓋到自己構造的堆棧的高內存部分,那麼在調試器或檢測系統在棧回溯時,遍歷到上層的調用項,被誘導進入另一個線程的調用棧序列中,那麼獲取到的數據就可能已經不是當前線程的數據了。

 

5.說明
本文中的代碼片段在任意版本的
Visual Studio Visual C++ 中均可編譯通過,感興趣的可自行測試。未貼出完整代碼內容,需自行補充頭文件包含等。另外上面部分代碼在編譯的時候會報出 warning C4731 的警告,提示棧幀指針寄存器 ebp 被內聯彙編代碼修改。直接無視即可。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章