【问题标题】:Hooking (hot patching) class member functions. Modifying vtable entries挂钩(热补丁)类成员函数。修改 vtable 条目
【发布时间】:2011-08-08 08:42:21
【问题描述】:

在相当长的序言之后,我有 2 个问题。

通过查看void* 处的任何函数指针,我可以修改它的第一条指令,将它们转换为jmp(32 位相对或 64 位绝对,通过r11,取决于 x86/ x86-64)。我相信在 C 和 C++ 中将函数代码视为数据是非法的,但它似乎以一种不受支持的方式在 MSVC (Win32) 和 GCC (OS X) 中工作。网上有好几个地方说将函数指针转换为void*is illegal

不只是简单地获得指向类成员的指针。我的意思是,编译器在尝试以与查看 void * 相同的方式查看此类指针时直接在构建时抛出错误,这种做法对于非成员函数似乎工作得很好。

幸运的是,为了挂钩 Direct3D9,我正在使用像 IDirect3DDevice9 这样的东西,它有一个 vtable。对于IDirect3DDevice9* 类型的pDev,我将pDev 视为PVOID* 就足够了。然后,pDev 的第一个值是函数指针数组的地址(vtable):

// IDirect3DDevice9::Present()
typedef HRESULT (CALLBACK *PRESENT_PROC)(
    LPDIRECT3DDEVICE9, const RECT*,
    const RECT*,
    HWND,
    const RGNDATA*
);

PVOID (*vPtr)[] = reinterpret_cast<PVOID (*)[]>(
    *reinterpret_cast<PVOID*>(pDev)
);
PRESENT_PROC pDevicePresent = reinterpret_cast<PRESENT_PROC>(
    (*vPtr)[17]
);

因为 Present 是第 18 个条目。

The first answer from here 提供了一种更优雅、更高级别的方法,从定义CINTERFACE 开始。我还没有测试过,但根据它我可以做类似的事情

reinterpret_cast<PVOID>(pDev->lpVtbl->Present)

没有错误。

第一个问题。我不是一个很棒的 C++ 程序员;一般来说,我如何获得一个指向成员函数的指针,以便我可以覆盖该函数的可执行字节。对于非会员,我这样做:

#include <windows.h>
#include <cstdio>
using namespace std;

const unsigned char OP_JMP = 0xE9;  // 32 bit relative jmp
const SIZE_T SIZE_PATCH = 5;        // jmp dword ptr distance; 1 byte + 4 bytes
typedef void (*MyProc)();

void SimpleFunction1()
{
    printf("foo\n");
}

void SimpleFunction2()
{
    printf("bar\n");
}

int main()
{
    PBYTE foo = reinterpret_cast<PBYTE>(SimpleFunction1);
    PBYTE bar = reinterpret_cast<PBYTE>(SimpleFunction2);

    DWORD oldProtection;
    // make sure the bytes of the function are writable
    // by default they are only readable and executable
    BOOL res = VirtualProtect(
        foo,
        SIZE_PATCH,
        PAGE_EXECUTE_READWRITE,
        &oldProtection
    );
    if (!res) return 1;

    // be mindful of pointer arithmetic
    // works with PBYTE, won't with PDWORD
    DWORD distanceToNewFoo = bar - foo - SIZE_PATCH;

    *foo = OP_JMP;
    *reinterpret_cast<PDWORD>(foo + 1) = distanceToNewFoo;

    // called though the pointer instead of foo()
    // to make sure the compiler won't inline or do some other stupid stuff
    reinterpret_cast<MyProc>(foo)(); // will print "bar\n"
    return 0;
}

和 x86-64 的内容相同。对于对象的虚拟成员,我从vtable 本身获得foo 指针,如上所示:

reinterpret_cast<FUNC_TYPE>(
    *(reinterpret_cast<void**>(
        *reinterpret_cast<void**>(objptr)) + n
    )
)

