本文是「从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指针(可选) │
└──────────────────────┘
关键点:
- 同一个类的所有对象共享同一个 vtable
- vtable 位于只读数据段(
.rdata) - RTTI 信息位于
vtable[-1],包含类名和继承关系
虚函数调用的汇编模式
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:从交叉引用反推
- 找到虚调用处
- 对 vtable 地址按
X查看交叉引用 - 找到所有使用该 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 │ (新增)
└─────────────────┘
逆向要点
- vtable 结构一致:派生类 vtable 前几项与基类结构相同
- 覆盖只改地址:如果派生类覆盖了虚函数,只是对应槽位的地址变了
- 新增追加末尾:派生类新增的虚函数追加在 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
结论:CTcpSocket 和 CUdpSocket 都继承自同一个基类 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] | +0 | void(this) | ~ISocket() | 析构/释放 |
| [1] | +8 | ?(this) | ? | 未知 |
| [2] | +16 | int(this, void*, int) | Send() | 发送数据 |
| [3] | +24 | void(this, CManager*) | OnConnect() | 连接回调 |
| [4] | +32 | bool(this, wchar_t*, int) | Connect() | 建立连接 |
| [5] | +40 | void(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 │
└─────────────────────┘
设计特点:
- 单继承:
CTcpSocket/CUdpSocket→ISocket(实现多态) - 组合:嵌入
CBuffer对象管理缓冲区(不是继承!) - 协议无关:上层代码通过
ISocket*接口操作,不关心底层是 TCP 还是 UDP
总结
虚函数逆向的核心难点在于:调用目标在运行时才能确定。
关键技巧
- 从构造函数入手 - vtable 地址直接暴露
- 对比多个构造函数 - 找共同前缀推断基类
- 识别嵌入的 vtable - 区分继承和组合
- 建立索引表 - 记录每个虚函数的用途
- 动态验证 - 调试确认静态分析结论
模式速查
| IDA 模式 | 含义 |
|---|---|
*(void***)obj | 取 vtable |
vtable[n](obj, ...) | 调用第 n+1 个虚函数 |
*((void**)obj + n) | 等价于 vtable[n] |
*(_QWORD*)(obj + offset) = &vftable | 组合(嵌入对象) |