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了

编译以后的二进制文件:

相关文章
相关标签/搜索