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

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

  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了

編譯以後的二進制文件:

相關文章
相關標籤/搜索