vTable 指针总是位于模块内吗?

逆向工程 视窗 C++ 记忆 虚表
2021-06-25 07:29:51

假设标准的 VC++ vTable 布局:

  Heap-Addr           TableAddr
+-----------+       +-----------+
| TableAddr | ----> | Funcion-1 | ----> Exec region of module
+-----------+       +-----------+
| a         |       | Funcion-2 | ----> Exec region of module
+-----------+       +-----------+
| b         |       | Funcion-3 | ----> Exec region of module
+-----------+       +-----------+
| c         |       |    ...    |
+-----------+       +-----------+

TableAddr 是否保证始终位于模块内部,或者它可以位于地址空间中的任何位置,如果是这样,什么条件会使 TableAddr 位于模块外部。

为清楚起见:模块是任何具有只读或可执行内存区域的 .exe 或 .dll,允许调用函数。根据我的经验,我只看到 TableAddr 驻留在模块的只读部分中。

2个回答

请注意,并非每个 C++ 编译器都必须使用 vtable 指针。例如,20 年前的 Watcom C++ 编译器,通过为对象本身内的每个方法保留一个函数指针来实现方法调用;new操作者它产生一个新的对象,每次初始化每个这些函数指针单独地。

vtable 的核心思想就是优化这一点。在生成对象时,它的确切类别是已知的。vtable 允许编译器一次分配所有这些“函数指针”(通过将 vtable 的地址放在对象中),代价是每次调用一个方法时多一个间接。但无论如何,任何可以实例化的类的 vtable 在编译时都是已知的*。这意味着 vtable 可以在编译时预先初始化,这意味着它必然会被放入某个已初始化的数据段中。

因此,对于由当前 C++ 编译器生成的代码,您应该能够假设所有 vtable 都驻留在已初始化的段中,而不是堆或堆栈中。

但是,如果您正在尝试编写反编译器或以某种方式自动进行动态分析的工具,您至少应该检查这种情况。如果我想混淆我的代码,我可以轻松地在汇编中实现一个函数,它接受一个指向对象的输入指针,生成该对象的 vtable 的副本,可能会修改一两个 vtable 条目,并调整 vtable 指针以引用复制。如果您编写了一个广为人知的分析工具,这甚至可能是混淆器用来专门击败您的工具的技术。

因此,虽然现在应该没有理由将 vtables 放在任何地方,但现在已初始化段中,但我不想在一般情况下依赖于此。

(*) 我有很长时间没有使用 C++,这意味着我没有密切关注较新的规范。我不能保证 C++11 或 C++14 没有引入一些需要动态构建 vtable 的奇怪构造。但如果是这样的话,我会感到非常惊讶。

我相信两者都有可能

  • 指向不同模块的 vtable 指针,以及
  • 用于 vtable 中的函数指针指向不同的模块(至少是间接的)。

如果您有一个从 DLL 导出的基类(至少有 1 个虚方法)和一个在可执行文件中从它派生的类(将导入 DLL),那么第一个将发生。

当在 exe 模块中构造派生类对象时,它将首先构造为基类对象(通过调用 DLL 中的基构造函数),因此该对象的 vtable 指针将指向 DLL 模块。这只会持续很短的时间,因为不久之后,可执行文件中的派生类构造函数将更新对象的 vtable 指针以指向将在可执行模块中的派生类 vtable。

如果派生类没有覆盖所有基类虚拟方法,则可能会发生第二种情况。对于这些方法,正确的实现将是驻留在 DLL 中的基类方法。因此,相关的 vtable 条目将需要在基类 DLL 中的方法处结束。这可能是通过可执行文件本身的导入存根函数实现的。