天天看點

繞過緩沖溢出防護系統

-介紹

近來一段時間,一些商業化的安全機構開始提出一些方案來解決緩沖區溢出問題。本文分析這些保護方案,并且介紹一些技術來繞過這些緩沖區溢出保護系統.

現在不少商業化組織創造了許多技術來防止緩沖區溢出。最近,最流行的一種技術是棧回溯技術,它是一種最容易實作的防溢出技術,但是同時它也是最容易被攻擊者繞過的一項技術.

值得一提的是,許多著名的商業化産品,比如Entercept和Okena就使用了這項技術。

2-棧回溯

現存的大多數商業安全體系實際上并不是防止緩沖區溢出,而是試圖檢測Shellcode的執行.

最普遍的檢測Shellcode的技術是檢查代碼頁的權限,通過檢測代碼是否在一個可寫的記憶體頁上執行來判斷是否執行了Shellcode.這種辦法是可行的,因為代碼段的記憶體屬性通常是不可寫的,并且X86體系不支援non-executable這種記憶體屬性位。

有些溢出防護體系也執行一些額外的檢查,用來判斷代碼的記憶體頁是否屬于一個PE檔案節區的記憶體映射,并且不屬于一個匿名的記憶體節區

[-----------------------------------------------------------]

page = get_page_from_addr( code_addr )

if (page->permissions & WRITABLE)

return BUFFER_OVERFLOW

ret = page_originates_from_file( page )

if (ret != TRUE)

return BUFFER_OVERFLOW

[-----------------------------------------------------------]

Pseudo code for code page permission checking

依賴棧回溯的緩沖區溢出保護技術(BOPT)并不真正建立一個不可執行的堆棧段,而是通過Hook作業系統調用來監測Shellcode的執行。

大多數的作業系統可以在使用者态或者核心态被Hook.

下面的章節我們講述如何逃避核心hook,再下一節講述如何繞過使用者态hook。

3-逃避核心态hook

當hook核心時,主機入侵保護系統(HIPS)必須能夠監測使用者态的API是被哪裡調用的。

由于kernel32.dll和ntdll.dll中的函數被大量的調用,一個API調用通常與真正的系統陷阱調用( syscall trap call)

相隔很多棧幀。是以,一些入侵防護系統依賴棧回溯來定位系統調用的原始調用者。

3.1-核心棧回溯

雖然棧回溯可以發生使用者态和核心态,但是相比使用者态元件而言,棧回溯技術對于緩沖區溢出保護技術的核心元件而言要重要得多.現有的商業BOPT的核心元件完全依賴棧回溯來檢測Shellcode的執行,是以逃避核心Hook可以簡化為使棧回溯機制失效。

棧回溯牽涉到曆遍棧幀,和把傳回位址傳遞給上層的緩沖溢出檢測程式來進行檢測。

通常情況下,有一個附加的"return into libc"檢查,包括檢查一個傳回位址是否指向Call或者Jmp的下一條指令。最基本的棧回溯操作的代碼(通常被用于BOPT),就像下面這個一樣

[-----------------------------------------------------------]

while (is_valid_frame_pointer( ebp )) {

ret_addr = get_ret_addr( ebp )

if (check_code_page(ret_addr) == BUFFER_OVERFLOW)

return BUFFER_OVERFLOW

if (does_not_follow_call_or_jmp_opcode(ret_addr))

return BUFFER_OVERFLOW

ebp = get_next_frame( ebp )

}

[-----------------------------------------------------------]

Pseudo code for BOPT stack backtracing

當讨論如何逃避棧回溯,最要弄明白的一點就是棧回溯如何在X86體系上工作的,當調用一個函數時,一個典型的棧幀看起來就像下面這個一樣:

: :

|-------------------------|

| function B parameter #2 |

|-------------------------|

| function B parameter #1 |

|-------------------------|

| return EIP address |

|-------------------------|

| saved EBP |

|=========================|

| function A parameter #2 |

|-------------------------|

| function A parameter #1 |

|-------------------------|

| return EIP address |

|-------------------------|

| saved EBP |

|-------------------------|

EBP寄存器指向下一個棧幀。沒有EBP寄存器,就會非常困難,或者根本不可能正确地辨別和追蹤所有的棧幀

現代編譯器通常不使用EBP作為棧幀指針,而是作為一個通用寄存器,經過EBP優化,一個棧幀看起來如下

|-----------------------|

| function parameter #2 |

|-----------------------|

| function parameter #1 |

|-----------------------|

| return EIP address |

|-----------------------|

注意EBP寄存器并沒有出現在棧中,沒有EBP寄存器,緩沖區溢出檢測技術就沒法準确地實施棧回溯.這樣一來,他們就很難進行檢測,一個簡單的"return into libc "型的攻擊就可以繞過保護。

簡單的調用比BOPT hook住的API更底層的API可以使這種檢測技術失效。

3.2-僞造棧幀

由于棧是在Shellcode的完全控制之下的,是以可以在API調用之前徹底更改它的内容,專門改造過的棧幀可以被用來繞過緩沖溢出的監測.

如同前面解釋的那樣,緩沖溢出檢測工具尋找格法代碼的3個關鍵标志:隻讀的頁屬性,記憶體映射檔案節區和指向call,jmp後面的指令的傳回位址.Since function pointers change

calling semantics, BOPT do not (and cannot) check that a call or jmp

actually points to the API being called. Most importantly, the BOPT cannot

check return addresses beyond the last valid EBP frame pointer

(it cannot stack backtrace any further).

