简介
LLVM 的安全堆栈功能是一种编译器模式,旨在强化生成的代码,以防范堆栈粉碎攻击(例如利用缓冲区溢出 bug 攻击)。
上述链接的 Clang/LLVM 文档页面介绍了一般架构。胶囊摘要是,每个线程都有两个堆栈,而不是通常的堆栈:“安全堆栈”和“不安全堆栈”。不安全堆栈可用于可能会使用堆栈内存的指针的所有用途,而安全堆栈仅用于任何代码都不应看到指向堆栈内存的指针的用途。因此,不安全的堆栈用于通过引用其他函数传递或将其地址存储在堆中的数组或变量。堆(可能发生缓冲区溢出或释放后使用 bug 及其漏洞的内存)中。安全堆栈用于处理编译器的寄存器溢出,以及函数调用的返回地址。因此,例如,简单的缓冲区溢出 bug 不能被利用来覆盖包含函数的返回地址,这是使用所谓的 ROP(“面向返回编程”)技术进行漏洞和攻击的基础。
该页面的兼容性部分不适用于 Zircon(或紫红色)。在 Zircon 用户模式代码(包括所有 Fuchsia)中,对 SafeStack 的运行时支持直接包含在标准 C 运行时库中,并且在共享库 (DSO) 中正常运行。
safe-stack 和 shadow-call-stack 插桩方案和 ABI 相关且相似,但也正交。您可以针对任何函数单独启用或停用每个参数。无论特定的 libc build 中使用或未使用什么插桩,Fuchsia 的编译器 ABI 和 libc 始终与使用或不使用这两种插桩构建的代码进行互操作。
互操作和 ABI 影响
一般来说,安全堆栈不会影响 ABI。特定于机器的调用约定保持不变。在使用安全堆栈构建的程序中,有些函数可以放置,而有些函数则不是。两者是来自直接编译的 .o
文件、来自归档库(.a
文件)还是来自共享库(.so
文件)的任意组合,因此两者的组合并不重要。
虽然每个线程还有一些额外的状态(不安全的堆栈指针,请参阅下文的实现详情下的内容),但不使用安全堆栈的代码不需要对该状态执行任何操作,以便在调用使用安全堆栈的代码或被使用安全堆栈的代码调用时保持正确的状态。唯一可能的例外情况是实现自身种类的非本地退出或上下文切换(例如协程)的代码。Zircon C 库的 setjmp
/longjmp
代码会自动保存和恢复此附加状态,因此,任何基于 longjmp
的内容都会正确处理所有内容,即使调用 setjmp
和 longjmp
的代码不知道安全堆栈也是如此。
在 Zircon 和 Fuchsia 中使用
这是通过 -fsanitize=safe-stack
命令行选项在 Clang 编译器中启用的。这是 *-fuchsia
目标的编译器的默认模式。如需针对特定编译停用此功能,请使用 -fno-sanitize=safe-stack
选项。
Zircon 支持为用户模式和内核代码使用安全堆栈。在 x86 Zircon build 中,使用 Clang 构建时始终启用安全堆栈(将 variants = [ "clang" ]
传递给 GN
)。
实现细节
为了支持安全堆栈代码,我们补充了不安全的堆栈指针。在抽象中,可以将其视为额外的寄存器,就像机器的普通堆栈指针寄存器。机器的堆栈指针寄存器用于安全堆栈,就像一直以来一样。不安全的堆栈指针被用作 ABI 中具有固定用途的另一个寄存器,但当然,机器实际上并没有新的寄存器,并且安全堆栈不会更改分配给所有机器寄存器的基本计算机专用调用规范。
Zircon 和 Fuchsia 的 C 和 C++ ABI 将不安全的堆栈指针存储在内存中,该指针位于相对于线程指针的固定偏移量处。<zircon/tls.h>
头文件会定义每台机器的偏移量。
对于 x86 用户模式,线程指针是 fsbase
,这意味着在汇编代码中访问的行为类似于 %fs:ZX_TLS_UNSAFE_SP_OFFSET
。对于 x86 内核,线程指针是 gsbase
,这意味着在汇编代码中进行的访问类似于 %gs:ZX_TLS_UNSAFE_SP_OFFSET
。
对于 Aarch64 (ARM64),在 C 或 C++ 代码中,__builtin_thread_pointer()
会返回线程指针。在用户模式下,线程指针位于 TPIDR_EL0
特殊寄存器中,必须(通过 mrs *reg*, TPIDR_EL0
)提取到普通寄存器中才能访问内存,因此它不是汇编代码中的单指令。在内核中,它与此相同,但使用的是 TPIDR_EL1
特殊寄存器。
低级别代码和汇编代码注意事项
大多数代码,即使是在汇编中,也完全不需要考虑安全堆栈问题。调用规范没有改变。无论是否使用安全堆栈,使用堆栈保存寄存器、查找返回地址等都完全相同。主要的例外情况是实现非本地退出或上下文切换等功能的代码。此类代码可能需要保存或恢复不安全的堆栈指针。longjmp
函数和 C++ throw
都已直接处理此事务,因此使用这些结构的 C 或 C++ 代码无需执行任何新操作。
内核中的上下文切换代码负责处理不安全的堆栈指针的切换。在 x86 上,这在代码中是显式的:%gs
指向 struct x86_percpu
,其成员 kernel_unsafe_sp
位于 ZX_TLS_UNSAFE_SP_OFFSET
;arch_context_switch
会将其复制到旧线程 struct arch_thread
的 unsafe_sp
字段中,然后将新线程的 unsafe_sp
复制到 kernel_unsafe_sp
中。在 ARM64 上,此操作由 set_current_thread
隐式完成,因为这会更改 TPIDR_EL1
特殊寄存器,该寄存器直接指向每个线程的 struct thread
,而不是像 x86 上那样的基于 CPU 的结构。
实现某种新型非局部退出或上下文切换的新代码将需要像处理传统机器堆栈指针寄存器一样处理不安全的堆栈指针。任何此类代码都应在编译时使用 #if __has_feature(safe_stack)
来测试特定 build 中是否使用了安全堆栈。该预处理器结构可以在 C、C++ 或汇编 (.S
) 源文件中使用。