LLVM的SafeStack缓冲溢出保护的优缺点

来源:互联网 发布:点数图交易软件 编辑:程序博客网 时间:2024/04/30 10:07

作者:SamuelGroß
原文地址:http://blog.includesecurity.com/2015/11/LLVM-SafeStack-buffer-overflowprotection.html

引子

在2015年6月,一个能减少内存破坏,称为SafeStack的新模块,由Google的PeterCollingbourne整合进llvm的开发分支,在将要到来的3.8版发布中可以使用。SafeStack作为CodePointer Integrity(CPI)的部分开发,但也可以单独使用。在安全上我们喜欢呆在前头,因此本文目的在于讨论其内部的活动,着眼于未来的攻击使用StackStack安全上的好处,以及未来对它可能的改进。

果壳里的SafeStack

SafeStack是一个类似于Stack Cookies(但更强大)的保护模块。它通过将原生栈分为两个区域,尝试保护栈上的关键数据:一个安全栈,用于控制流信息以及仅以安全方式访问的数据(通过静态分析确定)。一个用于保存其他数据的非安全栈。这两个栈位于进程地址空间中不同的内存区域,因此防止了非安全栈的溢出破坏安全栈。

对常见的基于栈的内存破坏攻击,SafeStack保证一个普遍良好的保护,而且仅引入很少性能开销(根据文档,平均0.1%)。

在启动SafeStack时,栈指针寄存器(x86/x64分别是esp/rsp)将用于安全栈,而非安全栈由一个线程局部变量来追踪。在二进制文件初始化期间,通过映射一个可读写内存区,并在该区域前放置一个大概是为了捕捉非安全区域中的栈溢出的保护页面,来分配非安全栈。

SafeStack(目前)与Stack Cookies是不兼容的,在使用SafeStack时,要禁止StackCookies。

实现细节

SafeStack被实现为一个LLVM的方法遍(instrumentationpass),主要逻辑实现在lib/Transforms/Instrumentation/SafeStack.cpp方法遍在(原生)代码生成前作为最后的步骤运行。

更专业些:这个方法遍检查一个函数的中间表达(IR)中的所有alloca指令(clang实现把代码编译为llvm的中间表达,随后,在各个方法/优化遍之后,将IR翻译为机器代码)。

Alloca指令在栈上为局部变量或数组分配空间。SafeStack方法遍遍历使用这些变量的指令列表,并确定这些访问是否安全。如果任何访问被这个方法遍确定为“不安全”,这个alloca指令被在非安全栈上分配空间的代码所替换,相应更新使用这个变量的指令。

方法IsSafeStackAlloc负责决定一个栈变量是否以一个“不安全的”方式访问。当前“不安全”的定义是相当保守的:在以下情形里一个变量被重定位到非安全栈:

·        该变量的一个指针保存在内存的某处

·        以一个非常量索引(即另一个变量)访问一个数组的元素

·        访问一个可变长度数组(使用常量或非常量索引)

·        该变量的一个指针用作一个函数的实参

SafeStack负责分配及初始化非安全栈的运行时,可以在这里找到。正如前面提到的,非安全栈只是一个常规的映射区域。

探索SafeStack:实际应用

现在让我们看一个非常简单的例子来理解SafeStack在底下如何工作。对于我的测试,我跟着这个指引http://clang.llvm.org/get_started.html,从源代码编译clang/llvm。

我们将使用以下的C代码片段:

voidfunction(char*str) {

    char buffer[16];

    strcpy(buffer, str);

}

我们从看在没有使用栈保护时生成的汇编代码开始。对此我们使用clang-O1 example.c进行编译(启用优化来减少干扰)

0000000000400580 <function>:

  400580:   48 83 ec 18            sub    rsp,0x18

  400584:   48 89 f8               mov    rax,rdi

  400587:   48 8d 3c 24            lea    rdi,[rsp]

  40058b:   48 89 c6               mov    rsi,rax

  40058e:   e8 bd fe ff ff         call   400450 <strcpy@plt>

  400593:   48 83 c4 18            add    rsp,0x18

  400597:   c3                     ret