是以,逃避BOPT可以通過建立一個帶有有效傳回位址的最終(final)棧幀,這個傳回位址必須指向一條駐留在隻讀記憶體頁上的來自于記憶體映射檔案節的,并且緊跟在一條call或者jmp指令之後的指令.假設僞造的傳回位址合理的接近于第2個傳回位址,Shellcodeo可以輕松地再次獲得控制權

為了指向僞造的傳回位址,理想的指令順序是

[-----------------------------------------------------------]

jmp [eax] or call [eax], or another register

dummy_return: ... some number of nops or easily

reversed instructions, e.g. inc eax

ret any return will do, e.g. ret 8

[-----------------------------------------------------------]

繞過核心BOPT元件很容易,因為它們必須依賴使用者控制的資料(棧)來檢驗API調用的有效性,通過正确地操作棧,可以有效地結束站傳回位址的分析

這種棧回溯逃避技術也影響使用者态的hooks

4.逃避使用者态hooks

假使在一個有效的記憶體區域内出現了一系列正确的指令,那麼繞過核心緩沖溢出保護是可能的.相似的技術可以被用來繞過使用者态的BOPT元件,更加的是,由于Shellcode運作的時候擁有和使用者态Hook相同的權限,我們還能采取其他不少技術來逃避BOPT的監測.

4.1-實際問題-不完全的API Hooking

使用者态緩沖溢出防護技術存在很多問題.例如攻擊者會選擇很多不同的辦法來實作他們的目的,而溢出保護系統可能隻能檢測其中的一部分.

嘗試預先判斷一個攻擊者會如何構造它的Shellcode是非常困難的,甚至是不可能的,要選擇一種好的辦法并不容易,有很多障礙橫在你面前,如

a.沒有同時考慮到API調用的UNICODE和ANSI版本

b.沒有考慮到API的鍊狀調用關系,比如很多kernel32.dll中的函數僅僅是把ntdll.dll中的函數微微包裝

c.Microsoft Windows API的經常性的更改

4.1-沒有Hook API的所有版本

實行使用者态Hooking時,一種最常遇到的失誤就是沒有完全覆寫代碼路徑.為了阻止惡意代碼,所有攻擊者可以利用的API都必須被Hook.這就要求溢出保護系統Hook住攻擊者必須用到的代碼,然而,就像我們下面要揭示的那樣,一旦一個攻擊者已經開始執行他的代碼,第3方保護系統就很難再掌握他所有的行動,事實上,我們已經測試過的商業保護系統中,沒有一個能有效覆寫攻擊者的代碼路徑

很多Windows的API有2個不同的版本, ANSI和UNICODE。ANSI函數通常以A結尾,而UNICODE以W結尾.ANSI的函數一般僅僅是UNICODE函數的簡單包裝,例如, CreateFileA是一個ANSI函數,傳遞給它的參數被它轉換成UNICODE字元串,然後它再調用CreateFileW.除非我們Hook住 API的ANSI和UNICODE2個版本,否則攻擊者可以通過調用API的另一個版本來繞過我們的保護機制

例如, Entercept4.1 Hook了LoadLibraryA,但是它忘記了Hook LoadlibraryW.如果一個保護系統僅僅Hook了API的一個版本,那它應該Hook UNICODE版本的API,在這一方面,Okenal/CSA做得比較好,它Hook了 LoadLibraryA,LoadlibraryW,LoadlibraryExA,LoadlibraryExW.不幸的是,對于第3方溢出保護系統來說,簡單的多Hook幾個kernel32.dll的函數并不夠.

4.1.2-沒有Hook得足夠深

在WindowsNT 中,kernel32.dll隻是Ntdll.dll得簡單包裝,但是大多數的溢出保護系統并沒有Hook ntdll.dll裡面的函數,這個錯誤和沒有Hook API的2個版本很相似,攻擊者可以直接調用ntdll.dll裡的函數來繞過保護系統在Kernel32.dll裡面設定的檢查.

例如,NAI Entercept嘗試檢測Shellcode調用kernel32.dll中的GetProcAddress(),然而,Shellcode可以被改寫成調用ntdll.dll的LdrGetProcedureAddress(),這可以達到相同的目的,并且繞過了檢測.

一般而言,shellcode可以完全直接避開使用者态的hook,通過調用系統調用來達到目的.(見4.5小節)

4.1.3-沒有充分地Hook-

各種不同的Win32 API之間的互相作用是非常複雜的,并且很難弄清楚,一不小心就會給入侵者留下一個進入的視窗.

例如,Okena/CSA和NAI entercept都hook了WinExec試圖防止攻擊者執行一個程序,WinExec調用順序如下

WinExec()-->CreateProcessA()-->CreateProcessInternalA()

Okena/CSA 和NAI entercept Hook了WinExec()和CreateProcessA(),(參見附錄A,但是,這2種系統都沒Hook CreateProcessInternalA()(被kernel32.dll導出),當編寫Shellcode時,攻擊者可以使用 CreateProcessInternalA()來替代WinExec().

在調用CreateProcessInternalA() 之前,CreateProcessA()壓入2個NULL進棧,是以Shellcode隻需要壓入2個Null進棧,然後直接調用 CreateProcessInteralA(),便可逃避這2種産品的使用者态Hook的監測.

當新的DLLs和APIs釋出時,Win32 API之間的互相作用更加複雜化了,這使問題更加複雜。第三方保護系統在實施他們的溢出保護技術的時候會很不利,一個小小的失誤就可能被攻擊者利用。

繼續閱讀