clang之SafeStack

1,790 阅读7分钟

Clang 12 documentation

Clang 12 documentation包含了一系列工具,如 AddressSanitizerThreadSanitizerLeakSanitizerLibTooling等。

  1. clang之AddressSanitizer
  2. clang之MemorySanitizer
  3. clang之LeakSanitizer
  4. clang之UndefinedBehaviorSanitizer
  5. clang之Hardware-assisted-AddressSanitizer
  6. clang之SafeStack
  7. clang之ShadowCallStack
  8. clang之ThreadSanitizer
  9. clang之Thread-Safety-Analysis
  10. clang之DataFlowSanitizer

这部分是对clang文档 Clang 12 documentation SafeStack 的翻译。仅供参考。

介绍

SafeStack 是一个编译器插桩的 pass(编译器优化步骤),用于保护程序免受基于栈溢出(stack buffer overflows)的攻击,而不需要额外引入性能上的损耗。其工作原理是将程序的栈空间分成两部分独立的区域:安全的栈区和不安全的栈区。安全的栈区存储的是返回地址、register spills、本地变量等通常以安全方式访问的内存区域。而不安全的栈区则存储的是其他所有的栈相关内存。这种隔离能够确保在不安全栈区上的栈溢出不会写到安全栈区内的任何内存区域。

SafeStackCode-Pointer Integrity (CPI) 工程的一部分。

性能

SafeStack 编译器插桩带来的性能损耗非常微小,从大量的测评数据(见 Code-Pointer Integrity 工程的相关论文)来看,平均损耗不到0.1%。这主要是因为:大部分的小函数其实并没有必须要使用到不安全栈区的那些变量。并且,它们并不需要创建不安全栈区的栈帧。为大函数创建不安全栈帧的损耗,已经被函数执行的损耗均摊了。

在一些场景中,SafeStack实际却可以提升性能。移动至不安全栈区的对象,通常都是一些在多级栈帧中使用的大的数组或变量。将这些对象从安全栈区中移出后,能提升栈中的频繁访问对象的定位效率(increases the locality of frequently accessed values on the stack),如 register spills、返回地址、小的本地变量。

兼容性

大部分的程序、静态库、或独立的文件都可以使用SafeStack来编译。SafeStack要求基本的runtime支持,而这在大部分平台上,都是以编译器运行时库(compiler-rt library)的形式,在程序被SafeStack编译的时候,自动链接进来的。

目前不支持使用SafeStack来链接一个DSO(动态库?)。

已知的兼容性限制

一些依赖于底层栈操作的代码,需要做一些适配工作才能与SafeStack一起使用。例如C/C++(如Oilpan in chromium/blink)中的标记清除垃圾收集算法的实现,就必须做一些修改来查找安全栈和非安全栈中的存活指针。

SafeStack支持静态模块的链接,而不管这些模块是否使用SafeStack编译过。一个使用SafeStack编译的可执行文件,可以加载未使用SafeStack编译过的动态库。而使用SafeStack来编译动态库却不行。

使用 sigaltstack() 的信号处理的回调方法,不能使用非安全栈区(见下文的 attribute((no_sanitize("safe-stack"))) )。

使用 ucontext.h 中API的程序却不支持SafeStack。

安全

SafeStack通过将返回地址、溢出的寄存器和本地变量单独隔离到一个专门的安全栈区,使得他们始终以安全的方式被访问。正因为安全栈区与非安全栈区在内存上是相互隔离的,且它的访问方式始终是安全的,所以它能自动被保护起来,避免栈缓冲溢出的发生。在当前的实现中,安全栈区是通过地址随机化和信息隐藏来进行保护,避免内存写入产生的漏洞问题(arbitrary memory write vulnerabilities)的发生:安全栈区在一个随机内存地址上分配,且编译器插桩能确保除了安全栈区以外,没有其他地方会使用到该内存。(见下文的限制)。

已知的安全限制

针对控制流劫持攻击的全面保护,需要将SafeStack与另一个能强制保证堆区或者不安全栈区的代码指针的正确性的机制(如CPI)结合起来使用,或者使用一个前向控制流矫正机制在函数直接调用时对其强制进行纠正(如带有参数检查的IFCC)。Clang针对C++的关键调用有控制流矫正保护机制,但对于非关键的间接调用则没有。有了SafeStack,攻击者可以重写堆区或非安全栈区上的函数指针,使得程序访问未知内存地址,从而可能触发 stack pivoting and return-oriented programming。(英文原文:With SafeStack alone, an attacker can overwrite a function pointer on the heap or the unsafe stack and cause a program to call arbitrary location, which in turn might enable stack pivoting and return-oriented programming.)

在当前的实现中,SafeStack针对栈溢出提供了精确的保护措施,但对于错误写入操作引发的内存漏洞却并未完全保护,需要以来地址随机化和信息隐藏。当前,随机化是基于系统实现的ASLR(地址符号随机化),所以有同样的安全限制。安全栈区的指针隐藏也并不完美:系统库的函数如 swapcontext、错误处理机制、intrinsics such as __builtin_frame_address,或者运行时支持下的底层bug,这些都可能会导致安全栈的指针泄漏。以后,类似的泄漏可以被静态或动态的分析工具检测到,并且调整这些函数,主要方式有将栈指针存储于堆中的时候对其进行加密(如glibc中已经通过 setjmp/longjmp 来实现了),或者干脆将其存储在安全区域。

CPI论文描述了两个替代方案:依赖于软件故障隔离的更强的安全栈区保护机制,或硬件分割(目前在x86-32和部分x86-64的CPU上可用)。

此时,SafeStack会假设编译器的插桩操作都是正确的。这一点仅仅通过手动代码注入验证过了,未来可能随时推翻结论。然而,有一个独立的静态库或动态库验证工具,专用于验证SafeStack插桩操作在最终二进制中的正确性,确实是有必要的。

用法

通过在编译和链接的命令行中传递 -fsanitize=safe-stack 标记,即可开启 SafeStack

支持的平台

SafeStackLinux, NetBSD, FreeBSD and 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"))),可以指定该函数不会被使用到safe stack插桩,即便已经全局开启了SafeStack(见 -fsanitize=safe-stack 标记)。在调用函数需要采用严格的栈桢布局时,可能需要使用到该特性。

使用该特性的函数中的所有本地变量都会被存储到安全栈区。在访问这些变量时,安全栈区是不受保护的,可能受到内存错误的影响。(英文原文:The safe stack remains unprotected against memory errors when accessing these variables)。所以必须格外小心地手动确保所有的这种内存访问都是安全的。此外,这种本地变量的地址不应该存储到堆内存中,因为它可能会泄漏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 及相关技术的设计部分的更多信息,请参考 Code-Pointer Integrity 工程。

setjmp and exception handling

OSDI’14 的论文提到,在Linux系统中,编译器插桩pass发现调用 setjmp 或者函数可能会抛出异常,于是在它们的调用地方插入了必须的插桩代码。

比较特别的一点是,编译器插桩pass在函数调用前,将影子栈指针保存到安全栈区,在调用 setjmp 或者抛出异常后,再将其恢复。这是在函数 SafeStack::createStackRestorePoints 中实现的。

Publications

Code-Pointer Integrity. Volodymyr Kuznetsov, Laszlo Szekeres, Mathias Payer, George Candea, R. Sekar, Dawn Song. USENIX Symposium on Operating Systems Design and Implementation (OSDI), Broomfield, CO, October 2014