足够简单。这个函数在栈上为缓冲在400580处分配空间,然后使用一个指向40058e处缓冲的指针调用strcpy。

现在让我们看一下在使用StackCookies时生成的汇编代码。为此,我们需要使用选项-fstack-protector(在gcc与clang中可用):clang-O1 -fstack-protector exmaple.c:

00000000004005f0 <function>:

  4005f0:   48 83 ec 18            sub    rsp,0x18

  4005f4:   48 89 f8               mov    rax,rdi

  4005f7:   64 48 8b 0c 25 28 00   mov    rcx,QWORD PTR fs:0x28

  4005fe:   00 00

  400600:   48 89 4c 24 10         mov    QWORD PTR [rsp+0x10],rcx

  400605:   48 8d 3c 24            lea    rdi,[rsp]

  400609:   48 89 c6               mov    rsi,rax

  40060c:   e8 9f fe ff ff         call   4004b0 <strcpy@plt>

  400611:   64 48 8b 04 25 28 00   mov    rax,QWORD PTR fs:0x28

  400618:   00 00

  40061a:   48 3b 44 24 10         cmp    rax,QWORD PTR [rsp+0x10]

  40061f:   75 05                  jne    400626 <function+0x36>

  400621:   48 83 c4 18            add    rsp,0x18

  400625:   c3                     ret

  400626:   e8 95 fe ff ff         call   4004c0 <_stack_chk_fail@plt>

在4005f7处从线程控制块(TCB,它是libc提供的每线程的数据结构)读入主cookie(该cookie的引用值),并放在栈上,在返回地址之下。后面,在40061a,在函返回前,这个值与TCB中的值比较。如果这两个值不匹配,__stack_chk_fail被调用,以一个类似于:”***stack smashing detected ***: ./example terminated”的消息终止该进程。

现在我们使用-fsanitize=safe-stack选项启用SafeStack:clang-O1 -fsanitize=safe-stack example.c:

0000000000410d70 <function>:

  410d70:  41 56                  push   r14

  410d72:  53                     push   rbx

  410d73:  50                     push   rax

  410d74:  48 89 f8               mov    rax,rdi

  410d77:  4c 8b 35 6a 92 20 00   mov    r14,QWORD PTR [rip+0x20926a]

  410d7e:  64 49 8b 1e            mov    rbx,QWORD PTR fs:[r14]

  410d82:  48 8d 7b f0            lea    rdi,[rbx-0x10]

  410d86:  64 49 89 3e            mov    QWORD PTR fs:[r14],rdi

  410d8a:  48 89 c6               mov    rsi,rax

  410d8d:  e8 be 00 ff ff         call   400e50 <strcpy@plt>

  410d92:  64 49 89 1e            mov    QWORD PTR fs:[r14],rbx

  410d96:  48 83 c4 08            add    rsp,0x8

  410d9a:  5b                     pop    rbx

  410d9b:  41 5e                  pop    r14

  410d9d:  c3                     ret

在410d7e,从线程局部储存(TLS)获取非安全栈指针的当前值。因为每个线程也有自己的非安全栈,非安全栈的栈指针被保存为一个线程局部变量。接着,在410d82,程序为我们在非安全线程上的缓冲分配空间,并将新值写回TLS(410d86)。然后它使用非安全栈内的一个指针调用strcpy函数。在这个函数的epilog(410d92),非安全栈旧的值被写回TLS(基本上,这些指令等效于subrsp, x; … add rsp, x,但对于非安全栈),并且函数返回。

如果我们以-fsanitize=safe-stack选项编译我们的程序,且发生了溢出,保存的返回地址(在安全栈上)不受影响,如果程序尝试写入非安全栈后的未映射/不可写内存,很可能发生段错误。

安全细节:Stack Cookies对比SafeStack

尽管StackCookies对栈破坏提供了相当好的保护,这个安全措施通常有几个弱点。特别的,至少在以下场景中漏过是可能的:

·        代码里的弱点是栈上一个非线性溢出/任意的相关写入。在这个情形里,只是“跳过”cookie。

·        栈上更上方的数据(比如函数指针)可以被破坏,且在该函数返回前被使用。