第二个问题。我不能简单地修改 vtable 中的条目为我的对象吗?这是一个例子,不用说,它不适用于直接取自 Direct3D 的 pDev 对象,但 Taksi 似乎使用这种方法:

#include <cstdio>
using namespace std;

class BaseClass
{
public:
    BaseClass(int a = 0, int b = 0);
    int GetA();
    int GetB();
    virtual void Test();
private:
    int _a;
    int _b;
};

BaseClass::BaseClass(int a, int b) :
    _a(a),
    _b(b)
{
}

int BaseClass::GetA()
{
    return _a;
}

int BaseClass::GetB()
{
    return _b;
}

void BaseClass::Test()
{
    printf("test %d; %d\n", _a, _b);
}

void TheNewFunction(BaseClass *bc)
{
    printf("I am an intruder\n");
}

typedef void (*PROC_TYPE)(BaseClass *);

int main()
{
    BaseClass foo(5, 56);
    PROC_TYPE proc = 0;
    proc = reinterpret_cast<PROC_TYPE>(
        *reinterpret_cast<void**>(
            *reinterpret_cast<void**>(&foo)
        )
    );
    proc(&foo);
    reinterpret_cast<void**>(
        *reinterpret_cast<void**>(&foo)
    )[0] = reinterpret_cast<void*>(TheNewFunction);

    foo.Test(); // runs same old Test(); maybe due to compiler optimization?
    proc = reinterpret_cast<PROC_TYPE>(
        *reinterpret_cast<void**>(
            *reinterpret_cast<void**>(&foo)
        )
    );

    proc(&foo); // runs TheNewFunction
    BaseClass *goo = &foo;
    goo->Test(); // runs TheNewFunction
    return 0;
}

