虚函数表Hook本质是篡改vtable中虚函数指针地址:先读取对象vptr得到vtable地址,再按索引(如x64下offset=索引×8)定位并替换对应槽位函数指针,需先解除内存写保护,且须准确获知虚函数布局以避免crash。

虚函数表挂钩本质是修改 vtable 指针指向的函数地址
在 C++ 对象实例中,虚函数调用不通过符号名解析,而是通过对象头偏移处的 vtable 指针查表跳转。Frida 本身不直接提供“hook 虚函数”的 API,必须手动定位对象的 vtable、计算目标虚函数在表中的索引(通常是 offset / 8 在 x64 上),再用 Memory.patchCode 或 Interceptor.replace 替换对应槽位的函数指针。
关键前提:你得知道目标类的虚函数布局——要么从符号表(如 _ZTV12TargetClass)推导,要么靠逆向确认虚函数顺序;否则替换错位置会导致 crash 或静默失效。
用 Frida 获取 vtable 地址并验证虚函数索引
常见错误是把对象地址直接当 vtable 地址用。实际对象首字节是 vtable 指针(x64 下 8 字节),需先读取:
const objAddr = ptr("0x12345678"); // 假设这是 new 出来的对象地址
const vtablePtr = objAddr.readPointer(); // 真正的 vtable 起始地址
const funcPtr = vtablePtr.add(0x10).readPointer(); // 假设第 3 个虚函数(索引 2 → offset 0x10)
console.log("original vfunc:", funcPtr);
注意:vtable 本身是只读内存页,直接 writePointer 会失败,必须先 Memory.protect 解除写保护。
立即学习“C++免费学习笔记(深入)”;
- 虚函数索引从 0 开始,
add(0)是第一个虚函数(常为析构函数) - Android 上
vtable可能被 linker 重定位,优先用Module.findExportByName查_ZTV符号 - x86 和 x64 的指针宽度不同,
add()偏移必须按 sizeof(void*) 计算
用 Interceptor.replace 安全替换虚函数指针
直接 patch vtable 槽位虽快,但多线程下有竞态风险;更稳妥的是用 Interceptor.replace 替换目标函数地址,并在 stub 中控制返回值:
const targetVFunc = vtablePtr.add(0x10).readPointer();
Interceptor.replace(targetVFunc, new NativeCallback(function (thisPtr) {
// thisPtr 就是调用该虚函数的对象地址
console.log("hooked virtual call on", thisPtr);
return ptr("0x1"); // 强制返回非零值(按 ABI 调整类型)
}, 'int', ['pointer']));
这里必须显式声明返回类型和参数类型('int', ['pointer']),否则 Frida 默认按 void 处理,导致寄存器状态错乱。
- 若原函数返回
std::string或复杂结构体,不能简单 return ptr() —— 需构造合法对象或改用 inline hook + 寄存器篡改 - 某些编译器(如 MSVC)会在 vtable 前插入 RTTI 指针,
vtablePtr实际是vtable + 8,需调试确认 - 替换后记得
Interceptor.flush()(尤其在 Android 旧版 Frida 上)
绕过编译器优化导致的虚调用内联
如果目标虚函数被编译器内联(例如 -O2 下 trivial 函数),vtable 查找逻辑会被消除,hook vtable 槽位完全无效。此时必须 hook 实际被调用的符号地址,而不是依赖虚表:
// 先尝试找符号(适用于未 strip 的二进制)
const symbol = Module.findExportByName(null, "_ZN12TargetClass10targetFuncEv");
if (symbol) {
Interceptor.replace(symbol, ...);
}
但多数情况符号已丢,只能靠内存扫描特征码或利用 Frida 的 DebugSymbol.fromAddress 辅助定位。
- Clang/GCC 的
-fno-rtti -fno-exceptions会简化 vtable 结构,但也可能合并多个类的 vtable,需交叉验证 - 虚函数带默认参数时,调用方可能传入栈上的默认值,stub 中无法感知,应避免依赖参数默认行为
真正麻烦的从来不是怎么改 vtable,而是怎么在没符号、开了 LTO、又用了多重继承的二进制里,精准定位那个该死的虚函数槽位——这时候得靠 IDA + Frida 联调,或者 dump 出 vtable 内容逐项比对。


















