LLVM笔记(6) - CompilerRT之safestack

好久没更新博客了, 最近调研安全编译选项(各类sanitizer), 抽空做个笔记. 本来想系统的分析一下compiler-rt代码, 但是最近实在太懒了, 所以先介绍最简单的安全栈safestack, 之后有空再补上compiler-rt框架以及其它sanitizer工具.

1. 什么是safestack

safestack是Code Pointer Integrity (CPI) Project的部分实现. CPI(代码指针完整性)是为了阻止控制流劫持攻击而提出的一种通过保证代码指针安全性的设计, 关于CPI的具体内容可以参考论文官网. safestack是CPI的一个组件, 但也可以单独使用(用于防止基于栈的控制流攻击), 它通过将栈分为两个独立区域, safe stack(用于存储函数返回地址, 寄存器spill, 保证安全访问的局部变量)和unsafe stack(其它存储在栈上的内容)来保证即使栈空间溢出也不会影响到程序流执行(链接地址不会被覆写).

2. 一个基于栈攻击的例子

目前CPI并没有完整的实现, 其preview版本可以通过源码下载. 但safestack已作为compiler-rt的一部分整合在LLVM工程中, 通过-fsanitize=safe-stack选项可以开启该特性.
以下是一个简单的示例, test()函数中栈空间被改写导致程序流没有正常返回, 而是进入hihack().

[21:32:13] [email protected]:~/llvm-mono (master)$ cat ~/1.c
#include <stdio.h>
void hijack() {
  puts("stack hijack\n");
}
int test() {
  long a = 0;
  long *pa = &a;
  *(pa + 2) = hijack;
  return a;
}
int main() {
  return test();
}
[21:41:41] [email protected]:~/llvm-mono (master)$ ./llvm_install/bin/clang ~/1.c -w && ./llvm_install/bin/llvm-objdump -D a.out > a.s
[21:41:44] [email protected]:~/llvm-mono (master)$ grep test\: a.s -A 15
00000000004004f0 test:
  4004f0: 55                            pushq   %rbp
  4004f1: 48 89 e5                      movq    %rsp, %rbp
  4004f4: 48 c7 45 f8 00 00 00 00       movq    $0, -8(%rbp)
  4004fc: 48 8d 45 f8                   leaq    -8(%rbp), %rax
  400500: 48 89 45 f0                   movq    %rax, -16(%rbp)
  400504: 48 8b 45 f0                   movq    -16(%rbp), %rax
  400508: 48 b9 d0 04 40 00 00 00 00 00 movabsq $4195536, %rcx
  400512: 48 89 48 10                   movq    %rcx, 16(%rax)
  400516: 48 8b 45 f8                   movq    -8(%rbp), %rax
  40051a: 5d                            popq    %rbp
  40051b: c3                            retq
  40051c: 0f 1f 40 00                   nopl    (%rax)
[21:52:04] [email protected]:~/llvm-mono (master)$ ./a.out 
stack hijack

段错误 (核心已转储)

通过反汇编test()可以看到程序的栈地址从高到低依次为:
---------------- top
link addr (push by hardware)
frame pointer (%rbp)
var a (%rbp - 8)
var pa (%rbp - 16)
---------------- bottom
因此修改(pa + 2)地址正好覆写硬件压栈的地址, 当执行retq以后程序跳转至hijack(), 程序流被改写. 现在来看看使用safestack后会怎样.

[22:02:03] [email protected]:~/llvm-mono (master)$ ./llvm_install/bin/clang ~/1.c -w -fsanitize=safe-stack && ./llvm_install/bin/llvm-objdump -D a.out > a.s
[22:02:22] [email protected]:~/llvm-mono (master)$ grep test\: a.s -A 25
0000000000401860 test:
  401860: 55                            pushq   %rbp
  401861: 48 89 e5                      movq    %rsp, %rbp
  401864: 48 8b 05 75 17 20 00          movq    2103157(%rip), %rax
  40186b: 64 48 8b 08                   movq    %fs:(%rax), %rcx
  40186f: 48 89 ca                      movq    %rcx, %rdx
  401872: 48 83 c2 f0                   addq    $-16, %rdx
  401876: 64 48 89 10                   movq    %rdx, %fs:(%rax)
  40187a: 48 89 ca                      movq    %rcx, %rdx
  40187d: 48 83 c2 f8                   addq    $-8, %rdx
  401881: 48 c7 41 f8 00 00 00 00       movq    $0, -8(%rcx)
  401889: 48 89 55 f8                   movq    %rdx, -8(%rbp)
  40188d: 48 8b 55 f8                   movq    -8(%rbp), %rdx
  401891: 48 c7 42 10 40 18 40 00       movq    $4200512, 16(%rdx)
  401899: 8b 71 f8                      movl    -8(%rcx), %esi
  40189c: 64 48 89 08                   movq    %rcx, %fs:(%rax)
  4018a0: 89 f0                         movl    %esi, %eax
  4018a2: 5d                            popq    %rbp
  4018a3: c3                            retq
  4018a4: 66 2e 0f 1f 84 00 00 00 00 00 nopw    %cs:(%rax,%rax)
  4018ae: 66 90                         nop

