跳转至

编写 VF 表钩子

在深入讨论如何放置 VF 表钩子之前,我们需要再谈谈那些烦人的调用约定。VF 表被类实例用来调用虚成员函数,所有成员函数都有 __thiscall 约定。__thiscall 这个名称来源于成员函数用来引用活动类实例的 this 指针。因此,成员函数把 this 作为 ECX 上的伪参数。

通过声明一个充当所有 __thiscall 钩子回调容器的类,可以匹配 __thiscall 的原型,但我不喜欢这个方法。相反,我发现用内联汇编控制数据更容易。让我们探讨在钩住一个看起来像这样的类时如何控制数据:

class someBaseClass {
public:
    virtual DWORD someFunction(DWORD arg1) {}
};
class someClass : public someBaseClass {
public:
    virtual DWORD someFunction(DWORD arg1) {}
};

someBaseClass 类只有一个成员(一个公共虚函数),someClass 类继承自 someBaseClass 并重载了 someBaseClass::someFunction 成员。要钩住 someClass::someFunction,你在 VF 表钩子中复制原型,如清单 8-3 所示(跟着项目中的 VFHookExample.cpp 文件做)。

DWORD __stdcall someNewVFFunction(DWORD arg1)
{
  static DWORD _this;
    __asm MOV _this, ECX
}

清单 8-3:VF 表钩子的开始

这个函数能作为钩子,是因为 __thiscall 与 __stdcall 的唯一区别是前者在 ECX 上接收 this。为了调和这个微小差异,回调函数使用内联汇编(由 __asm 表示)把 this 从 ECX 复制到静态变量①。由于静态变量实际上初始化为全局变量,在 MOV _this, ECX 之前执行的唯一代码是设置栈帧的代码——而那段代码从不碰 ECX。这确保汇编执行时 ECX 中有正确的值。

 如果多个线程开始调用同一个 VF 函数,someNewVFFunction() 钩子会失效,因为 _this 可能在一个调用仍在使用时被另一个调用修改。我个人从未遇到过这个问题,因为游戏通常不会在线程之间传递关键类的多个实例,但一个有效的补救方法是把 _this 存储在线程本地存储中,确保每个线程有自己的副本。

返回之前,VF 表回调还必须恢复 ECX,以符合 __thiscall 约定。以下是这个过程的样子:

DWORD __stdcall someNewVFFunction(DWORD arg1)
{
    static DWORD _this;
    __asm MOV _this, ECX
    // 在这里做修改游戏的事情
    __asm  MOV ECX, _this
}

执行一些游戏破解代码后,这个版本的 someNewVFFunctio n() 用清单 8-3 中第一条 MOV 指令的反向版本恢复 ECX①。

但与 __cdecl 函数不同,你不应该只用函数指针和 typedef 从纯 C++ 调用使用 __thiscall 约定的函数(就像对调用钩子那样)。从 VF 表钩子调用原始函数时,你必须用内联汇编——这是确保正确传递数据(特别是 _this)的唯一方法。例如这是继续构建 someNewVFFunction() 钩子的方式:

DWORD __stdcall someNewVFFunction(DWORD arg1)
{
    static DWORD _this, _ret;
    __asm MOV _this, ECX
    // 在这里做调用前的事情
    __asm {
        PUSH arg1
        MOV ECX, _this
        CALL [originalVFFunction]
        MOV _ret, EAX
    }
    // 在这里做调用后的事情
  __asm MOV ECX, _this
    return _ret;
}

现在,someNewVFFunction() 把 this 存储在 _this 变量中,允许执行一些代码,调用被钩住的原始游戏函数①,把该函数的返回值存储在 _ret②中,允许执行更多代码,把 this 恢复到 ECX③,并返回存储在 _ret 中的值。被调用者清理 __thiscall 调用的栈,所以与调用钩子不同,压入的参数不需要移除。

 如果你想在任何时候移除单个压入的参数,使用汇编指令 ADD ESP, 0x4,因为单个参数是 4 字节。