CFI 技术新探索,struct_san 今日登场
![CFI技术新探索,struct_san今日登场](https://static001.geekbang.org/infoq/8e/8eae54ef836da8830228e52f1172749c.png)
一、背景
C/C++开发的应用程序,长久以来存在内存破坏类的安全问题。当攻击者掌握了目标程序的漏洞后,就可以开发漏洞利用程序劫持目标程序的控制流。早期的漏洞利用是采用代码注入的方式,通过在缓冲区置入一段代码(shellcode),然后控制 pc 寄存器跳入到缓冲执行这段代码。为了阻止此类攻击,后来计算机系统部署了 DEP(Data Execution Prevention)机制,通过配置内存页属性,将缓冲区设置成不可执行,起到了很好的防御效果。为了绕过 DEP,攻击者探索出了代码重用技术,在目标程序中搜索出一些攻击者期望的操作的代码片段,通过组织这些片段最终完成实现对目标机器的控制。这类攻击技术有 Return-to-libc、ROP(Return OrientedProgramming)、JOP(Jump OrientedProgramming)等。如下图所示,代码有两条动态路径,在路径 1 存一个含有漏洞的节点。当攻击者通过漏洞修改这个节点的跳转逻辑,如果没有可靠的合法性验证机制,那么攻击者最终可以完全控制目标机器。
![](https://static001.geekbang.org/infoq/11/1160142bbb74384c2cf5efd60b2c29ca.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
为了抵御上面的代码复用攻击,加州大学和微软公司于 2005 年提出了控制流完整性(Control-Flow-Integrity, CFI)的防御机制。Control-Flow-Integrity (CFI) 是一种确保软件必须在先前确定的控制流图上执行的安全策略。其核心思想就是在函数在发生不确定的跳转时,验证跳转的合法性。
CFI 分为 Forward Edges CFI 和 Backward Edges CFI。前者是在间接调用前验证控制流,而后者是在函数返回时验证返回地址是否属于调用者。下面罗列了 Linux 下相关实现,如下:
![](https://static001.geekbang.org/infoq/f0/f01d4c59adb3ddc0ffd9e82d1bc42fa3.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
目前还有硬件实现的 Backward CFI
* Intel CET 基于硬件的只读影子调用栈
* ARM V8.3a Pointer Authentication("signed return address")
二、struct sanitizer
我们通过分析一些常见的内核漏洞 POC,发现这些 POC 对控制流的修改都集中在几种结构体内置函数指针的修改上。而上面的 CFI 的方案需要对所有代码进行插桩验证控制流,这样势必会带来明显的性能下降问题。所以我们提出了 struct-sanitizer(struct_san)这种新的控制流完整性检测机制。
struct_san 与上面的 CFI 方案相比,struct_san 在对结构体指针的验证要比已有的 CFI 技术更严苛。当前主流的 CFI 技术主要是验证函数指针的类型,而 struct_san 在此基础上还要验证此函数指针是否还属于当前结构体实例。struct_san 还可以做到非全量插桩,以减少一些非不必要的性能损耗。
三、实现原理
struct_san 工作原理如下:
![](https://static001.geekbang.org/infoq/6b/6ba3ba8fa28296136b5cb3be342b5c1d.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
struct san 通过对在结构体里的函数调用前加入校验函数__sanitizer_struct_guard__(),来验证此函数指针是否属于当前结构体实例,如果验证合法则继续运行下面的间接调用函数,否则抛出 ud2。
四、使用方法
struct_san 为了避免非全量插桩,新增一个 GNU Attributes __attribute__ ((sanitize_struct)) 。
使用方法是在想要保护的结构体类型声明处和调用此结构体的函数指针的函数前加入此关键字,例如想要保护内核中的 pipe_buf_release()代码中的 pipe_buf_operations->release()函数。
1.在结构体类型声明时加入此关键字
![](https://static001.geekbang.org/infoq/d8/d8cd79490ff6d5961f3097723f203832.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
在类型声明完成以后,struct_san 会将此类型的所有结构体实例保存到.sanitize_struct 段内。
2.在需要保护的函数中也要加入上面的关键字。例如在 pipe_buf_release()函数的声明和定义处加关键字,加入关键字后会在调用 pipe_buf_operations->release()前插入校验函函数__sanitizer_struct_guard__()
下面是插桩前后在 gcc 的 gimple IR 中的不同表示:
![](https://static001.geekbang.org/infoq/d5/d54f25c4afe3d20e8257a30ef8cf3857.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
插桩前
![](https://static001.geekbang.org/infoq/9e/9ee23bf61f62f071979f458287bc9cab.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
插桩后
五、检测算法
struct_san 目前只在内核中完成了相关实现。其算法是在内核中开辟一个 128M 大小 shadow memory 用来保存结构体和结构指针的对应关系。__sanitizer_struct_guard__()在调用时会检测传入的 struct 和函数指针是否在 shadow memory 中,如果不在则抛出一个 ud2 异常,否则返回函数指针。实现方案如下:
![](https://static001.geekbang.org/infoq/c1/c1d93b4365c76af5a4acfd9297d8f6b0.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
这个算法参考了 AddressSanitizer 的实现,兼顾了效果和效率。
六、效果
以漏洞 CVE-2021-22555 的攻击代码为例,在启用 struct_san 的情况下,CFI 阻断了攻击代码的执行,起到了有效的防御。
![](https://static001.geekbang.org/infoq/56/56f1e060cddcd4f03022a5f8f9a9360a.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
七、开源地址
我们对 struct_san 进行了开源,期望和业界一起探讨 CFI 技术的改进。后续我们也会推出一些其它的漏洞缓解技术。
评论