指向多态类的悬空指针会导致未定义行为。是否真的可能成为任何想象得到的损坏的源头?

10

我知道一旦发生未定义行为,就不可能再考虑代码了。我完全相信这一点。我甚至认为我不应该过于深入理解未定义行为:一个健全的 C++ 程序不应该涉及未定义行为,就这样。

但是为了让我的同事和经理们确信它的真正危险性,我试图找到一个具体的例子,使用我们产品中存在的错误(他们认为这不是危险的,最多只会因为访问冲突而崩溃)。


我的主要关注点是在指向多态类的悬空指针上调用虚成员函数。

当一个指针被删除时,Windows 操作系统会在堆块的头部写入几个字节,并且通常也会覆盖堆块本身的前几个字节。这是它跟踪堆块,将它们管理为链表的方式...操作系统相关的内容。

尽管这在 C++ 标准中没有定义,但据我所知,多态是使用虚表来实现的。 在 Windows 下,指向虚表的指针位于堆块的前几个字节中,给定一个仅继承一个基类的类。(对于多重继承可能更为复杂,但我不会考虑这个。我们只考虑一个继承了 A 的类以及几个继承了 A 的 B、C、D 类。)


现在假设我有一个指向 A 的指针,它被实例化为 D 对象。并且该 D 对象已经在代码的其他位置被删除:因此堆块现在是一个自由堆块,其首字节已经被覆盖,因此虚表指针几乎随机地指向内存中的某个位置,比如说地址 0x01234567

当我们在代码中的某个地方调用:

void test(A * pA)   
{
    # here we do not know that pA is dangling pointer
    # that memory address has been deleted by another thread, in another part of the code
    pA->SomeVirtualFunction();
}

我说得对吗:

  • 运行时将会把内存地址0x01234567的内容解释为虚表。
  • 如果错误地把这个内存地址解释成虚表,但没有到达禁止访问的内存区域,就不会发生任何访问违规。
  • 被误解的虚表将为虚函数提供一个随机地址,比如说0x09876543
  • 随机地址0x09876543处的内存将被解释为有效的二进制代码,并且真正地被执行。
  • 这可能导致任何想象得到的损坏。

我不想夸大其词,只是想说明情况。所以,我的说法正确、可能和有可能发生吗?


严格来说,未定义行为可能会触发编译器喜欢的任何行为。实际上,大多数编译器仍会生成大致确定性的代码,因此如果您的程序通过了测试,那么在更改编译器(或版本)之前,您可能是安全的。例如,有符号整数溢出是未定义行为,但可能是无害的。从一个“工作”的产品中删除所有UB可能代价太高。尽管如此,您在技术上是正确的,这是最好的正确方式。祝你好运。 - Jon Chesterfield
5
就安全性而言,使用已释放内存漏洞(use-after-free bugs)可能是仅次于缓冲区溢出漏洞易于利用的第二种漏洞类型。虚函数表使攻击变得更加容易。攻击者可以基本上将某些内容强制分配到先前的内存空间中,在那里编写一些攻击代码,然后触发你程序的漏洞以控制该程序。嘭——你就被黑客攻击了。 - nneonneo
2
如果在同一位置分配了另一个对象,那么您将通过该“其他对象”的虚函数表进行调用,这基本上意味着调用具有随机参数的随机函数。 - Raymond Chen
1个回答

3
您的例子是一种可能性。
然而,情况要严重得多。如果有人攻击您的应用程序用户,则内存将不包含随机数据。攻击者将尝试并可能成功地影响这些数据。一旦发生这种情况,攻击者可能能够确定将要执行的代码。除非您的应用程序已经正确地隔离(我敢打赌您的联合开发者很可能没有意识到这点),否则攻击者可能能够接管用户的计算机。
这不是一种假设的可能性,而是已经发生过,并将再次发生的现实。

目前我最关心的是未经解释的损坏问题。我认为安全可能会稍后考虑。 - Stephane Rolland
1
如果恶意接管是可能的话,那么任何形式的内存损坏也是可能的。这包括会导致应用程序崩溃的尖叫式内存损坏和“只是”会导致错误结果的邪恶无声形式。试想一下,如果悬空指针的内存已经被重用于另一个多态对象,虚函数调用很可能会解析为该新对象的成员函数,从而获得随机的参数。该成员函数可能会以代码实际使用对象不会预期的方式改变对象的状态。疯狂开始... - cmaster - reinstate monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接