00000000004018b0 main:
  4018b0: 55                            pushq   %rbp
  4018b1: 48 89 e5                      movq    %rsp, %rbp
  4018b4: 48 83 ec 10                   subq    $16, %rsp
[22:02:27] [email protected]:~/llvm-mono (master)$ ./a.out 
段错误 (核心已转储)

改写后的汇编存储空间发生变化:
---------------- top
link addr (push by hardware)
frame pointer (%rbp)
var pa (%rbp - 8)
---------------- bottom
同时pa中存储的地址是(%fs:(%rax) - 8), 因此hijack()函数地址被存储到(%fs:(%rax) + 8)而非返回地址. 在后文中会详细解释这个特殊地址的由来, 为何变量a及hijack()地址被存到该地址开始的区域, 以及虽然控制流未被改写但是为何仍然core dump(safestack的局限性).

3. 安全栈与栈金丝雀的优劣比较

栈金丝雀(-fstack-protector)也是一个栈溢出保护特性, 其原理是在每个函数起始与结束处插桩. 在被调函数开辟栈空间前将生成的随机数作为guard存到栈上, 当被调函数返回前重新生成随机数并与guard比较, 若不相同则报错退出.
从原理上讲安全栈比栈金丝雀能够更全面防护基于栈溢出的攻击. 由于栈金丝雀是基于栈溢出是顺序覆写的假设, 并不能百分百保证程序流正确性. 而安全栈从理论上可以保证程序流不被改写(最极端的例子是原始栈只保存链接地址和压栈的参数).
从性能开销上安全栈也优于栈金丝雀, 前者几乎没有性能负载(0.05%), 后者我没有查询资料, 但就从实现来讲增加的指令是肯定多于前者的.

4. safestack的实现

作为compiler-rt的一个组件, safestack也分成两部分实现: 编译器插桩及compiler-rt支持.

5. 编译器插桩部分

当添加-fsanitize=safe-stack选项后, 编译器会为函数添加safestack属性. 如果不想为某个函数添加安全栈特性, 可以在函数声明时添加__attribute__((no_sanitize("safe-stack"))).
LLVM中实现名为SafeStackLegacyPass(lib/CodeGen/SafeStack.cpp). 这部分代码比较简单就不具体分析了, 简要列举下几个关键函数:
SafeStack::findInsts()收集了函数所有的alloca, return, call指令并判断是否需要放入unsafe stack(注意intrinsic并不会被收集, 因此包含修改内存的intrinsic的函数可能生成不正确的栈).
SafeStack::IsSafeStackAlloca()用于判断对一条alloca指令的所有访问是否永远是safe access的, 具体方法是DFS遍历所有use. 注意这里load与store的处理是不一致的, 对于load而言alloca指令只能是地址操作数, 所以只需判断访问是否越界, 而对于store而言其本身也能做立即数, 此时保守处理默认unsafe(这就是为什么上文中变量a处于unsafe stack而指针pa反而落在safe stack, 感兴趣的读者可以自己打印该pass前后的日志分析一下).
TargetLoweringBase::getSafeStackPointerLocation()是架构相关的hook, 用于返回unsafe stack地址, 其中的变量名与compiler-rt保持一致(定义在compiler-rt中), 对于不使用compiler-rt的情况则需自己提供实现, 移植代码时需要注意.
SafeStack::moveStaticAllocasToUnsafeStack()负责计算并分配unsafe stack空间.
SafeStack::createStackRestorePoints()用于longjmp/exception返回时恢复栈指针, 这块手边没有例子, 也没仔细看, 以后再补充.

6. compiler-rt部分

compiler-rt部分主要实现unsafe stack的内存分配, 栈地址的返回以及几个builtin函数, 代码见compiler-rt/lib/safestack.
先来看下builtin函数: __builtin__get_unsafe_stack_ptr() / __builtin__get_unsafe_stack_bottom()(另有废弃接口__builtin__get_unsafe_stack_start()) / __builtin__get_unsafe_stack_top()分别返回当前线程的unsafe stack的栈指针 / 栈底 / 栈顶.
compiler-rt中还定义了线程存储的全局变量unsafe_stack_start / unsafe_stack_size / unsafe_stack_guard, 其初始化见构造函数__safestack_init(). 可以看到unsafe stack实际是mmap映射出来的一块内存区域. 由于栈空间是线程独立的, 所以可以看到safestack.cpp还拦截了pthread_create(), 这块具体以后写sanitizer时候再分析吧.
最后一个问题, 为什么启用safe stack后仍然core dump了? 因为unsafe stack是mmap出来的区域, 在栈顶偏移访问了越界的地址(上文case太简单导致test调用时unsafe stack还是空的, 指针本来就指向栈顶, 再偏移就溢出了). 所以safe stack并不能100%保证程序正常运行, 只能保证不被hijack.

7. 移植safe stack

比移植ASAN简单多了, 编译器侧不用做修改(有需要可以修改上文提到的hook). 如果不启用compiler-rt需要定义hook中使用到的指针. 如果启用compiler-rt那肯定是基于linux或其它OS了, 什么都无需修改.

8. 与其它安全特性兼容

已经一点了, 以后有空再写吧...

猜你喜欢

转载自www.cnblogs.com/Five100Miles/p/11986686.html