为返回地址和函数参数使用单独的堆栈是一种可行的安全措施吗?

信息安全 开发 缓冲区溢出
2021-08-27 05:43:07

据我所知,许多漏洞利用依赖于覆盖他们试图利用的函数的返回地址。他们通过缓冲区溢出来做到这一点。但是,如果编译器在地址空间中设置了两个彼此相距很远的独立堆栈,并使用其中一个(可能是系统堆栈,如x86* 系统上的esp/驱动堆栈)作为返回地址,而另一个用于传递函数参数,那会怎样? rsp? 在这种情况下,任何缓冲区溢出都会覆盖一些局部变量,可能包括调用函数的局部变量,但仍会保持返回地址不变。

此外,返回地址的堆栈可用于存储允许间接跳转/调用的所有函数指针和类似实体,因此它们也将受到保护。

是否已经考虑过这个选项?可行吗?如果是,为什么没有实施(或已经实施?)?

2个回答

大多数操作系统的问题在于它们遵循特定的“调用约定”。此约定要求将函数参数放在堆栈上,这是 C 风格调用约定的某种派生。必须使用此约定以实现与该操作系统的 ABI(应用程序二进制接口)兼容性。因此,如果没有操作系统支持,您只能将此功能用于在应用程序中进行的调用。

这会使编译器相当复杂,并且可能需要大量的工作。简而言之,如果你有一个支持这种调用约定的编译器,你可以保护你自己的程序,但是当你必须做诸如读/写文件等事情时,你仍然会受到操作系统的支配。缓冲区溢出例如,在 DLL 中,不能通过更改调用约定来修复。

其次,直到最近,随着虚拟化的出现,像这样建立一个单独的区域确实是不可行的,因为分段很昂贵,而内存虚拟化更是如此。今天,这实际上不是问题,但由于我们必须处理历史软件(例如十年前编写的仍然需要传统调用方法的东西),操作系统将被迫在一段时间内无限期支持这两种模型的时间。

如果编写了一个没有兼容性问题的操作系统,它当然可以这样做,但它可能不会发生,因为有更多可行的方法。微软自己的Singularity操作系统完全不受缓冲区溢出的影响(根据他们的说法),因为操作系统静态地验证每个程序不可能行为不端。有趣的是,该操作系统不使用 Windows、Linux、Mac OS 等所使用的“内存保护”。程序在运行之前经过验证是否正确行为,而不是在运行时当然,如果病毒能够感染这个系统,那么由于缺乏硬件级别的保护,它将拥有无限的系统控制权。

简而言之,即使没有对该主题进行任何认真的研究,这种方法显然也有可能奏效,但在 FOSS(免费和开源软件)之外,从财务角度来看,这种方法不可能奏效。Linux 可以从内核向上重写以支持新模型,为其推出一个编译器,然后可以重新编译那里的所有软件而无需太多努力(注意:“太多”相对于,比如说,微软)。

微软、苹果等没有这个好处,因为代码已经被数百万不同的开发者编译,所以任何无法更新的东西都会立即过时,或者他们必须编写模拟器来支持旧的软件。基本上,直到有人想出一个内置此功能的操作系统,并具有兼容的编译器(至少 C 和 C++,可能还有 Cocoa 和 Win32 C++),并且它获得了足够的支持以成为与 Linux、Microsoft 和Mac OS,很难证明转向新模型的合理性。Linux 将是最容易转移的,尽管所有软件都必须编译,直到 RPM 和其他包类型支持新的调用模型。

最后,DEP(数据执行保护)在大多数情况下几乎解决了这个问题,使得执行不应执行的代码变得更加困难。这也减少了切换到新模型的需要,尽管正如 Singularity 所展示的那样,如果硬件不经常被迫保护免受程序员的错误和他们提出的漏洞利用,它可能会快得多。

是的,这已经在之前实施过。这篇博文中,Erin Ptacek 简要提到了 AVR 如何具有不同的程序和数据存储器,以及这如何使利用变得更加困难。

哈佛建筑有两个不同的记忆;有程序存储器(imem,通常是闪存)和数据存储器(dmem,通常是 SRAM)。他们生活在两个不同的地址空间中。CPU 从 imem 读取指令,但指令本身读取和写入 dmem。这很简洁,因为使漏洞更难编写。您不能简单地将代码上传到缓冲区并以某种方式将 PC 指向它;该缓冲区位于 dmem 中,而不是 imem 中。几周后你就会明白我的意思了。

虽然这有助于防止攻击者利用缓冲区溢出,但并不能完全防止缓冲区溢出被利用。覆盖局部变量对于获得对进程的控制大有帮助。Phyrfox 还提出了很好的观点,说明为什么这不符合现代操作系统的期望。