原文地址:Reversing C++ programs with IDA pro and Hex-rays
簡介
在假期期間,我花了很多時間學習和逆向用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()或者棧分配內存,調用構造函數或者析構函數。你也會看到訪問成員變量(成員對象),調用虛函數。
虛函數很蠢,如果不下斷點運行程序,你很難知道哪些代碼會被執行。
成員函數簡單點,他們就像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了
編譯之后的二進制文件: