原文来自微信公众号“编程语言Lab”:基于智能状态和源代码插桩的 C 程序内存安全性动态分析
搜索关注“编程语言Lab”公众号(HW-PLLab)获取更多技术内容!
欢迎加入 编程语言社区 SIG-程序分析 参与交流讨论(加入方式:添加小助手微信 pl_lab_001,备注“加入SIG-程序分析”)。
分享嘉宾 | 陈哲
回顾整理 | 纪妙
编辑校对 | Skylar
作者介绍
陈哲,南京航空航天大学副教授,硕士生导师。研究方向:软件验证,程序分析,形式化方法等。
个人主页:https://drzchen.github.io
视频回顾
SIG-程序分析技术沙龙回顾|基于智能状态和源代码插桩的C程序内存安全性动态分析
摘要
C 程序的内存错误可能导致程序崩溃和安全缺陷,因此使用动态分析工具在运行时自动发现内存错误是工业界面临的一个痛点,然而传统的内存安全性动态分析工具具有三个缺点:低有效性、优化敏感和平台依赖。
为了克服以上问题,我们提出了一种基于智能状态的监控算法和一种源代码级别的插桩框架,并依此实现了一款新的动态分析工具 —— Movec。实验表明,Movec 工具比 AddressSanitizer、Valgrind 等著名工具能找到更多的内存错误,在性能和可用性方面也非常有竞争力。这项长达五年的持续研究已经在 ISSTA’19 1、ISSTA’21 2、ICSE’22 3、IEEE TSE 4 等顶级会议和期刊发表了 4 篇论文,并获得一次 ACM SIGSOFT 杰出论文奖。
Movec 官网:https://drzchen.github.io/projects/movec
Movec 下载:https://github.com/drzchen/movec
以下为正文。
# 背景 #
今天为大家带来 C 程序内存安全性动态分析相关工作的分享,包含两个创新点,一个是 智能状态,一个是 源代码插桩。
C 语言经常被应用于系统软件的编程,比如嵌入系统、操作系统、编译器等,它可以对内存进行低级别的控制。然而由于开发人员水平参差不齐,C 程序极易出现内存错误,导致数据腐败、程序崩溃等一系列漏洞。
使用静态分析工具,可以在不运行程序的情况下,在程序编译期间发现错误。 但由于不可判定性,静态分析工具往往会产生误报。这时我们需要在准确性和性能之间做出平衡,往往需要牺牲一些精确性来获得更好的性能。
使用动态分析工具,可以在运行时动态分析程序执行有无错误。 动态分析工具一般情况下不会产生误报,但可能产生一些 运行时负载,即程序的运行速度会比未插桩分析之前的程序慢。但通常来说,动态分析工具一般用于测试阶段,这种为程序带来的慢是可以接受的,所以动态分析工具现在很流行。
# 内存错误分类
我们将内存错误分为四类:
空间错误 Spatial Errors
即对空指针或未初始化指针的解引用。比如数组越界,定义一个数组,大小是 10,如果访问第 11 个元素,那么就发生了越界,属于空间错误。
时间错误 Temporal Errors
包含悬空指针,或者对指针的二次释放。比如在堆里分配一个内存,把它释放掉后,接着又去访问它或者释放它。
段混淆错误 Segment Confusion Errors
即未根据指针预期的类型来使用指针。比如有一个函数指针,正常应该用指针去调用函数,但使用时却用指针来输出里面的指针所指向区域的数据。
内存泄漏错误 Memory Leaks
即堆内存上存在不会再被使用也未被释放的对象。比如,在内存中分配了一个对象,但其既未被使用,也未被释放,导致内存越用越少,形成内存泄露。
# 动态分析方法和工具
## 监控方法
动态分析的 监控方法 有很多种。这些监控方法有一个共同点,即 在运行时维护一些元数据。这些元数据用来描述程序当中的一些空间和时间信息,比如,一个对象“被分配了多少空间”、“是否现在还存在于内存当中”、“是否正在被使用”等等。根据这些元数据信息,可以判断程序当中是否有内存错误。
下面列出了动态分析的几种主要监控方法,以及支持的工具。比如 SoftBoundCETS 5,使用基于指针的方法和基于标识符的方法;Google 的 ASan 6 使用了面向对象的方法和内存哨兵技术。保存元数据是这些工具基本的思想。
- 基于指针的方法:SoftBoundCETS
- 基于标识符的方法:SoftBoundCETS
- 面向对象的方法:Google’s ASan, Valgrind 7
- 内存哨兵技术:Google’s ASan
- …
举个例子,假设在内存当中使用 malloc
分配了一个内存空间,工具会记录 p
所指向的空间的基地址是 p
的值,上界是 p+100
。如果程序访问的是 [p, p+100]
之间的内存,则工具会判定是合法的,若程序访问超出了这个范围的内存,比如 p+101
,则工具会认定是一个内存错误。
int *p = malloc(100*sizeof(int));
// The base of p is "p".
// The bound of p is "p + 100".
## 插桩框架
动态分析工具的另一个基础技术是 插桩框架。这些插桩框架需要在待验证的程序中插入一些代码片段,这些代码片段实现的就是监控元数据的方法。被插入的代码片段会随着程序的运行而运行,并收集数据来判定是否有内存错误。
现有的插桩框架都是在中间代码或者二进制层面上进行插桩,目前没有在源代码级别进行插桩的工具。
- 中间代码层 (中间表示):SoftBoundCETS, Google’s ASan
- 二进制层 (目标代码,可执行文件):Valgrind
# 面临的挑战 #
现有的算法和框架,面临着三个挑战。
一是 有效性比较低 low effectiveness。现有的方法无法 确定地、完整地 找到所有的内存错误,包括子对象越界、释放后使用、段混淆、内存泄漏。
此外,现有的算法工具 对优化敏感 optimization sensitivity。不同的编译器优化级别会对检测的最终结果造成影响,一些在低优化级别能找到的内存错误在高优化级别下就找不到了。
第三个挑战是 平台依赖 platform dependence。现有工具只能在一些主流的平台上运行,如 Linux、Windows;无法在一些特定的领域系统上使用,如航空航天领域、嵌入式系统领域,操作系统如 VxWorks 和 uC/OS、架构如 MIPS, MicroBlaze, RISC-V 等。
下面是具体的挑战举例。
# 低有效性
段混淆错误
以非法的解引用为例。
下面的示例代码中,定义了一个指针 s
指向一个函数 (第七行)。但在使用时 (第八行),访问的是它所指向的内存空间里面的第 0 号元素。这个错误可能引起信息泄露 information leakage。
// Example 1: Using a function as data
// This error may cause information
// leakage.
1 void foo(); /* a func */
2 char *func(char *c) {
3 return c;
4 }
5 int main() {
6 void (*p)() = foo;
7 char *s = func(p);
8 char ch = s[0]; /*error*/
9 return 0;
10 }
下面示例代码中,定义了一个指针 p
,指向数组里面的一个元素。但在使用时,把它当成了一个函数来调用 (第七行)。这种错误可能引起控制流劫持 control-flow hijacking。
// Example 2: Using data as a function
// This error may cause control-
// flow hijacking.
1 void (*f(void (*p)()))() {
2 return p;
3 }
4 int main