V-Table 修补(通过 DLL 注入)是否仅适用于 COM 对象/类?

逆向工程 修补 虚表
2021-06-14 11:45:20

好的,不久前我被介绍了挂钩/绕行的概念,我花了一些时间来了解,但尽管缺乏可用信息,但最终我能够挂钩BeginSceneEndScene使用IDAMS Detours以及其中之一d3d9用于测试的示例应用程序。

但我意识到,这只是一种方法,所以我决定我也想熟悉我见过的其他方法。

这让我对 V-Table 进行了一些研究,更具体地说,是一种被描述为 V-Table 动态修补的实践。

我发现这样做的典型方法DirectX是创建一个“虚拟设备”,然后更改它的 V-Table,这可能会影响设备类的所有实例。

我继续尝试使用我自己的类创建一个缩小的环境,其中包含一个虚拟函数和一些实例化所述类的代码,然后连续调用所述函数。

然后我注入了一些可以访问此类副本的代码,它创建了一个新实例并尝试用另一个函数的指针修补 V-Table 函数指针。

但我很快发现我设置它的方式并没有像我预期的那样工作。我很确定它修改了 V-Table,但仅限于该类的实例。(我通过将代码组合到一个程序中进行了测试,该程序简单地调用了该函数,抓取了 V-Table,将其修改为指向另一个函数,然后再次调用该函数以确认它已被交换)

所以我想知道,DirectX 是什么让使用设备类/对象的新实例修补原始 V-Table 成为可能?

热修补是特定于 COM 的,还是有其他解释,也许我都错了?

(编辑)这就是我想要做的。

目标:

using namespace std;

class A {
public:
    virtual void doThing() {
        cout << "Class A doThing" << endl;
    }

};


void dynamicOverride() {
    cout << "Function replaced" << endl;
}


int main()
{

    A* a = new A();
    while (1)
    {
        a->doThing();
        Sleep(2000);
    }

    system("pause");
    return 0;
}

注入代码:

using namespace std;

class A {
public:
    virtual void doThing() {
        cout << "Class A doThing" << endl;
    }

};


void dynamicOverride() {
    cout << "Function replaced" << endl;
}


void MainThread()
{
    A* a = new A();
    void** vtable = *(void***)a;

    DWORD curProtection;
    VirtualProtect(&vtable[0], 4, PAGE_EXECUTE_READWRITE, &curProtection);
    vtable[0] = dynamicOverride;
}

我已经在挂钩各种d3d9功能的代码中看到了这一点,大多数人将其称为创建虚拟设备。但是在这种情况下,当我尝试在 DLL 中使用一个类并注入代码来修改该类的 V-Table 时。它永远不会影响目标进程中该类的实例。

1个回答

你绝对在正确的轨道上

但,

A* a = new A();
...
a->doThing()

根本不会打扰 vtable。因为a只能是A类型。MSVC 仍然会创建 vftable,但在那种情况下a->doThing()只会变成A::doThing()

在这里,我将您的示例修复到我自己的示例中以证明您是正确的,您只需要调用doThing某种级别的继承来触发 vftable 查找。

A_vftable 结构还提供了一个更好的表示,如果你能看到它,vtable 将会是什么。

class Base {
public:
    void virtual VirtualMethod1() = 0;
    void virtual VirtualMethod2() = 0;
};

class A : public Base {
public:
    A() {
        printf("A Constructor\n");
    }

    void virtual VirtualMethod1() {                 
        printf("VirtualMethod1\n");
    }

    void virtual VirtualMethod2() {
        printf("VirtualMethod2\n");  
    }
};

struct A_vftable {
    void(*VirtualMethod1)(A* self);
    void(*VirtualMethod2)(A* self);
};

int main() {

    A a;
    A_vftable* vtable = *(A_vftable**)&a;

    vtable->VirtualMethod1(&a);
    a.VirtualMethod1();

    DWORD oldprotect;
    VirtualProtect(vtable, sizeof(vtable), PAGE_EXECUTE_READWRITE, &oldprotect);
    vtable->VirtualMethod1 = vtable->VirtualMethod2;
    VirtualProtect(vtable, sizeof(vtable), oldprotect, NULL);

    a.VirtualMethod1(); // calls A::VirtualMethod1()
    Base* b = &a;
    b->VirtualMethod1(); // calls our redirected function

    return 0;
}

哦,void dynamicOverride()实际上对virtual void doThing().

doThing() 实际上是 void __thiscall doThing(A* this)

由于dynamicOverride()这里缺少 arg,这很糟糕,但不是灾难性的,因为__thiscall第一个 arg 是通过 ECX 传递的。

但不知道这是一个很好的方式来炸掉筹码,然后你自己就下线了。

始终将挂钩函数替换为完全处理 args/调用约定的函数。