【技術分享】偽造面向對象編程——COOP
C和C++向來以“let the programmer do what he wants to do”的貼近底層而為廣大開發者所喜愛。語言對開發者行為的較少限制,就使其成為不安全的語言。針對C和C++程序的控制流劫持攻擊,如ROP、JOP等已經在長期的實踐中證明了其破壞力,而眾多保護措施也已經被提出。攻擊者要么是將控制流轉向其注入的惡意代碼,要么是借由代碼重用攻擊來惡意地重用進程空間中已有的代碼片段。無論是已有的攻擊,還是相應的防御措施,往往都沒有,或者是很少考慮C++的自身語言特性,包括其面向對象的特性。而現如今許許多多的應用程序都是通過C++開發,或者是包含部分C++代碼,如Microsoft Internet Explorer,Google Chrome, Mozilla Firefox, Adobe Reader, Microsoft Office, LibreOffice, 和openJDK等等。因此,針對C++語言特性的攻擊很可能造成巨大的破壞。接下來,我就為大家介紹一下針對C++特性的攻擊——偽造面向對象編程(COOP)。
偽造面向編程(counterfeit object-oriented programming ,以下簡稱COOP)是由Felix Schuster等人于2015年提出來的一種主要針對C++語言特性的攻擊方式。C++提供了面向對象的特性,如類、方法以及虛函數等。而COOP就利用了C++程序中的虛函數都要進行取地址操作(因為要維持一張虛函數表),而這就意味著有一個存在一個不變的指針來對應一個虛函數,同時也會使得C++程序中相比同樣體量的C程序中存在更多的進行取地址操作的函數。下面我具體給大家介紹一下。
前期知識
二進制層面的C++虛函數
對于每個至少包含一個虛函數的對象來說,在其內部偏移0處都會存在一個相應的指針,通常被稱為vptr。如下圖所示:

可以看到,類A不含虛函數,那么它的內存布局中就沒有vptr指針和虛函數表。類B則恰好相反,在其內部偏移0處存放有vptr指針,指向虛函數表。
而調用一個虛函數通常會對應類似以下的匯編指令:

rcx寄存器存儲著this指針,通過取內容將vptr指針存入rax寄存器,然后再通過相應的偏移量(此處是8,但有可能是其他數)讀取虛函數表,取出對應虛函數地址進行調用。
攻擊假設
COOP對攻擊者的能力做了如下假設:
- 攻擊者控制了一個包含虛函數的C++對象.
- 攻擊者能夠推斷出一個他已經知道內存布局(至少是部分知道)的C++模塊基址。
對于第一點來說,攻擊者只要利用鄰近該對象的某處溢出漏洞或者是use-after-free漏洞(正如后續某些攻擊時所提到的那樣)。
而對于第二點來說,一個公開的C++庫就能符合要求。
攻擊目標
COOP的設計初衷是為了達成以下目標:
(1)不出現已有代碼重用攻擊的特征,包括:
- 不會間接跳轉(通過call 或者是jmp指令)到沒有被取地址(被取地址的通常包括函數頭等)的內存位置。
- 不會不經調用棧(call stack)執行return操作。
- 控制流中不出現過多的間接跳轉。
- 不會劫持棧上指針。
- 不會注入新的或者是利用已有的代碼指針(返回地址或者是函數指針)。
(2)使控制流和數據流盡量與一般的C++代碼相似。
(3)能被廣泛地用于攻擊C++程序。
(4)在真實情景下實現圖靈完備(Turing complet,具體定義比較復雜,可以自行了解,簡單的說就是能夠實現條件分支,循環、讀寫等操作)。
攻擊實施步驟
為了更好地理解COOP攻擊模式,在介紹COOP具體攻擊實施步驟時,將會使用簡單的代碼來進行介紹,主要涉及以下幾個類:Student、Course和Exam。
(1)劫持C++對象
每一次COOP攻擊都要以劫持目標C++對象開始,稱為initial object。這一步是為了是控制流轉向下一步偽造的對象當中。比如可以劫持以下類Course:

students是一個指向數組的指針。其中的Student類定義如下:

(2)偽造對象
在上一步操作之后已經可以使控制流發生改變,接下來要做的就是偽造包含有攻擊者選定的vptr指針和一些數據域的對象。偽造的對象并不是目標程序自身有的,而是由攻擊者注入到程序進程空間中的。偽造的對象和必要的一些數據將會作為連續的一個內存塊(chunk)被注入到攻擊者控制的某片內存區域。注入偽造對象后的內存布局如下圖所示:

