原文地址:Reversing C++ programs with IDA pro and Hex-raysios
簡介
在假期期間,我花了不少時間學習和逆向用C++寫的程序。這是我第一次學習C++逆向,而且只使用IDA進行分析,感受難度仍是比較大的。編程
這是你用Hex-ways分析一個有意思的函數時看到的東西小程序
v81 = 9; v63 = *(_DWORD *)(v62 + 88); if ( v63 ) { v64 = *(int (__cdecl **)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))(v63 + 24); if ( v64 ) v62 = v64(v62, v1, *(_DWORD *)(v3 + 16), *(_DWORD *)(v3 + 40), bstrString); }
咱們的任務是添加一些符號名稱、分辨出類等,讓hex-rays可以有足夠的信息給出咱們一個可靠、易於理解的輸出數據結構
padding = *Dst; if ( padding < 4 ) return -1; buffer_skip_bytes(this2->decrypted_input_buffer, 5u); buffer_skip_end(this2->decrypted_input_buffer, padding); if ( this2->encrypt_in != null ) { if ( this2->compression_in != null ) { buffer_reinit(this2->compression_buffer_in); packet_decompress(this2, this2->decrypted_input_buffer, this2->compression_buffer_in); buffer_reinit(this2->decrypted_input_buffer); avail_len = buffer_avail_bytes(this2->compression_buffer_in); ptr = buffer_get_data_ptr(this2->compression_buffer_in); buffer_add_data_and_alloc(this2->decrypted_input_buffer, ptr, avail_len); } } packet_type = buffer_get_u8(this2->decrypted_input_buffer); *len = buffer_avail_bytes(this2->decrypted_input_buffer); this2->packet_len = 0; return packet_type;
固然hex-rays不會本身命名這些變量名,你須要理解這些代碼,至少給這些類一個合適的名字能幫你分析代碼。函數
這裏個人全部例子都是用visual studio或者Gnu C++編譯的,這兩個編譯器的結果是類似,即便他們在某些語法上並不兼容。若是本身的編譯器遇到問題,本身改下代碼吧。工具
C++程序的結構
這裏我就不介紹OOP編程的知識了,你也應該已經知道了。咱們只從總體看下OOP是如何工做的和實現的。學習
Class = data structure + code (methods).
類的數據結構只能在源碼裏看到,函數則會顯示在你的反彙編器裏。測試
Object = memory allocation + data + virtual functions.
對象是一個類的一個實例,你能夠在IDA裏看到它。一個對象須要內存,因此你會看到調用new()或者棧分配內存,調用構造函數或者析構函數。你也會看到訪問成員變量(成員對象),調用虛函數。this
虛函數很蠢,若是不下斷點運行程序,你很難知道哪些代碼會被執行。spa
成員函數簡單點,他們就像C語言裏的結構。而且IDA有很是順手的工具聲明結構,hex-rays能在反彙編過程當中很好的用到這些結構信息。
接下來咱們將回到具體的問題上來。
對象的建立
int __cdecl sub_80486E4() { void *v0; // ebx@1 v0 = (void *)operator new(8); sub_8048846(v0); (**(void (__cdecl ***)(void *))v0)(v0); if ( v0 ) (*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0); return 0; }
這是一個我用G++編譯的小程序的反彙編結果,咱們能看到new(8),意思是這個對象大小爲8bytes,而不是咱們有一個8bytes大小的變量。
函數sub_8048846在調用new()以後馬上被調用,並把new()產生的指針做爲參數,這確定就是構造函數了。
下一個函數就有點讓人頭大了,它在調用v0以前對v0作了兩次解引用。這是一個虛函數調用。
全部的多態對象在他們變量中都有一個特殊的指針,被稱做vtable。這個表包含了全部虛函數的地址,因此C++程序在須要的時候可以調用他們。在多種編譯器中,我測試出vtable老是一個對象的第一個元素,老是待在相同的位置,即便是在子類中。(這也許對多繼承不合適,我沒有測試過)。
讓咱們開始用IDA進行分析:
重命名符號名稱
點擊一個名字,而後按n,就會彈出修更名字的窗口,你能夠把它改爲一個有意義的名字。目前咱們還不知道這個類在作什麼,因此我建議把這個類命名成「class1」,直到咱們理解了這個類在作些什麼。在咱們完成分析class1以前咱們極可能會遇到其餘類,因此我建議遇到他們的時候只改下這些類的名字。
int __cdecl main() { void *v0; // ebx@1 v0 = (void *)operator new(8); class1::ctor(v0); (**(void (__cdecl ***)(void *))v0)(v0); if ( v0 ) (*(void (__cdecl **)(void *))(*(_DWORD *)v0 + 8))(v0); return 0; }
建立結構
IDA的結構(structures)窗口很是有用。按shitf + f9可以調出來。我建議你把它拖出來放到IDA窗口的右邊(IDA的QT版能這麼作),而後你就能同時看到反彙編窗口和結構窗口。
按Insert鍵並建立一個新的結構「class1」。咱們已經知道這個結構是8bytes長,按d鍵增長變量,直到咱們有兩個dd變量。重命名第一個變量爲「vtable」,而後就變成下面的樣子了。
接下里咱們添加函數的類型信息,右鍵v0,選擇Convert to struct * ,選擇class1。此外,按y,而後輸入「 class1 * 」也能獲得同樣的結果。
建立一個新的長度爲12bytes的結構並把它命名成「class1_vtable」。如今咱們並不知道vtable有多大,但改結構的大小很容易。點擊class1結構裏的vtable,按y,把它的類型改爲「class1_vtable *」。按F5刷新下僞代碼的窗口,結果以下:
咱們能夠把方法命名成"method1"到「method3」。method3固然就是析構函數。根據編程約定和所使用的編譯器,第一個函數常常是析構函數,但這裏有一個反例。如今咱們分析下構造函數。
分析構造函數
int __cdecl class1::ctor(void *a1) { sub_80487B8(a1); *(_DWORD *)a1 = &off_8048A38; return puts("B::B()"); }
你能夠先把a1的類型改一下。puts()調用證明了這個是構造函數,咱們甚至能瞭解到這個類叫「B」。
sub_80487B8() 在構造函數裏被直接調用,這個函數也許是class1的經函數,但也多是父類的構造函數。
off_8048A38是class1的vtable,到這裏你已經能知道vtable的大小了(只須要看vtable附近有Xref的數據的數量)和一個class1虛函數的列表。你能夠把他們命名成「 class1_mXX」,但須要注意的是其中的一些函數可能與其餘類共享。
更改這個vtable的類型信息也是沒有問題的。但我不推薦這麼作,由於你會丟掉IDA的經典窗口,而且這樣作也提供不了任何你在經典窗口裏看不到的東西。
構造函數裏的奇怪調用:
int __cdecl sub_80487B8(int a1) { int result; // eax@1 *(_DWORD *)a1 = &off_8048A50; puts("A::A()"); result = a1; *(_DWORD *)(a1 + 4) = 42; return result; }
構造函數裏的sub_80487b8() 函數是一樣類型的函數:一個虛函數表 指針放到了vtable成員裏,puts()調用告訴咱們咱們在另一個構造函數裏。
不要把參數a1的類型改爲class1,由於我咱們已經不在class1裏了。咱們找到了一個新的類,把它命名成class2。這個類class1的父類。咱們作下和class1同樣的工做。他們之間的區別僅僅是咱們不知道class2成員的具體大小。這裏有兩種方法找到它:
- 看對class2 ::ctor的xref,若是咱們能找到一個對它的直接調用,例如一個對class2的實例化,咱們就能知道class2成員函數的大小。
- 看vtable裏的函數,嘗試找出被訪問過的最高的成員。
在咱們這種狀況下,class2 ::ctor訪問了最開始的4個字節以後的4個字節。由於class2的子類class1是8個字節長,因此class2的大小也是8個字節。
爲全部的子類作一樣的操做,從父類到子類給這些虛函數進行命名。
對析構函數的研究
Let’s go back to our main function. We can see that the last call, before our v0 object becomes a memory leak, is a call to the third virtual method of class2. Let’s study it.
if ( v0 ) ((void (__cdecl *)(class1 *)) v0->vtable->method_3)(v0);
void __cdecl class1::m3(class1 *a1) { class1::m2(a1); operator delete(a1); }
void __cdecl class1::m2(class1 *a1) { a1->vtable = (class1_vtable *)&class1__vtable; puts("B::~B()"); class2::m2((class2 *)a1); }
void __cdecl class2::m2(class2 *a1) { a1->vtable = (class2_vtable *)&class2__vtable; puts("A::~A()"); }
咱們能夠看到, class1::m3是一個析構函數,調用了class1::m2這一class1的主要析構函數。這個析構函數經過設置vtable爲class1確保咱們在class1的上下文。而後調用了class2的析構函數,這個析構函數也把vtable設置爲class2的上下文。這種方法被用來遍歷整個類的繼承樹,由於繼承樹的全部類的虛析構函數都要被調用。
這些映射是怎麼回事,爲何兩個結構裏定義了同樣的變量?
在用C表示OOP的過程當中,咱們遇到了和你同樣的問題:有時候某些變量在全部的繼承樹裏都會出現。下面是我避免變量重複定義的方法:
對每個類,定義一個classXX_members, classXX_vtable, classXX結構 classXX 包含 +++ vtable (typed to classXX_vtable *) +++ classXX-1_members (members of the superclass) +++ classXX_members, if any classXX_vtable contains +++classXX-1_vtable +++classXX’s vptrs, if any
理想狀況下,你應該從父類開始到子類結束,直到你分析到一個沒有子類的類位置。在這個例子裏,下面使咱們的解決辦法:
00000000 class1 struc ; (sizeof=0x8) 00000000 vtable dd ? ; offset 00000004 class2_members class2_members ? 00000008 class1 ends 00000008 00000000 ; ----------------------------------------------00000000 00000000 class1_members struc ; (sizeof=0x0) 00000000 class1_members ends 00000000 00000000 ; ----------------------------------------------00000000 00000000 class1_vtable struc ; (sizeof=0xC) 00000000 class2_vtable class2_vtable ? 0000000C class1_vtable ends 0000000C 00000000 ; ----------------------------------------------00000000 00000000 class2 struc ; (sizeof=0x8) 00000000 vtable dd ? ; offset 00000004 members class2_members ? 00000008 class2 ends 00000008 00000000 ; ----------------------------------------------00000000 00000000 class2_vtable struc ; (sizeof=0xC) 00000000 method_1 dd ? ; offset 00000004 dtor dd ? ; offset 00000008 delete dd ? ; offset 0000000C class2_vtable ends 0000000C 00000000 ; ----------------------------------------------00000000 00000000 class2_members struc ; (sizeof=0x4) 00000000 field_0 dd ? 00000004 class2_members ends 00000004
int __cdecl main() { class1 *v0; // ebx@1 v0 = (class1 *)operator new(8); class1::ctor(v0); ((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.method_1)(v0); if ( v0 ) ((void (__cdecl *)(class1 *)) v0->vtable->class2_vtable.delete)(v0); return 0; }
int __cdecl class1::ctor(class1 *a1) { class2::ctor((class2 *)a1); a1->vtable = (class1_vtable *)&class1__vtable; return puts("B::B()"); }
class2 *__cdecl class2::ctor(class2 *a1) { class2 *result; // eax@1 a1->vtable = (class2_vtable *)&class2__vtable; puts("A::A()"); result = a1; a1->members.field_0 = 42; return result; }
總結
- 當你找到一個新的類時,對其進行命名,在分析出這個類的有意義的名字前分析出整個繼承樹。
- 從父類開始分析到子類。
- 先查看構造函數和析構函數,找到對new()和靜態方法的調用。
- 同一個類的函數在編譯過的文件裏通常彼此相鄰。而相關的類(繼承關係)可能彼此之間離得很遠。有時候構造函數會在子類的構造函數裏內聯,甚至在實例化的地方出現。
- 若是你想在逆向繼承關係比較複雜的結構時,使用「結構包含結構」的技巧只須要命名一次變量。
- 儘管使用hex-rays的類型系統,它很是強大。
- 純虛類很讓人頭大,你能夠發現幾個類有類似的vtable,但卻一般沒有代碼,要注意他們。
本文中用到的代碼
#include <iostream> #include <stdio.h> class A { public: A(){ printf("A::A()\n"); id = 42; } virtual void a(){ printf("Virtual A::a()\n"); } virtual ~A(){ printf("A::~A()\n"); } private: int id; }; class B : public A { public: B(){ printf("B::B()\n"); } virtual ~B(){ printf("B::~B()\n"); } virtual void a(){ printf("Virtual B::a()\n"); A::a(); } }; int main(){ A *b = new(B); b->a(); delete(b); return 0; }
爲了方便我直接把二進制文件後綴改爲jpg了,下載下來把文件後綴去掉就OK了
編譯以後的二進制文件: