SafeStack

简介

SafeStack 是一个检测过程,它通过在不引入任何可衡量性能开销的情况下保护程序免受基于堆栈缓冲区溢出的攻击来实现的。它通过将程序堆栈分成两个不同的区域来工作:安全堆栈和不安全堆栈。安全堆栈存储返回地址、寄存器溢出和始终以安全方式访问的局部变量,而不安全堆栈存储所有其他内容。这种分离确保了不安全堆栈上的缓冲区溢出不能用于覆盖安全堆栈上的任何内容。

SafeStack 是 代码指针完整性 (CPI) 项目 的一部分。

性能

SafeStack 检测的性能开销在各种基准测试中平均少于 0.1%(有关详细信息,请参见 代码指针完整性 论文)。这主要是因为大多数小函数没有任何需要不安全堆栈的变量,因此不需要创建不安全堆栈帧。创建不安全堆栈帧的大函数的成本由执行函数的成本分摊。

在某些情况下,SafeStack 实际上提高了性能。最终移到不安全堆栈的对象通常是通过多个堆栈帧使用的较大数组或变量。将此类对象从安全堆栈中移出将提高堆栈上频繁访问的值(如寄存器溢出、返回地址和小局部变量)的局部性。

兼容性

大多数程序、静态库或单个文件都可以按原样使用 SafeStack 进行编译。SafeStack 需要基本运行时支持,在大多数平台上,它作为编译器运行时库实现,在使用 SafeStack 编译程序时会自动链接进来。

目前不支持使用 SafeStack 链接 DSO。

已知兼容性限制

某些依赖于底层堆栈操作的代码需要进行调整才能与 SafeStack 一起使用。一个例子是 C/C++ 的标记和清除垃圾收集实现(例如,chromium/blink 中的 Oilpan),它必须更改为在安全堆栈和不安全堆栈上查找活动指针。

SafeStack 支持静态链接使用和不使用 SafeStack 编译的模块。使用 SafeStack 编译的可执行文件可以加载未使用 SafeStack 编译的动态库。目前,不支持使用 SafeStack 编译动态库。

使用 sigaltstack() 的信号处理程序不能使用不安全堆栈(请参阅以下的 __attribute__((no_sanitize("safe-stack"))))。

目前不支持使用 ucontext.h 中的 API 的程序。

安全性

SafeStack 通过将它们分离到专用的安全堆栈区域来保护始终以安全方式访问的返回地址、溢出寄存器和局部变量。安全堆栈会自动受到基于堆栈的缓冲区溢出的保护,因为它在内存中与不安全堆栈分离,并且它本身始终以安全的方式访问。在当前实现中,安全堆栈通过随机化和信息隐藏来抵御任意内存写入漏洞:安全堆栈在随机地址分配,检测过程确保永远不会在安全堆栈本身之外存储指向安全堆栈的指针(请参阅以下限制)。

已知安全限制

完全防止控制流劫持攻击需要将 SafeStack 与另一种机制结合使用,该机制强制执行存储在堆或不安全堆栈上的代码指针的完整性,例如 CPI,或在间接调用站点强制执行正确调用约定的前向边缘控制流完整性机制,例如带有基数检查的 IFCC。Clang 对 C++ 虚拟调用 具有控制流完整性保护方案,但没有非虚拟间接调用。仅使用 SafeStack,攻击者可以覆盖堆或不安全堆栈上的函数指针并导致程序调用任意位置,这反过来可能会启用堆栈枢纽和面向返回的编程。

在当前实现中,SafeStack 提供了对基于堆栈的缓冲区溢出的精确保护,但对任意内存写入漏洞的保护是概率性的,并依赖于随机化和信息隐藏。随机化目前基于系统强制执行的 ASLR,并共享其已知的安全限制。安全堆栈指针隐藏也不完美:系统库函数(如 swapcontext)、异常处理机制、内联函数(如 __builtin_frame_address)或运行时支持中的低级错误可能会泄露安全堆栈指针。将来,可以使用静态或动态分析工具检测此类泄漏,并通过调整此类函数来防止它,方法是将堆栈指针存储在堆中时对其进行加密(如 glibc 中的 setjmp/longjmp 实现已经完成),或者将其存储在安全区域中。

CPI 论文 描述了两种替代的、更强大的安全堆栈保护机制,它们依赖于软件故障隔离或硬件分段(如 x86-32 和某些 x86-64 CPU 上可用)。

目前,SafeStack 假设编译器的实现是正确的。除了通过手动代码检查之外,这一点尚未得到验证,并且将来可能会出现回归。因此,最好有一个单独的静态或动态二进制验证工具来检查最终二进制文件中 SafeStack 检测的正确性。

用法

要启用 SafeStack,只需将 -fsanitize=safe-stack 标志传递给编译和链接命令行。

支持的平台

SafeStack 在 Linux、NetBSD、FreeBSD 和 macOS 上进行了测试。

底层 API

__has_feature(safe_stack)

在一些罕见的情况下,可能需要根据是否启用了 SafeStack 来执行不同的代码。可以使用 __has_feature(safe_stack) 宏来实现此目的。

#if __has_feature(safe_stack)
// code that builds only under SafeStack
#endif

__attribute__((no_sanitize("safe-stack")))

在函数声明上使用 __attribute__((no_sanitize("safe-stack"))) 来指定即使在全局启用了 SafeStack 检测(请参阅 -fsanitize=safe-stack 标志)也不应将该函数应用于该函数。对于对堆栈帧的精确布局有假设的函数,可能需要此属性。

使用此属性的函数中的所有局部变量都将存储在安全堆栈上。在访问这些变量时,安全堆栈仍然不受内存错误的保护,因此必须格外小心以手动确保所有此类访问都是安全的。此外,此类局部变量的地址永远不应该存储在堆上,因为它会泄露 SafeStack 的位置。

__builtin___get_unsafe_stack_ptr()

此内联函数返回当前线程的当前不安全堆栈指针。

__builtin___get_unsafe_stack_bottom()

此内联函数返回指向当前线程的不安全堆栈底部的指针。

__builtin___get_unsafe_stack_top()

此内联函数返回指向当前线程的不安全堆栈顶部的指针。

__builtin___get_unsafe_stack_start()

已弃用:此内联函数是 __builtin___get_unsafe_stack_bottom() 的别名。

设计

有关 SafeStack 及其相关技术的设计的更多信息,请参阅 代码指针完整性 项目页面。

setjmp 和异常处理

OSDI'14 论文 指出,在 Linux 上,检测过程会查找对 setjmp 的调用或可能引发异常的函数,并在其调用站点插入必要的检测。具体来说,检测过程在调用站点之前将影子堆栈指针保存到安全堆栈上,并在对 setjmp 的调用之后或捕获异常之后将其恢复。这是在 SafeStack::createStackRestorePoints 函数中实现的。

出版物

代码指针完整性. Volodymyr Kuznetsov, Laszlo Szekeres, Mathias Payer, George Candea, R. Sekar, Dawn Song. USENIX 操作系统设计与实现研讨会 (OSDI), 科罗拉多州布鲁姆菲尔德,2014 年 10 月