其中的object0和object1就是偽造的對象,而initial object(也就是上文提到的Course類)已經被劫持,students數組各成員已經指向相應的偽造對象,nStudents已經被設置為偽造對象的數量。接下來要做的就是通過各個vptr指針調用攻擊者選定的虛函數了。
(3)調用虛函數
通過initial object和偽造對象,相應的vptr指針和需要的數據已經準備好,接下來就是調用攻擊者選定的虛函數執行相應操作了。值得注意的是,由于COOP的目標之一是使控制流和數據流盡量與一般的C++代碼相似,所以各個vptr指針應該指向實際存在的虛函數表(理想情況下應該是虛函數表開頭)。與ROP中的gadgets類似,COOP中的目標虛函數被稱為vfgadgets,從其功能上來看,具體可以分為以下幾類:
- 主循環函數ML-G。
ML-G是包含有以指向偽造對象指針為循環變量的循環的虛函數,比如前述Course類中的?Course函數:

該函數會在循環過程中訪問每個students數組成員,而該數組已經指向偽造對象,也就是說,通過該循環能將控制流轉向偽造對象。
現實當中也有相應的例子,比如VS 2013 agents.h中:

- 算數或邏輯運算函數ARITH-G。
如Exam類中以下虛函數:

- 內存讀/寫函數W-G/R-G。
如涉及字符串讀寫等函數。如以下函數:

- 調用函數指針函數INV-G。
如以下函數:

- 條件寫函數W-COND-G。
- 帶有initial object的數據作為參數的主循環函數ML-ARG-G。

- 寫入第一個參數指針指向地址的函數W-SA-G。

- 只調整棧指針而無其他操作的函數MOVE-SP-G。
- 64位下讀取參數寄存器rdx,r8,r9函數LOAD-R64-G。
有了這些類型的vfgadgets,COOP的整個流程就比較清晰了:從initial object的vptr指針開始,調用第一個虛函數進入到主循環函數ML-G(如果選定的主循環函數有參數的話就是ML-ARG-G),然后循環地通過各個偽造對象的vptr指針去調用對應的由攻擊者選定的虛函數。整個流程如下圖所示:

其中各個數字代表執行順序。而為了實現不同的操作,就需要以上幾類vfgadgets的協同,這就是接下來要介紹的。
(4)不同操作的實現
正如前述所討論的那樣,通過主循環函數ML-G ,任意數目的虛函數可以被執行。而通過不同vfgadgets的配合,可以實現以下操作:
- 任意寫。
考慮如下Examl類:

其各個元素在內存中排布如下:

可以看到,SimpleString::set(object1)的buffer指針和Exam(object0)的score是重疊的。而buffer指針是字符串復制的目標地址,在此基礎上,通過操縱score的值,實際上buffer的值也就能夠控制,進而任意寫的地址任意性就達成了,接下來就要解決源數據(此處的字符串s)。而注意到此處的字符串是傳入的參數,那么接下來要解決的就是傳參問題了。以上在內存中構造重疊的對象是COOP很常用的手法。
- 傳遞參數。
參數傳遞和具體的系統有關,主要有:
(A)windows x64
windows x64平臺下函數的前四個參數由rcx,rdx,r8和r9四個寄存器來傳遞,更多的參數則通過棧來傳遞。特別的,對于C++程序來說,this指針通過rcx寄存器來傳遞。其他三個寄存器通常會作為臨時寄存器使用。考慮下列64位下讀取參數寄存器vfgadget:

其對應匯編指令如下:

相當于進行了如下操作:

因此,只要攻擊者合理地選取對應偏移量為10h和18h的數據,就能對應的改變相應寄存器的值,進而通過這些寄存器為某個目標函數傳參。
(B)Linux x64
Linux x64使用了6個寄存器來傳遞前六個參數,利用原理與windows x64是相同的,但因為有更多寄存器,所以更加簡單。
(C)windows x86
windows x86下,this指針通過ecx寄存器傳遞,其他參數通過棧來傳遞。這個時候就要以下列帶參數的主循環函數ML-ARG-G來傳遞參數。

其對應匯編指令為:

其中第9-22行進行了如下操作:

使用下列寫入第一個對參數指針指向地址的vfgadget,就可以實現對以上arg0的控制,進而執行傳參操作。