【问题讨论】:

    标签: c++ winapi hook direct3d


    【解决方案1】:

    执行这种丑陋演员的最快方法(void * 的成员函数)将是臭名昭著的union_cast&lt;&gt;

    template <class T1, class T2>
    T1 union_cast(T2 v)
    {
      static_assert(sizeof(T1) >= sizeof(T2), "Bad union_cast!");
      union UT {T1 t1; T2 t2;} u {};
      u.t2 = v;
      return u.t1;
    }
    

    这样使用:

    class MyClass
    {
    public:
      void foo(int);
    };
    
    auto p = union_cast<void *>(&MyClass::foo);
    

    现在,我给了你一把上膛的枪,没有安全装置。请谨慎使用...

    【讨论】:

      【解决方案2】:

      正如您所指出的,您的方法不是“可移植的”,但它实际上适用于您的特定情况。恕我直言,没有问题。

      第一个答案:

      这是使用成员函数指针的语法:

      class SomeClass {
          int SomeFunc(int, int);
      };
      
      int (SomeClass::* pfn)(int, int); // variable pfn is a pointer to a SomeClass's member function
      
      pfn = &SomeClass::SomeFunc; // assign this variable to the member function with the adequate prototype.
      
      SomeClass obj; // instance of this class
      
      int res = (obj.*pfn)(1, 2); // call the member function pointer
      

      第二个答案:

      您可以直接修改vtable 的成员,但您应该知道,这样您将所有这个类的对象(可能还有一些派生类)子类化。

      如果您只想对一个特定的 obj 进行子类化,您应该创建另一个函数表,并覆盖对象的 vtable 以指向新的函数表。

      【讨论】:

      • 为什么,谢谢!在第一个答案中,我只需要一种访问SomeFunc 内部操作码的方法,我是一个快乐的露营者;所以将pfn 转换为void*,我不能。第二个答案提供了一些启示;正是我想要的——改变班级成员的行为。
      • 选择你的答案并将我的解决方案添加到第一个问题。
      【解决方案3】:

      在 C++ 中没有可移植的方法来执行此操作,因为语言规范指出,从指向成员函数或指向函数的指针到 void* 所做的任何类型的转换都会导致未定义的行为。如果您想进行这种动态代码重写,您需要查阅您正在使用的特定平台的编译器文档。

      如果您使用生成 vtable 的编译器,您应该能够通过爆破 vtable 来更改成员函数代码的位置,假设 vtable 是可变的。我相信大多数编译器将 vtable 放在只读段中,因此您不能意外(或恶意)这样做。您还必须担心编译器优化以内联调用,因为在某些情况下没有什么可以阻止编译器识别方法调用的目标并且只是对调用进行硬编码。

      【讨论】:

      • 相当不错。我的意思是我已经注意到编译器在我的长代码示例中做了一些讨厌的事情。第一个是 Windows 上的 msvc 2010,而第二个是 OS X 上的 gcc 4.2。第二个 goo-&gt;Test() 调用新函数,而 foo.Test() 仍然调用旧函数。在我的 Windows 应用程序中,我注意到虽然炸毁我的 IDirect3DDevice9 对象 vtable 似乎并不能完成这项工作,而普通的旧注入(使用 E9 操作码,jmp)可以。我猜游戏会制作自己的 Direct3D 对象副本,里程因应用程序而异。
      【解决方案4】:
      class Original {
      public:
          int a;
      
          Original* open(const char *filename, int openmode) {
              printf("Original: %s and %d // %d\n", filename, openmode, this->a);
              this->a = openmode;
              return this;
          }
      };
      
      class Group {
      public:
          Original* my_open(const char *filename, int openmode) {
              Original *self = (Original*) this;
              printf("Fake:: %s and %d // %d\n", filename, openmode, self->a);
              return self;
          }
      };
      
      #define GetPointerToClassMethod(RETURN, CLASS, METHOD, ...) __GetPointerToClassMethod<CLASS, RETURN (CLASS::*)(__VA_ARGS__)>(&CLASS::METHOD)
      
      template <class _Class, class _MethodPrototype>
      LPVOID __GetPointerToClassMethod(_MethodPrototype method) {
          return *(LPVOID*) &method;
      }
      
      int main() {
          Original o;
          o.open("Dorian", 15);
      
          DirectHookProcedure(
              GetPointerToClassMethod(Original*, Original, open, const char*, int),
              GetPointerToClassMethod(Original*, Group, my_open, const char*, int)
          );
      
          o.open("Langbeck", 20);
          return 0;
      }
      

      【讨论】:

      • 一些解释可能会有所帮助
      【解决方案5】:

      在某些情况下你肯定是正确的,比如这个

      BaseClass foo(5, 56);
      //...
      foo.Test(); // runs same old Test(); maybe due to compiler optimization?
      

      编译器可以很容易地看到foo 的类型,根本不需要使用vtable。它知道无论如何要调用什么函数。

      当您通过指向对象的指针进行调用时,编译器设计者不会费心去检查它是否总是相同的类型。在您的代码中,他们肯定可以看到这一点,但也许这样的优化在实际代码中还不够有用?或者他们可能会将它添加到编译器的下一个版本中。 :-)

      而且我们不应该忘记,vtables、它们的存在和可能的布局都是语言标准没有提及的所有实现细节。

      【讨论】:

      • 对。我要做的就是将机器代码视为“一流的价值”。这甚至没有记录在案,但在 C 中也不鼓励,更不用说 C++ 试图在类型安全方面看起来更加强硬。
      【解决方案6】:

      第一个问题的几乎可移植的解决方案 - 辅助函数中的va_list

      void *DisMember(size_t size, ...)
      {
          if (size != sizeof(void *)) return NULL;
          va_list args;
          va_start(args, size);
          void *res = va_arg(args, void *);
          va_end(args);
          return res;
      }
      
      // snip
      void Base::MyMethod() { /* ... */ }
      // snip
      
      void *anything = DisMember(sizeof(void (Base::*)()), &Base::MyMethod);
      

      valdo 拿下了第二个。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-12-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-02-05
        • 2011-12-26
        相关资源
        最近更新 更多