之前写了一篇blog,描述 Windows 在x86 下,如何确定Virtual Function在Table中的位置。
没有写x64的情况,主要原因是x64不支持内联汇编。
我们知道MSVC编译器不会直接访问Table,而是会写一个thunk间接访问。
我们第一步要获得这个thunk的地址,用伪代码就是: &Class::vfn_name
c++认为这是一个成员函数指针,他是无法转化为void*的。(还是有办法的)
所以在上一篇blog中,我们用如下的泛型函数和内联汇编实现
template<typename src_type>
void* pointer_cast(src_type src)
{
void* ret = NULL;
__asm
{
mov eax, src
mov ret, eax
}
return ret;
}
调用如下:
void* p_thunk_fn = pointer_cast(&Class::vfn_name);
这样就通过”黑魔法“获得了这个thunk的地址。
当然,到了x64这个方法就行不通了。
后来我在咨询deepseek的时候,他偶尔提了一嘴,说可以用Union的方法
然后我想起来还有这个玩法,以前看到过。
这样直接通过Union就可以骗过编译器,获得thunk的地址
实现如下:
union FunctionPointer {
void (Class::*vfn_name)(int arg);
void* ptr;
};
FunctionPointer fp;
fp.vfn_name = &Class::vfn_name;
void* thunk = fn.ptr; //getchu~
当然,我们为了获取所有地址,需要每一个函数都写一个这样的FunctionPointer
这样要疯掉。
让deepseek给我写了一个泛型,这时候体现出ai作为工具能极大提高工作效率!
template <typename T>
union TFP;
// 特化模板,支持成员函数指针
template <typename ClassType, typename ReturnType, typename... Args>
union TFP<ReturnType(ClassType::*)(Args...)> {
using MemberFuncPtr = ReturnType(ClassType::*)(Args...); // 成员函数指针类型
MemberFuncPtr memberFunc; // 成员函数指针
void* ptr; // void* 指针
};
实现了泛型的FunctionPointer,TFP
接下去实现泛型的GetThunk
template <typename ClassType, typename ReturnType, typename... Args>
void* GetThunk(ReturnType(ClassType::* func)(Args...))
{
TFP<decltype(func)> fp;
fp.memberFunc = func;
return fp.ptr;
}
任意成员函数只需要
void* pthunk = GetThunk(&Class::vfn_name);
到这里我们获得了Thunk
接着开始反汇编x64的thunk
我们发现他的逻辑和x86是一模一样的,只是指令变为了x64的版本
下面直接贴出实现 x64 下, Windows MSVC环境如何从 thunk 获取 虚函数的 位置(Offset)
int GetMemberFnOffset(void* p_thunk_fn)
{
int vftable_offset = -1;//虚函数表的偏移量
auto jmp_o1_o2_o3_o4 = *(char*)p_thunk_fn;
//第一步
//p_thunk_fn 指向一个 近跳转, 就是当前地址 + offset
//如下
//jmp xti::XtTraderApi::vcall'{4}' (0926C75h)
//E9 Offset1 Offset2 Offset3 Offset4
//E9 A1 58 00 00
if (jmp_o1_o2_o3_o4 != (char)0xE9)
{
throw std::exception("没有找到类似 jmp xti::XtTraderApi::vcall'{4}' (0926C75h) 的语句");
}
auto offset = *(int*)((char*)p_thunk_fn + 1);//跳转的偏移量
auto new_address = (char*)p_thunk_fn + 5 + offset;//当前地址 + 5 个指令(jmp_o1_o2_o3_o4) + 偏移量
//第二步
//转到了
//00007FF754442494 48 8B 01 mov rax, qword ptr[rcx]
//00007FF754442497 FF 60 08 jmp qword ptr[rax + 8]
/*
48:这是 x86-64 架构中的 REX 前缀(REX prefix)。48 表示使用 64 位操作数大小(64-bit operand size)。
8B:这是操作码(opcode),表示 MOV 指令。具体来说,8B 表示将一个值从内存移动到寄存器。
01:这是 ModR/M 字节(ModR/M byte),用于指定操作数的寻址模式。01 的二进制形式是 00000001,具体含义如下:
前两位 00 表示寄存器间接寻址(register indirect addressing)。
中间三位 000 表示目标寄存器是 RAX(64 位寄存器)。
最后三位 001 表示源操作数是 RCX 寄存器指向的内存地址。
*/
auto mov_rax_qword_ptr_rcx = *(unsigned short*)new_address;
if (mov_rax_qword_ptr_rcx != 0x8B48)
{
//抛出异常
throw std::exception("没有找到语句mov rax, qword ptr[rcx]");
}
//到了最后读取 rax + x 中 x 的值
//2个情况
//FF 60 x1 偏移值较小
//FF A0 x1 x2 x3 x4 偏移值较大
auto byte_60_A0 = *((char*)new_address + 3 + 1);
if (byte_60_A0 == 0x60)
{
//FF 60 X1
vftable_offset = *((char*)new_address + 4 + 1);
}
else if (byte_60_A0 == (char)0xA0)
{
//FF A0 X1 X2 X3 X4
vftable_offset = *(int*)((char*)new_address + 4 + 1);
}
else
{
//抛出异常
throw std::exception("没有找到类似 jmp dword ptr [eax + x]的语句, 预期的数据不是60或者A0");
}
return vftable_offset;
}
最后的最后,为了操作简单,做一个Wraper
template <typename ClassType, typename ReturnType, typename... Args>
int GetMemberFnOffsetEx(ReturnType(ClassType::* func)(Args...))
{
auto p_thunk_fn = GetThunk(func);
return GetMemberFnOffset(p_thunk_fn);
}
这样我最终调用效果如下:
dt[GetMemberFnOffsetEx(&XtTraderApi::destroy)] = "destroy";
dt[GetMemberFnOffsetEx(&XtTraderApi::join)] = "join";
dt[GetMemberFnOffsetEx(&XtTraderApi::init)] = "init";
dt[GetMemberFnOffsetEx(&XtTraderApi::getVersion)] = "getVersion";
dt[GetMemberFnOffsetEx(&XtTraderApi::getUserName)] = "getUserName";
dt[GetMemberFnOffsetEx(&XtTraderApi::userLogin)] = "userLogin";
上面dt是一个map,存储着virtual function在table中的位置