IDA Pro - 使用IDA Pro逆向C++程序


原文地址: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成員的具體大小。這里有兩種方法找到它:

  1. 看對class2 ::ctor的xref,如果我們能找到一個對它的直接調用,例如一個對class2的實例化,我們就能知道class2成員函數的大小。
  2. 看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;
}

總結

  1. 當你找到一個新的類時,對其進行命名,在分析出這個類的有意義的名字前分析出整個繼承樹。
  2. 從父類開始分析到子類。
  3. 先查看構造函數和析構函數,找到對new()和靜態方法的調用。
  4. 同一個類的函數在編譯過的文件里一般彼此相鄰。而相關的類(繼承關系)可能彼此之間離得很遠。有時候構造函數會在子類的構造函數里內聯,甚至在實例化的地方出現。
  5. 如果你想在逆向繼承關系比較復雜的結構時,使用“結構包含結構”的技巧只需要命名一次變量。
  6. 盡管使用hex-rays的類型系統,它非常強大。
  7. 純虛類很讓人頭大,你可以發現幾個類有相似的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了

編譯之后的二進制文件:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM