逆向 C++
這些年來,逆向工程分析人員一直是憑藉著彙編和 C 的知識對大多數軟體進行逆向工程的,但是,現在隨著越來越多的應用程式和惡意軟體轉而使用 C++語言進行開發,深入理解 C++ 物件導向方式開發的軟體的反組譯碼技術就顯得越發的必要。本文試圖通過分析在反組譯碼時如何手工識別 C++對象,進而討論如何自動完成這一分析過程最終介紹我們自己開發的自動化 工具,一步一步的協助讀者掌握逆向 C++程式的一些方法。
作者:Paul Vincent Sabanal
Mark Vincent Yason
譯者:Hannibal509@gmail.com
I.引言和必要性
對於逆向工程分析人員來說,能從一個二進位可執行檔中識別出 C++程式
的結構,並且能標識出各個主要的類,以及這些類之間的關係(繼承、派生等)
是非常重要的。為了能做到這一點,逆向工程分析人員就必須要
(1) 能識別出這些類
(2) 能識別出這些類之間的關係
(3) 識別出類中的各個成員
本文就是 要教大家能做到上述三點。首先我們先來討論如何手工的分析一個 C++程式編譯的二進位可執行檔,從中提取出有關的類的資訊。然後我們再來討論如何自動化這一手工分析的過程.
當然,要做到這一點需要你花上不少的功夫學習很多技巧,但是為什麼我們要學習並掌握這些東西呢?我認為有下面這三點理由要求我們這麼做:
1) 用 C++開發的惡意軟體越來越多了
跟據我們分析惡意軟體的經驗,現在我們要分析的惡意軟體中使用 C++開發的惡意軟體越來越多了。你知道,把這些惡意軟體扔到 IDA 裡去進行靜態分析的難度會比較大,因為相對於 C 中的直接函數調用而言,靜態分析 C++中的虛函數調用就比較困難,因為 C++中的調用虛函數是採用間接調用的方式,有時你甚至都能難確定某個函數是否被調用過。比如臭名昭著的 Agobot 病毒就是用 C++寫的,另外我自己的蜜罐裡最近也捕獲了一些新的 C++寫的惡意軟體。
2) 用 C++開發的現代的應用程式也越來越多了
隨著作業系統和應用程式的規模和複雜度的與日俱增,C++越來越受軟體開發人員的青睞。這也導致了在漏洞發掘等逆向工程任務中面對 C++語言編寫的軟體的可能性也就越來越大。所以逆向分析人員必須要掌握 C++相關的逆向工程技術
3)關於 C++的逆向工程資料極少
我們相信把 C++的逆向工程資料整理成冊,提供給逆向工程分析人員是一件功德無量的好事,因為這一方面的資料是在是太少了。(譯註:在《駭客反組譯碼揭密》一書中有部分討論)
注意:本文中討論的 C++可執行檔僅限於使用 Microsoft Visual C++編譯器編譯出的 C++可執行檔.
2.手工方法
這一節, 主要討論手工分析 C++可執行檔的方法。主要討論如何識別類及 其成員(變數,函數以及建構函式和解構函式) 以及類與類之間的關係。
A. 識別類及其建構函式
要識別出類的成員及類與類之間的關係,我們首先要把各個類給識別出來,所以我們先來識別類及其建構函式。我們可以通過下列特徵從一個可執行檔中 把類和它的建構函式識別出來:
1) 大量的使用 ECX 寄存器(作為 this 指標)。我們應該首先注意到的是在反組譯碼代碼中會大量出現使用 ECX 寄存器(用來傳遞 this 指標)的情況。如,我們看到在給 ECX 寄存器賦值之後,馬上調用了一個函數。
另外,我們在函數中可能會經常看到 ECX 寄存器還沒有初始化就直接被使用的情況(如),這時我們基本上就可以猜出來:這個函數應該就是某個類的成員函數。
2)呼叫慣例。這一點與 1)有關,類的成員函數在被調用時基本上是把函數的參數壓入棧中,而使用 ECX 傳遞 this 指標。如下面這個例子,在為類建立了一個對象之後,new 返回的指標(該指標指向分配給對象的地址)EAX 的值馬上被傳給了 ECX,然後就調用了建構函式。
另外,我們有時還會遇到一些間接函數調用,這很可能是調用類的虛函數,當然,在靜態分析的情況下(即不是在調試器中進行動態分析)如果不是事先明確的知道這個虛函數是哪個類的,要深入跟蹤這個虛函數還是很困難的。我們考慮下面這個例子:
在這個例子裡,我們首先要知道 ClassA 的虛函數表(virtual function table)在哪裡,然後才能根據虛函數表來確定虛函數的代碼所在的位置。
3)STL(標準模版庫 Standard Template Library)中的代碼和可執行檔匯入的 DLL。另外,如果我們在檢查二進位可執行檔時發現這個可執行檔使用了 STL 中的代碼,這一點可以通過分析可執行檔要求匯入的函數或者通過 IDA 的 FLIRT 之類的庫簽名識別方法來做到:
下面是調用 STL 中的代碼的情況:
類的執行個體
在我們進一步深入討論之前,逆向工程分析人員還應該熟悉對象(或者說一個類的執行個體)在記憶體中是個什麼樣子,說的文縐縐一點就是類在記憶體中的布局情況。我們先來看一個簡單的類:
這個類在記憶體中是這個樣子的:
最後一個類的成員變數後面有 3 個位元組的填充,這是因為要求 4 位元組對齊。 在 Visual C++中,類的成員變數是按照其聲明的大小依次排列在記憶體中的。 看 PPT 裡的更清楚一點:
那麼怎麼才能得到上面這張圖呢? 我們可以使用-d1reportAllClassLayout 這個編譯開關,它可以讓 MSVC 編譯器(譯註:至少是 MSVC 6.0 以上的版本)產生一個.layout 檔案,在該檔案中包含有大量的極具價值的類的布局資訊,包括基類在衍生類別中的位置,虛函數表,虛基類表(virtual base class table 我們下面會深入討論),類的成員變數等資訊(實際上我們這些圖表都是從.layout檔案中取出的)。
那麼,如果在一個類中含有虛函數呢?
下面是這個類在記憶體中的存在形式:
注意指向虛函數表的指標(vfptr)是被添加在最前面的,而在虛函數表裡面,各個虛函數是按照其聲明的順序排列的。類 Ex2 的虛函數表如下:
下面這個圖是 PPT 裡的更清楚一點:
當一個類是繼承另一個類的話,情況又會怎麼樣呢? 下面討論一個簡單的單一繼承關係
正如您所看到的,衍生類別只是簡單的把基類嵌入到自己內部就完事了。但是萬一要是有多重繼承有會有什麼情況發生呢? 記憶體中的情況會是這樣:看 PPT 上的圖更清楚一點: 衍生類別將每個基類都嵌入了自身,而且每個基類還都保留有自己的虛函數
表。但是請注意,第一個基類的虛函數表是被衍生類別共用的,衍生類別的虛函數將
會被例在基類虛函數表的後面。另外要注意的是,因為 Ex5 中也有一個和 Ex4
的虛函數同名的 func1(),所以根據 C++的規則,Ex4 虛函數中的 func1()函數的
指標已經被 Ex5 的 func1()的函數指標給替換掉了。