或者也可以借由內存寫vfgadget來進行。
(D)Linux x86
該平臺下的傳參均通過棧來進行,所以使用帶參數的主循環函數ML-ARG-G來傳遞參數。
以上均是只同時傳遞一個參數,要想同時傳遞多個參數,就有必要做棧平衡。下圖展示了不同參數個數的棧情形:

棧平衡操作可以使用MOVE-SP-G類型的vfgadget來完成,最終達到的效果就是參數被逐個堆積到棧上,如下圖所示:

- 調用API函數
要調用API函數,攻擊者可以直接將偽造對象的vptr指針指向已加載模塊的IAT表或者是EAT表,這二者均存有API函數信息。如果遇到權限問題,則可以用this指針作為參數調用VirtualProtect()函數設置即可。
又或者攻擊者可以使用調用函數指針的vfgadget——INV-G來調用API函數。
- 實現條件分支和循環
如果選定的變量沒有存儲在棧上,那么下列條件寫vfgadget——W-COND-G就能實現條件分支。

如果變量在棧上,MOVE-SP-G類型的vfgadget也可以被用作分支。
循環的實現也是類似的。
工具框架
為了完成COOP攻擊,就要完成以下兩個任務:
- 尋找目標vfgadgets
為了識別應用程序中有用的vfgadget,搜索時僅依賴于二進制代碼以及可選的調試符號。使用IDA將目標C++模塊反匯編,C ++模塊中的每個虛函數都被視為潛在的vfgadget。使用調試符號來靜態標識C ++模塊中的所有虛函數表。如果沒有該符號以標識虛函數表,就使用啟發式的方法:把所有帶有地址信息的函數指針數組作為一個潛在的虛函數表。檢查所有已識別的虛函數,通常只將具有一個或三個基本塊的虛函數作為潛在vfgadget。唯一的例外是ML-G和ML-ARG-G,由于帶有循環,它們通常由更多基本塊組成。
搜索時,每一個基本塊用靜態單賦值形式(static single assignment form,通常簡寫為SSA form或是SSA,是中間表示IR的特性,每個變數僅被賦值一次)進行總結以反映其I/O行為特征。依賴于METASM二進制代碼分析工具包的backtracking功能,在基本塊級別進行符號執行(symbolic execution)操作。接下來就是對潛在vfgadgets基本塊的SSA表示應用過濾器。例如 “賦值的左側不能引用任何參數寄存器;右側不能引用this指針”能識別64位系統下的內存寫vfgadget——W-G。
- 構造重疊的偽造對象
正如前述所說的,構造重疊的偽造對象對于COOP很重要。具體實現時,攻擊者定義偽造的對象和標簽。可以將標簽分配給偽造對象內的任何字節。當為不同對象中的字節分配相同的標簽時,應確保將這些字節映射到最終緩沖區中的相同位置,同時確保具有不同標簽的字節被映射到不同的位置。這些約束通常可以滿足,因為偽造對象中的實際數據通常很少。
比如,偽造的對象A僅僅在內部偏移+0處有vptr指針,偏移+16處有個整數,偏移+136處有標簽X。偽造對象B只有vptr指針,內部偏移+8處有相同標簽X。那么,B+8就可以映射到A+136處。
實際攻擊
COOP進行了以下幾個POC:
1.64位IE 10
攻擊64位IE 10使用了21個偽造對象,其中8個是重疊的。另外使用了mshtml.dll中8個不同的vfgadgets。攻擊有兩條執行路徑:a.彈出計算器和b.打開畫圖。以下是攻擊使用的vfgadgets:

2.32位IE 10
攻擊32位IE 10使用了22個偽造對象,其中6個是重疊的。另外使用了mshtml.dll、ieframe.dll、Jscript.dll等三個動態鏈接庫中11個不同的vfgadgets。攻擊調用了WinExec()彈出了計算器。以下是攻擊使用的vfgadgets:

3.64位 Firefox 36.0a1
攻擊使用了9個偽造對象,其中2個是重疊的。另外使用了libxul.so中5個不同的vfgadgets。攻擊執行了system(“/bin/sh”)。以下是攻擊使用的vfgadgets:

4.其他人進行的COOP攻擊
鏈接為翻譯,原文在鏈接都有相應鏈接。
使用最新的代碼重用攻擊繞過執行流保護(https://bbs.pediy.com/thread-217335.htm)
防御COOP
由于COOP主要針對C++語言特性,所以較好考慮了C++語言特性的防御措施都能比較好的地防御 COOP攻擊。COOP提出者在后續論文里也提出了表隨機化(Table Randomization)來對抗COOP。