跳转到内容
She11code Blog
Go back

从0开始的逆向工程(二):C++虚函数机制

Edit page

本文是「从0开始的逆向工程」系列的第二篇,将深入讲解C++虚函数的底层机制及其逆向分析方法。

Table of contents

Open Table of contents

概述

虚函数为什么在逆向中特别难

虚函数是C++实现多态的核心机制,但这个机制天然地将”编译时可知”的信息推迟到了”运行时才知道”,这正是逆向分析困难的根源。

普通函数 vs 虚函数

// ========== 普通函数 ==========
void normalFunc() { printf("Hello"); }

void caller() {
    normalFunc();  // 编译时就知道跳转地址
}

// ========== 虚函数 ==========
class Base {
public:
    virtual void func() { printf("Base"); }
};

class Derived : public Base {
public:
    void func() override { printf("Derived"); }
};

void caller(Base* obj) {
    obj->func();  // 编译时不知道跳转到哪里!
}

编译后的区别

; ========== 普通函数调用 ==========
call    normalFunc           ; 地址固定: 0x401000
                              ; 静态分析可直接跟随

; ========== 虚函数调用 ==========
mov     rax, [rcx]           ; 取vptr (运行时的值)
call    [rax+8]              ; 间接调用 (地址未知!)
                              ; 静态分析无法确定目标

核心问题:静态分析时,我们无法知道 call [rax+8] 到底调用了哪个函数,因为 vptr 的值只有在程序运行时才能确定。


虚函数底层机制

虚函数表(vtable)

每个含有虚函数的类都有一个虚函数表(vtable),这是一个函数指针数组。每个对象实例在内存开头都会存储一个指向 vtable 的指针(vptr)。

对象实例                    虚函数表(类级别共享)
┌──────────┐               ┌──────────────────────┐
│ vptr ────┼──────────────►│ [0] &Base::func1     │
├──────────┤               ├──────────────────────┤
│ 成员变量  │               │ [1] &Base::func2     │
└──────────┘               ├──────────────────────┤
                           │ [2] &Base::func3     │
                           ├──────────────────────┤
                           │ [-1] RTTI指针(可选)   │
                           └──────────────────────┘

关键点

虚函数调用的汇编模式

x64 MSVC(64位)

; 假设 rcx = this 指针
mov     rax, [rcx]         ; 取 vptr
call    qword ptr [rax+8]  ; 调用 vtable[1](第二个虚函数)

x86 MSVC(32位,__thiscall)

; 假设 ecx = this 指针
mov     eax, [ecx]         ; 取 vptr
call    dword ptr [eax+4]  ; 调用 vtable[1]

如何在IDA中识别虚函数

识别模式

IDA 反编译后,虚函数调用会呈现出特定的模式:

模式1:标准虚函数调用

// IDA 反编译输出
v11 = *(void***)obj;        // 取 vtable
v11[n](obj, args...);       // 调用第 n+1 个虚函数

连着好几个*是因为对象this指针是指向对象的指针,对象内存开始部分是vptr(虚函数表指针),虚函数表指针指向了虚函数表,虚函数表里面又有执行真正函数的指针。。。。

模式2:紧凑写法

// IDA 反编译输出
(*(void(**)(__int64))(*(_QWORD *)obj + 24))(obj);
//                    ^^^^^^^^^^^^      ^^^
//                    取 vtable[n]      调用

看起来很混乱,可以这样理解:

返回类型 (调用约定 *级)(参数列表)
         │        │
         │        └── * 的数量 = 指针层级
         └─────────── 调用约定(可省略)

快速阅读技巧

看到这种复杂类型转换时,直接跳过类型声明,只看关键部分:

((unsigned __int8 (__fastcall *)(__int64, wchar_t *, _QWORD))v11[4])(v2, &Destination, v12)
^^^^^^^   │  │              │
 │                                                     索引      │  this          参数
 └───────────────────── 类型转换(可忽略)────────────────────┘

简化理解:v11[4](v2, ...) = 调用 vtable 第5个虚函数

定位虚函数表的方法

方法A:从构造函数追踪

构造函数中必有 vptr 初始化:

*(_QWORD *)a1 = &CTcpSocket::`vftable';  // vtable 地址直接暴露!

方法B:从交叉引用反推

  1. 找到虚调用处
  2. 对 vtable 地址按 X 查看交叉引用
  3. 找到所有使用该 vtable 的地方

单继承分析

单继承是最简单的情况:子类只继承一个父类,可以覆盖父类的虚函数。

代码示例

class Base {
    int a;
public:
    virtual void foo() {}
    virtual void bar() {}
};

class Derived : public Base {
    int b;
public:
    void foo() override {}   // 覆盖父类虚函数
    virtual void baz() {}    // 新增虚函数
};

内存布局

Derived 对象:
┌─────────────────┐
│ +0x00: vptr     │ ──► Derived::vtable
├─────────────────┤
│ +0x08: a        │ (Base 成员)
├─────────────────┤
│ +0x0C: b        │ (Derived 成员)
└─────────────────┘

Derived::vtable:
┌─────────────────┐
│ [0] &Derived::foo  │ (覆盖了 Base::foo)
│ [1] &Base::bar     │ (继承,未覆盖)
│ [2] &Derived::baz  │ (新增)
└─────────────────┘

逆向要点

  1. vtable 结构一致:派生类 vtable 前几项与基类结构相同
  2. 覆盖只改地址:如果派生类覆盖了虚函数,只是对应槽位的地址变了
  3. 新增追加末尾:派生类新增的虚函数追加在 vtable 末尾

逆向分析方法论

分析流程

1. 找构造函数

2. 提取 vtable 地址

3. 解析 vtable 内容

4. 分析每个虚函数

5. 推断类结构和继承关系

应对策略

策略方法
从构造函数追踪*(_QWORD*)this = &XXX::vftable
建立 vtable 索引表记录每个槽位对应的函数
对比多个构造函数找共同前缀推断基类结构
利用 RTTI获取类名和继承关系
动态调试验证在虚调用处设断点观察

实战案例:银狐木马分析

下面我们以银狐木马为例,演示如何在实际逆向中分析虚函数。

案例背景

银狐木马是一个典型的远控木马,其网络通信模块使用了 C++ 的面向对象设计,通过虚函数实现协议无关的通信接口。

原始代码

主函数 StartAddress

点击展开 IDA 反编译代码
void __fastcall __noreturn StartAddress(LPVOID lpThreadParameter)
{
  int v1; // eax
  __int64 v2; // rdi
  void *v3; // rax
  __int64 inited; // rbp
  void *v5; // rax
  __int64 v6; // rcx
  __int64 v7; // rsi
  __int64 v8; // rcx
  int v9; // r11d
  int v10; // eax
  void (__fastcall **v11)(_QWORD); // rbx
  unsigned int v12; // eax
  int v13; // ebx
  __int64 v14[2]; // [rsp+28h] [rbp-60h] BYREF
  int v15; // [rsp+38h] [rbp-50h]
  HANDLE hObject; // [rsp+40h] [rbp-48h]
  int v17; // [rsp+50h] [rbp-38h]
  HANDLE hHandle; // [rsp+58h] [rbp-30h]
  void *v19; // [rsp+98h] [rbp+10h] BYREF

  v1 = sub_1400098B0(&unk_14002292C);
  Sleep(1000 * v1);
  v2 = 0i64;
  v3 = operator new(0xB8ui64);
  if ( v3 )
    inited = Init_TcpSocket((__int64)v3);
  else
    inited = 0i64;
  v5 = operator new(0x368ui64);
  v19 = v5;
  if ( v5 )
    v7 = Init_Udpsocket(v5);
  else
    v7 = 0i64;
  while ( 1 )
  {
    sub_140003210(v6);
    if ( byte_1400217EE )
    {
      wcscpy_s(&Destination, 0xFFui64, &word_1400224AC);
      wcscpy_s(&word_1400215B0, 0x1Eui64, &word_1400226AA);
      v9 = dword_1400226E8;
    }
    else
    {
      wcscpy_s(&Destination, 0xFFui64, &Source);
      wcscpy_s(&word_1400215B0, 0x1Eui64, &word_14002246C);
      v9 = dword_1400224A8;
    }
    byte_1400217EE = byte_1400217EE == 0;
    dword_1400213A0 = v9;
    if ( ++dword_140022244 == 200 )
    {
      sub_140003210(v8);
      wcscpy_s(&Destination, 0xFFui64, &word_1400226EC);
      wcscpy_s(&word_1400215B0, 0x1Eui64, &word_1400228EA);
      v9 = dword_140022928;
      dword_1400213A0 = dword_140022928;
      dword_140022244 = 0;
    }
    if ( v2 )
    {
      (**(void (__fastcall ***)(__int64))v2)(v2);
      v9 = dword_1400213A0;
    }
    v2 = v7;
    if ( v9 == 1 )
      v2 = inited;
    v10 = sub_1400098B0(&unk_140022968);
    Sleep(1000 * v10);
    v11 = *(void (__fastcall ***)(_QWORD))v2;
    v12 = sub_1400098B0(&word_1400215B0);
    if ( ((unsigned __int8 (__fastcall *)(__int64, wchar_t *, _QWORD))v11[4])(v2, &Destination, v12) )
    {
      v13 = dword_140022AE8;
      v14[0] = (__int64)&CManager::`vftable';
      v14[1] = v2;
      (*(void (__fastcall **)(__int64, __int64 *))(*(_QWORD *)v2 + 24i64))(v2, v14);
      hObject = CreateEventA(0i64, 1, 0, 0i64);
      v15 = 0;
      v14[0] = (__int64)&CKernelManager::`vftable';
      hHandle = 0i64;
      v17 = v13;
      LOWORD(v19) = 260;
      (*(void (__fastcall **)(__int64, void **, __int64))(*(_QWORD *)v2 + 16i64))(v2, &v19, 2i64);
      (*(void (__fastcall **)(__int64))(*(_QWORD *)v2 + 40i64))(v2);
      WaitForSingleObject(hHandle, 0xFFFFFFFF);
      v14[0] = (__int64)&CKernelManager::`vftable';
      CloseHandle(hHandle);
      v14[0] = (__int64)&CManager::`vftable';
      CloseHandle(hObject);
    }
  }
}

构造函数 Init_TcpSocket

点击展开 IDA 反编译代码
__int64 __fastcall Init_TcpSocket(__int64 a1)
{
  struct WSAData WSAData; // [rsp+20h] [rbp-1B8h] BYREF

  *(_QWORD *)a1 = &CTcpSocket::`vftable';
  *(_QWORD *)(a1 + 8) = CreateEventW(0i64, 1, 0, 0i64);
  *(_QWORD *)(a1 + 16) = 0i64;
  *(_DWORD *)(a1 + 24) = 0;
  *(_DWORD *)(a1 + 88) = 0;
  *(_QWORD *)(a1 + 72) = 0i64;
  *(_QWORD *)(a1 + 80) = 0i64;
  *(_QWORD *)(a1 + 64) = &CBuffer::`vftable';
  *(_QWORD *)(a1 + 96) = &CBuffer::`vftable';
  *(_DWORD *)(a1 + 120) = 0;
  *(_QWORD *)(a1 + 104) = 0i64;
  *(_QWORD *)(a1 + 112) = 0i64;
  *(_QWORD *)(a1 + 128) = &CBuffer::`vftable';
  *(_DWORD *)(a1 + 152) = 0;
  *(_QWORD *)(a1 + 136) = 0i64;
  *(_QWORD *)(a1 + 144) = 0i64;
  WSAStartup(0x202u, &WSAData);
  _InterlockedExchange((volatile __int32 *)(a1 + 32), 0);
  *(_QWORD *)(a1 + 176) = -1i64;
  *(_QWORD *)(a1 + 160) = 0i64;
  *(_QWORD *)(a1 + 168) = 0i64;
  *(_DWORD *)(a1 + 28) = 0;
  return a1;
}

构造函数 Init_Udpsocket

点击展开 IDA 反编译代码
__int64 __fastcall Init_Udpsocket(__int64 a1)
{
  HANDLE EventW; // rax
  HANDLE v3; // rax
  HANDLE v4; // rax
  HANDLE v5; // rax

  *(_QWORD *)(a1 + 16) = 0i64;
  *(_DWORD *)(a1 + 24) = 0;
  *(_QWORD *)a1 = &CUdpSocket::`vftable';
  EventW = CreateEventW(0i64, 1, 1, 0i64);
  *(_QWORD *)(a1 + 64) = EventW;
  if ( !EventW )
    sub_1400012D0(2147500037i64);
  *(_DWORD *)(a1 + 72) = 1;
  *(_QWORD *)(a1 + 76) = 5i64;
  *(_DWORD *)(a1 + 84) = 1;
  *(_QWORD *)(a1 + 88) = -1i64;
  *(_QWORD *)(a1 + 96) = 0i64;
  *(_QWORD *)(a1 + 104) = 0i64;
  *(_DWORD *)(a1 + 112) = 60;
  *(_DWORD *)(a1 + 116) = 60;
  *(_QWORD *)(a1 + 120) = 0i64;
  *(_QWORD *)(a1 + 128) = 0i64;
  *(_DWORD *)(a1 + 136) = 0;
  *(_DWORD *)(a1 + 140) = 3;
  *(_QWORD *)(a1 + 144) = 0i64;
  *(_QWORD *)(a1 + 152) = 0i64;
  *(_QWORD *)(a1 + 160) = 0i64;
  *(_QWORD *)(a1 + 168) = ((__int64 (__fastcall *)(__int64 (__fastcall ***)()))off_14001E000[3])(&off_14001E000) + 24;
  *(_WORD *)(a1 + 176) = 0;
  sub_140008650(a1 + 184);
  *(_DWORD *)(a1 + 432) = 0;
  if ( !InitializeCriticalSectionAndSpinCount((LPCRITICAL_SECTION)(a1 + 440), 0) )
    sub_1400012D0(2147500037i64);
  *(_QWORD *)(a1 + 488) = 0i64;
  *(_QWORD *)(a1 + 496) = 0i64;
  *(_DWORD *)(a1 + 480) = 0;
  *(_QWORD *)(a1 + 504) = a1 + 184;
  v3 = CreateEventW(0i64, 0, 0, 0i64);
  *(_QWORD *)(a1 + 512) = v3;
  if ( !v3 )
    sub_1400012D0(2147500037i64);
  v4 = CreateEventW(0i64, 0, 0, 0i64);
  *(_QWORD *)(a1 + 520) = v4;
  if ( !v4 )
    sub_1400012D0(2147500037i64);
  v5 = CreateEventW(0i64, 0, 0, 0i64);
  *(_QWORD *)(a1 + 528) = v5;
  if ( !v5 )
    sub_1400012D0(2147500037i64);
  *(_DWORD *)(a1 + 536) = 0;
  *(_DWORD *)(a1 + 540) = 0;
  *(_DWORD *)(a1 + 544) = 1;
  *(_DWORD *)(a1 + 548) = 1;
  *(_DWORD *)(a1 + 552) = 2;
  *(_DWORD *)(a1 + 556) = 10;
  *(_DWORD *)(a1 + 560) = 128;
  *(_DWORD *)(a1 + 564) = 512;
  *(_DWORD *)(a1 + 568) = 30;
  *(_DWORD *)(a1 + 572) = 1432;
  *(_DWORD *)(a1 + 576) = 5;
  *(_DWORD *)(a1 + 580) = 4096;
  *(_DWORD *)(a1 + 584) = 5000;
  *(_QWORD *)(a1 + 592) = 0i64;
  *(_QWORD *)(a1 + 600) = 0i64;
  sub_140008780(a1 + 616);
  *(_QWORD *)(a1 + 760) = &CBuffer::`vftable';
  *(_DWORD *)(a1 + 784) = 0;
  *(_QWORD *)(a1 + 768) = 0i64;
  *(_QWORD *)(a1 + 776) = 0i64;
  *(_QWORD *)(a1 + 792) = &CBuffer::`vftable';
  *(_DWORD *)(a1 + 816) = 0;
  *(_QWORD *)(a1 + 800) = 0i64;
  *(_QWORD *)(a1 + 808) = 0i64;
  *(_QWORD *)(a1 + 824) = &CBuffer::`vftable';
  *(_DWORD *)(a1 + 848) = 0;
  *(_QWORD *)(a1 + 832) = 0i64;
  *(_QWORD *)(a1 + 840) = 0i64;
  _InterlockedExchange((volatile __int32 *)(a1 + 32), 0);
  *(_DWORD *)(a1 + 28) = timeGetTime();
  *(_QWORD *)(a1 + 8) = CreateEventW(0i64, 1, 0, 0i64);
  *(_QWORD *)(a1 + 856) = CreateEventW(0i64, 0, 0, 0i64);
  *(_DWORD *)(a1 + 28) = 0;
  *(_QWORD *)(a1 + 592) = operator new(*(unsigned int *)(a1 + 580));
  *(_QWORD *)(a1 + 160) = operator new(0x598ui64);
  return a1;
}

第一步:从构造函数推断类结构

CTcpSocket 内存布局

Init_TcpSocket 可以分析出 CTcpSocket 的内存布局:

CTcpSocket (大小: 0xB8 = 184 字节)

偏移      类型              内容
────────────────────────────────────────────────
+0x00     void**           vptr → CTcpSocket::vftable
+0x08     HANDLE           m_hEvent (CreateEventW)
+0x10     void*            0 (未使用?)
+0x18     int              状态标志
+0x1C     int              0
+0x20     atomic int       引用计数 (_InterlockedExchange)
          ─────────────────────────────────────
+0x40     CBuffer          m_sendBuffer (嵌入对象)
+0x60     CBuffer          m_recvBuffer (嵌入对象)
+0x80     CBuffer          m_workBuffer (嵌入对象)
          ─────────────────────────────────────
+0xA0     ...              其他成员
+0xB0     SOCKET           m_socket (-1 = INVALID_SOCKET)

CUdpSocket 内存布局

Init_Udpsocket 可以分析出 CUdpSocket 的内存布局:

CUdpSocket (大小: 0x368 = 872 字节)

偏移      类型              内容
────────────────────────────────────────────────
+0x00     void**           vptr → CUdpSocket::vftable
+0x08     HANDLE           m_hEvent
+0x10     void*            0
+0x18     int              状态标志
+0x20     atomic int       引用计数
          ─────────────────────────────────────
+0x40     HANDLE           事件句柄
+0x48     int              状态 = 1
+0x4C     __int64          超时 = 5
+0x54     int              标志 = 1
+0x58     SOCKET           m_socket (-1)
          ─────────────────────────────────────
...大量 UDP 特有成员...
          ─────────────────────────────────────
+0x2F8    CBuffer          m_buffer1
+0x318    CBuffer          m_buffer2
+0x338    CBuffer          m_buffer3

第二步:推断继承关系

关键发现

两个类的 前 0x28 字节结构完全相同

+0x00: vptr
+0x08: HANDLE m_hEvent
+0x10: void* m_ptr
+0x18: int m_state
+0x20: atomic int m_refCount

结论CTcpSocketCUdpSocket 都继承自同一个基类 ISocket

还原的类定义

// 基类:抽象 Socket 接口
class ISocket {
protected:
    void* vptr;           // +0x00
    HANDLE m_hEvent;      // +0x08
    void* m_ptr;          // +0x10
    int m_state;          // +0x18
    atomic int m_refCount;// +0x20

public:
    virtual ~ISocket() = 0;
    virtual bool Connect(const wchar_t* host, int port) = 0;
    virtual int Send(void* data, int len) = 0;
    virtual void OnConnect(CManager* mgr) = 0;
    virtual void Run() = 0;
};

// TCP 实现
class CTcpSocket : public ISocket {
    // +0x28 ~ +0x3F: TCP 特有成员
    CBuffer m_sendBuffer;   // +0x40
    CBuffer m_recvBuffer;   // +0x60
    CBuffer m_workBuffer;   // +0x80
    SOCKET m_socket;        // +0xB0
};

// UDP 实现
class CUdpSocket : public ISocket {
    // +0x28 ~ +0x2F7: UDP 特有成员(非常多的配置)
    CBuffer m_buffer1;      // +0x2F8
    CBuffer m_buffer2;      // +0x318
    CBuffer m_buffer3;      // +0x338
};

第三步:分析主函数中的虚函数调用

调用 1:析构函数(vtable[0])

// 原始代码
if ( v2 )
{
    (**(void (__fastcall ***)(__int64))v2)(v2);
}

拆解

void*** vtable = *(void***)v2;  // 取 vtable
(*vtable)(v2);                   // 调用 vtable[0](this)

还原

if (prevSock) {
    delete prevSock;  // 或 prevSock->Release();
}

调用 2:连接函数(vtable[4])

// 原始代码
v11 = *(void (__fastcall ***)(_QWORD))v2;
v12 = sub_1400098B0(&word_1400215B0);
if ( ((unsigned __int8 (__fastcall *)(__int64, wchar_t *, _QWORD))v11[4])(v2, &Destination, v12) )

拆解

v11 = *(void***)v2;                          // 取 vtable
v12 = GetPort(&config);                      // 获取端口
bool success = ((bool(*)(void*, wchar_t*, int))v11[4])(v2, &serverAddr, v12);
                                              // 调用 vtable[4]

还原

if (sock->Connect(serverAddr, port)) {
    // 连接成功...
}

调用 3:连接回调(vtable[3])

// 原始代码
(*(void (__fastcall **)(__int64, __int64 *))(*(_QWORD *)v2 + 24i64))(v2, v14);
//                                            ^^^^^^^^^^^^^^
//                                            +24 = vtable[3]

拆解

void* func = *((void**)v2 + 3);  // vtable[3]
func(v2, v14);                    // 调用

还原

sock->OnConnect(manager);  // 通知管理器连接成功

调用 4:发送数据(vtable[2])

// 原始代码
(*(void (__fastcall **)(__int64, void **, __int64))(*(_QWORD *)v2 + 16i64))(v2, &v19, 2i64);
//                                                     ^^^^^^^^^^^^^^
//                                                     +16 = vtable[2]

还原

sock->Send(buffer, 2);  // 发送心跳/注册包

调用 5:运行主循环(vtable[5])

// 原始代码
(*(void (__fastcall **)(__int64))(*(_QWORD *)v2 + 40i64))(v2);
//                                   ^^^^^^^^^^^^^^
//                                   +40 = vtable[5]

还原

sock->Run();  // 进入主循环,处理命令

第四步:重建虚函数表

根据以上分析,可以重建 ISocket 的虚函数表:

索引偏移函数签名推测名称用途
[0]+0void(this)~ISocket()析构/释放
[1]+8?(this)?未知
[2]+16int(this, void*, int)Send()发送数据
[3]+24void(this, CManager*)OnConnect()连接回调
[4]+32bool(this, wchar_t*, int)Connect()建立连接
[5]+40void(this)Run()运行主循环

第五步:完整流程还原

// 线程入口函数
DWORD WINAPI StartAddress(LPVOID lpThreadParameter) {
    // 初始延迟
    Sleep(GetConfigInt(&g_config) * 1000);

    // 创建 TCP 和 UDP Socket 对象
    CTcpSocket* tcpSock = new CTcpSocket();  // 0xB8 字节
    CUdpSocket* udpSock = new CUdpSocket();  // 0x368 字节

    ISocket* prevSock = nullptr;

    while (true) {
        // 获取服务器配置(双服务器轮换)
        wchar_t serverAddr[256];
        int port;
        if (g_useBackupServer) {
            wcscpy_s(serverAddr, g_backupHost);
            port = g_backupPort;
        } else {
            wcscpy_s(serverAddr, g_primaryHost);
            port = g_primaryPort;
        }
        g_useBackupServer = !g_useBackupServer;

        // 每 200 次循环切换到备用服务器
        if (++g_loopCount == 200) {
            wcscpy_s(serverAddr, g_fallbackHost);
            port = g_fallbackPort;
            g_loopCount = 0;
        }

        // 释放上一个 Socket
        if (prevSock) {
            delete prevSock;
        }

        // 根据协议选择 Socket
        ISocket* sock = (port == 1) ? (ISocket*)tcpSock : (ISocket*)udpSock;
        prevSock = sock;

        // 等待间隔
        Sleep(GetConfigInt(&g_interval) * 1000);

        // 虚函数调用:连接服务器
        if (sock->Connect(serverAddr, port)) {
            // 创建管理器
            CManager* manager = new CManager(sock);

            // 连接成功回调
            sock->OnConnect(manager);

            // 发送注册包
            sock->Send(g_registerPacket, 2);

            // 进入主循环(处理 C2 命令)
            sock->Run();

            // 清理
            delete manager;
        }
    }
}

设计模式总结

银狐木马的网络模块采用了 单继承 + 组合 的设计模式:

┌─────────────────────────────────────────────────────┐
│                ISocket (抽象基类)                    │
│                                                     │
│  +0x00: vptr                                        │
│  +0x08: HANDLE m_hEvent                             │
│  +0x10: void* m_ptr                                 │
│  +0x18: int m_state                                 │
│  +0x20: atomic int m_refCount                       │
│                                                     │
│  虚函数:                                             │
│    [0] ~ISocket()                                   │
│    [2] Send()                                       │
│    [3] OnConnect()                                  │
│    [4] Connect()                                    │
│    [5] Run()                                        │
└─────────────────────────────────────────────────────┘

           ┌──────────────┴──────────────┐
           │                              │
┌──────────┴──────────┐      ┌───────────┴──────────┐
│    CTcpSocket       │      │     CUdpSocket       │
│    (0xB8 字节)       │      │     (0x368 字节)      │
├─────────────────────┤      ├──────────────────────┤
│ +0x28: TCP 成员     │      │ +0x28: UDP 成员       │
│ +0x40: CBuffer ◄────│──┐   │ ...大量配置...        │
│ +0x60: CBuffer      │  │   │ +0x2F8: CBuffer ◄────│──┐
│ +0x80: CBuffer      │  │   │ +0x318: CBuffer      │  │
│ +0xB0: SOCKET       │  │   │ +0x338: CBuffer      │  │
└─────────────────────┘  │   └──────────────────────┘  │
                         │                              │
                         │   ┌─────────────────────┐   │
                         └──►│ CBuffer (组合)       │◄──┘
                             │ +0x00: vptr         │
                             │ +0x08: data ptr     │
                             │ +0x10: size         │
                             └─────────────────────┘

设计特点


总结

虚函数逆向的核心难点在于:调用目标在运行时才能确定

关键技巧

  1. 从构造函数入手 - vtable 地址直接暴露
  2. 对比多个构造函数 - 找共同前缀推断基类
  3. 识别嵌入的 vtable - 区分继承和组合
  4. 建立索引表 - 记录每个虚函数的用途
  5. 动态验证 - 调试确认静态分析结论

模式速查

IDA 模式含义
*(void***)obj取 vtable
vtable[n](obj, ...)调用第 n+1 个虚函数
*((void**)obj + n)等价于 vtable[n]
*(_QWORD*)(obj + offset) = &vftable组合(嵌入对象)

Edit page
Share this post on:

Next Post
从0开始的逆向工程(一):PE与IDA的正确使用方法