C++铸造到基础和"overwriting" VPTR 问题
C++ casting to base and "overwriting" vptr issue
我刚刚读到一个新的C++挑战: http://blogs.msdn.com/b/vcblog/archive/2014/02/04/challenge-vulnerable-code.aspx
提供的代码如果充满了问题,有些对任何有良好编程习惯的人来说都是显而易见的,有些只有C++本地人才能看到:-)
评论中描述,有一行(37)特别危险:
ImageFactory::DebugPrintDimensions((ImageFactory::CustomImage*)image);
然后,该函数调用CustomImage
的虚拟方法(在CustomImage
中首次定义)。
据称,这会导致CustomImage
的第一个成员被视为实例的 vptr(实际上这是一个unique_ptr
),并使它指向的二进制文件被视为可执行(可能是恶意的)代码。
虽然我可以理解这一点,但我想知道为什么这真的有效。
CustomImage
是一个虚拟类,所以(可能)它的前 4 个字节(假设 X86)是 VPTR,下一个是unique_ptr
成员。而且由于演员阵容似乎没有改变任何东西......
。如何执行unique_ptr
持有的数据?
我的看法(我非常乐意得到纠正):
在这里,CustomImage
是一个多态类(vptr 作为 Windows ABI 下的第一个"成员"),但Image
不是。定义的顺序意味着ImageFactory
函数知道CustomImage
是一个Image
,但main()
不知道。
因此,当工厂这样做时:
Image* ImageFactory::LoadFromDisk(const char* imageName)
{
return new (std::nothrow) CustomImage(imageName);
}
CustomImage*
指针转换为Image*
,一切正常。因为Image
不是多态的,所以指针被调整为指向CustomImage
内的(POD)Image
实例 - 但严格来说,这是在vptr之后,因为它总是在MS ABI的多态类中排在第一位(我假设)。
然而,当我们到达
ImageFactory::DebugPrintDimensions((ImageFactory::CustomImage*)image);
编译器看到一个 C 样式的转换从一个它一无所知的类到另一个类。它所做的只是获取它拥有的地址并假装它是一个CustomImage*
。此地址实际上指向自定义映像中的Image
;但是由于Image
是空的,并且可能空基类优化是有效的,所以它最终指向CustomImage
内的第一个成员,即unique_ptr
。
现在,ImageFactory::DebugPrintDimensions()
假设它被交给了一个指向完全完成CustomImage
的指针,因此地址等于vptr
的地址。但它没有 - 它被交给了unique_ptr
的地址,因为在调用它的时候,编译器不知道更好。所以现在它取消了它认为的vptr(实际上是我们控制的数据)的引用,寻找虚函数的偏移量并盲目地解释它 - 现在我们遇到了麻烦。
有几件事可以帮助缓解这种情况。首先,由于我们通过基类指针操作派生类,因此Image
应该有一个虚拟析构函数。这将使Image
多态性,并且很可能我们不会有问题(我们也不会泄漏内存)。
其次,因为我们是从基到派生的强制转换,所以应该使用dynamic_cast
而不是 C 样式转换,这将涉及运行时检查和正确的指针调整。
最后,如果编译器在编译时掌握了所有信息main()
,它可能会警告我们(或正确执行强制转换,调整CustomImage
的多态性)。因此,也建议将类定义移到main()
上面。
据推测,内存布局使得 vptr 位于基本子对象之前,如下所示:
class CustomImage {
void * __vptr;
Image __base; // empty
unique_ptr<whatever> evil;
};
这意味着从Image*
到CustomImage*
的有效转换需要从指针中减去几个字节。但是,您发布的邪恶演员表在类定义之前,因此它不知道如何正确调整指针。相反,它的作用就像reinterpret_cast
,只是假装指针指向CustomImage
而不调整其值。
现在,由于基类为空,unique_ptr
内的指针将被误解为 vptr。这指向另一个指针,该指针将被误解为 vtable 指向第一个虚拟成员函数的指针。这反过来指向从文件加载的数据,这些数据将在调用该虚拟函数时作为代码执行。作为锦上添花,内存保护标志是从文件中加载的,并且不会进行调整以防止执行。
这里的一些教训是:
- 避免使用 C 样式转换,尤其是在指针或引用类型上。如果转换无效,它们会回退到
reinterpret_cast
,导致未定义行为的雷区。(雪上加霜的是,它们的语法也是不可挽回的,如果你不太仔细地阅读代码,很容易错过。 - 避免使用非多态基类。除了在概念上可疑,并使删除更加尴尬和容易出错之外,正如我们在这里看到的那样,它可以对内存布局造成令人惊讶的事情。如果基类是多态的,我们可以使用
dynamic_cast
(或通过提供合适的虚函数来完全避免强制转换),而不可能进行无效转换。 - 避免不必要的间接级别 -
m_imageData
没有特别需要成为指针。 - 切勿将用户数据放入可执行内存中。
- 何时为派生类初始化 vptr?
- 了解虚拟继承类 vtables 和 vptr 创建
- 为什么带有 vptr 的对象长 12 个字节?
- 如果在创建对象时创建了 VPTR,那么为什么具有虚拟功能的类的大小在 32 位系统上为 4,在 64 位机器上为 8
- 通过 VPTR 调用虚函数
- C++中的 vptr(虚拟指针)类型
- vptr 和 vtable 在下面的虚拟相关代码中是如何工作的?
- 如果派生类还具有基类中不存在的虚函数,则创建的 vptr 数
- 64位计算机上的VPTR的大小是否为64位
- 如何在编译时初始化 vptr
- 如何在C 汇编代码中找到VPTR
- 其中,在只有参数化构造函数的类中初始化的 VPTR(虚拟指针)
- 多重继承的派生类中的Vptr
- C++铸造到基础和"overwriting" VPTR 问题
- vptr 是否位于对象的开头
- vptr名称在c++中已损坏
- 当基类私有派生时,编译器插入的vptr是否变为私有
- 在多重继承的情况下选择 VPTR
- 是否有可能在构造函数和析构函数之外修改“vptr”
- 从 Objdump 实用程序检索 vptr(指向虚拟表又名 VTABLE 的指针)