编译器如何检测缓冲区溢出?

信息安全 缓冲区溢出 系统妥协 编译器
2021-08-19 09:30:47

我刚开始研究系统级别的安全性和挑战,特别是对于 C/C++ 和 Objective-C 等低级语言。我已经了解缓冲区溢出及其工作原理。我在 OS X 和 Ubuntu 上玩弄它。当然,这些系统实现了 ASLR 和堆栈保护,我们可以在编译时禁用它们。所以我对此有几个问题:

  1. 编译器如何在编译时检测溢出?我知道在编译时它会添加一个金丝雀,如果有任何指令试图覆盖它,它会抛出一个错误。但具体的算法是什么?如果有人可以为此指出 gcc 代码功能,那就太好了。

  2. 禁用堆栈保护并使堆栈/堆在 Ubuntu 上可执行就足够了吗?或者 ASLR 仍然可以使其难以被利用吗?

  3. 如果我有一个二进制文件(没有源代码)并且我知道它由于缓冲区溢出而崩溃,我该如何检测它?

2个回答

在编译时未检测到缓冲区溢出。有一些代码分析工具,例如SparseLint ( cpplintpc-lint ),它们将对源代码文件或编译的二进制文件执行进一步的分析。每个分析工具都有自己的算法来确定缓冲区溢出,但它归结为导致缓冲区溢出的常见已知指令。

您还可以在编译时添加边界检查,为每个分配的内存块插入边界信息。然后在运行时检查此边界信息以确保缓冲区在其限制范围内。一个常见的实现是使用“胖”指针。其中既包含指向数据的真实指针的地址,也包含描述其所在区域的附加数据。我相信 Firefox 这样做是为了分配内存,但我可能弄错了。

在编译时插入金丝雀,通过word在缓冲区和堆栈上的控制数据之间插入数据来帮助检测缓冲区溢出。在函数返回之前的某个时刻,金丝雀被验证为完好无损。


ASLR 与堆栈保护无关。它随机化程序在内存中运行的地址。这意味着您不能依赖函数在每次运行程序时都位于同一地址。这可以防止库和函数地址的硬编码。无论如何,使堆栈或堆(或您尝试执行的任何内存)可执行是必要的,但 ASLR 仍然会给您带来问题。如果您只是在试验并试图理解漏洞利用代码,我会执行以下操作:

  1. 禁用 ASLR
  2. 禁用堆栈保护
  3. 单独攻击每个问题,然后一次又一次地重新启用它们,直到您的漏洞利用可以同时处理这两个问题。

如果您的二进制文件由于......以及任何事情(不仅仅是缓冲区溢出)而崩溃,请在调试器中运行该程序。调试器将在程序失败的确切点捕获崩溃。您可能应该意识到这可能不是溢出的确切位置,但堆栈跟踪应该有助于确定根本原因。如果您不熟悉逆向工程工具或 x86 计算机体系结构,我建议您阅读这些帖子。

为什么缓冲区溢出会按照它们所在的方向执行?
逆向工程工具

考虑以下代码

void f1()
{
     char buf[20];
     strcpy(buf,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // The buffer overrun
} 

void f2() 
{
    ....
    f1(); 
    (address of this place will be pushed onto to the stack 
     as the return address before calling f1)
    ...
}

f2调用f1时,默认情况下,返回地址(即调用f1完成后必须返回的地址)被压入堆栈。

通常,当缓冲区 buf 在 中溢出时f1,会通过覆盖堆栈上的返回地址来利用它,以便在f1完成后代码返回到攻击者控制可以执行脚本或执行漏洞利用的东西的数据的某个地方。

Microsoft 编译器以下列方式实施保护。
当函数加载时,它会在缓冲区的末尾和堆栈中存储返回地址的位置之间放置一个安全 cookie(一个随机值)。编译器还在函数末尾添加一些代码f1来检查安全 cookie 是否与放置的相同。
因此,假设存在缓冲区溢出,缓冲区溢出用于更改缓冲区结束后的所有内容,包括返回地址。这意味着安全 cookie 也被覆盖了。当函数f1完成执行时,添加在函数末尾的代码将检测到这一点并关闭程序——这意味着漏洞利用(攻击者想要发生的代码运行)将不会运行。

这里有更详细的解释