·        向攻击者泄露了信息有访问权。依赖于该泄露的本质,攻击者可以直接从栈上获取该cookie或主cookie。一旦得到,攻击者溢出这个栈并以泄露信息中的值再次覆盖该cookie。

·        在弱熵(entropy)的情形下,如果在生成该cookie值时没有足够的熵可用,攻击者可能能够计算出正确的cookie值。

·        在一个复制(forking)的服务情形里,对所有的子进程,栈cookie值将维持一样。这可能使得按字节暴力破解栈cookie成为可能,仅改写cookie的一个字节,观察进程是否崩溃(猜错),或继续直到超过下一个返回语句(猜对),每个未知的栈cookie字节,最多要求255次尝试。

不过注意到,在启用栈cookie时,由操作C字符串的函数(比如strcpy)导致的大多数基于栈的溢出是无机可乘的。因为大多数栈cookie实现通常强制其中一个字节为0字节,这使得使用一个C字符串时,字符串溢出越过它是不可能的(虽然使用一个网络缓冲及未处理内存拷贝时仍然可能的)。

实现上可能的失误且置一边,至少在理论上因为分离的内存区域,SafeStack对这些免疫。

不过,SafeStack所不能保护的(设计上),是非安全栈上的数据破坏。换而言之,SafeStack的安全性是建立在关键数据不保存在非安全栈上这个假设之上的。

另外,相比StackCookie,SafeStack不预防被调用者破坏调用者的数据(更准确地,StackCookie阻止调用者在被调用者返回后使用被破坏的数据)。下面的例子展示了这:

voidsmash_me() {

    char buffer[16];

    gets(buffer);

}

 

intmain() {

    char buffer[16];

    memset(buffer, 0,sizeof(buffer));

    smash_me();

    puts(buffer);

    return0;

}

使用-fsanitize=safe-stack编译这个代码,提供多于16个字节作为输入将溢出到main()的缓冲并破坏了它的内容。作为对比,在使用-fstack-protector编译时,将检测到这个溢出,在main()使用被破坏的缓冲前,进程终止。

这个弱点可以通过除SafeStack以外,使用StackCookie来(部分)解决。在这个场景里,可以在安全栈上保存主cookie,并对每个函数调用(或函数调用链)重新生成主cookie。这可以进一步保护上面描述的单纯StackCookie的某些弱点。

非安全栈保护的缺乏,以及在实现中“非安全”定义的保守性,可能在非安全栈上为攻击者提供了足够的关键数据来劫持应用程序。作为一个例子,我们将设计一个,或多或少,真实的代码片段,它将导致(安全性关键的)变量pl被放置在非安全栈上,buffer之前(虽然看起来在编译期间启用了优化使得更少的变量被放置在非安全栈上):

voiddetermine_privilege_level(int*pl) {

    // dummy function

    *pl=0x42;

}

 

intmain() {

    int pl;

    char buffer[16];

    determine_privilege_level(&pl);

    gets(buffer);             // This can overflow andcorrupt 'pl'

    printf("privilege level: %x\n", pl);

    return0;

}

因为当前实现从不递归进入被调用函数,而是将(大多数)函数实参视为非安全,这个“仅涉及数据” 的攻击是可能的。

不过通过改进静态分析,变量重排,以及如上所述,保护被调用者的非安全栈帧,可以大大降低非安全栈上关键数据被破坏的危险

还要注意到,当前实现对安全栈只进行系统级别的ASLR保护。这意味着信息泄露,结合随意的写基本操作,仍然使得攻击者可以改写安全栈上的返回地址。更多信息参考运行时支持实现顶上的注释。最后,应该指出有一个学术研究给出了CPI的某些额外细节。

结论

就上面提到的例外而言,SafeStack实现的安全措施是StackCookie的一个超集,使得它在许多场景里阻止了对易受攻击栈的非法利用。结合低的性能开销,这使得SafeStack在未来的编译中是一个好的选择。

SafeStack仍然在其初级阶段,但看上去它是开发者的编译器武器库中,限制非法利用的一个非常有前途的补充。虽然我们不能称它为缓冲溢出的终结,但它是攻击者要克服的一个重大的障碍。

0 0
原创粉丝点击