软件学报  2020, Vol. 31 Issue (6): 1600-1618   PDF    
垂悬指针检测与防御方法
王豫1 , 高凤娟1 , 马可欣1 , 司徒凌云1 , 王林章1 , 陈碧欢2,3,4 , 刘杨5 , 赵建华1 , 李宣东1     
1. 计算机软件新技术国家重点实验室(南京大学), 江苏 南京 210023;
2. 复旦大学 计算机科学技术学院, 上海 200433;
3. 上海市数据科学重点实验室(复旦大学), 上海 200433;
4. 上海智能电子系统研究所(复旦大学), 上海 200433;
5. School of Computer Science and Engineering, Nanyang Technological University, Singapore
摘要: 随着技术的发展,信息物理融合系统(cyber-physical system,简称CPS)在生活中扮演着越来越重要的角色,例如电力系统、铁路系统.如果CPS遭到攻击,将对现实世界的正常运转造成巨大影响,甚至威胁生命安全.垂悬指针是指向的区域被释放后未被置为空的指针,它是一种会导致攻击的软件缺陷.由垂悬指针导致的use-after-free和double-free漏洞能够执行任意恶意代码.迄今为止,只有少量工作针对垂悬指针进行检测、防御.其中多数都会导致过高的额外运行时开销.提出DangDone用于检测和防御垂悬指针.首先,通过静态分析检测潜在垂悬指针;然后,基于检测到的垂悬指针信息和一系列预定义的指针变换规则,依据指针传播信息变换指针,使得指针及其别名都指向同一个新引入的指针.基于该方法,实现了DangDone的原型工具.基于11个开源项目和SPEC CPU benchmark的实验结果表明:DangDone的静态分析部分只有33%的误报率,指针变换部分只引入了1%左右的额外开销.同时,DangDone成功防护了11个开源项目中的use-after-free和double-free漏洞.实验结果体现了DangDone的高效率及有效性.
关键词: 垂悬指针    程序转换    程序漏洞    静态分析    
Detecting and Preventing Dangling Pointers
WANG Yu1 , GAO Feng-Juan1 , MA Ke-Xin1 , SITU Ling-Yun1 , WANG Lin-Zhang1 , CHEN Bi-Huan2,3,4 , LIU Yang5 , ZHAO Jian-Hua1 , LI Xuan-Dong1     
1. State Key Laboratory for Novel Software Technology (Nanjing University), Nanjing 210023, China;
2. School of Computer Science, Fudan University, Shanghai 200433, China;
3. Shanghai Key Laboratory of Data Science (Fudan University), Shanghai 200433, China;
4. Shanghai Institute of Intelligent Electronics & Systems (Fudan University), Shanghai 200433, China;
5. School of Computer Science and Engineering, Nanyang Technological University, Singapore
Abstract: Due to rapid technology advance, cyber-physical system (CPS) plays increasingly important rules in society, such as power system and railway system. However, if these systems are attacked, it would be a serious problem for the world even threats human lives. Dangling pointers is such kind of software defects and can lead to use-after-free and double-free vulnerabilities, which can be leveraged by attackers. So far, only a few approaches have been proposed to protect against dangling pointers, while most of them suffer from high overhead. This paper study proposes a lightweight approach, named DangDone, to detect dangling pointers dynamically. Built upon the root cause of a dangling pointer, i.e., a pointer and its aliases are not nullified but the memory area they point to is deallocated. DangDone first detects dangling pointers by static analysis and fuzzing. Based on the result, DangDone realizes the detection by inserting an intermediate pointer between the pointers (i.e., a pointer and its aliases) and the memory area they point to. Hence, nullifying the intermediate pointer will nullify the pointer and its aliases, which causes crash when encountering use-after-free or double-free. Experimental results have demonstrated that DangDone introduces negligible runtime overhead (i.e., around 1% on average) on SPEC CPU benchmark and is able to protect 11 real-world use-after-free or double-free vulnerabilities. The evaluation demonstrates the efficiency and effectiveness of DangDone.
Key words: dangling pointers    program transformation    vulnerabilities    static analysis    

信息物理融合系统(cyber-physical system, 简称CPS)是计算过程和物理过程的融合系统.它强调信息世界与物理世界的融合, 强调对交互环境的实时监测与控制.它通过与交互环境的实时交互来增加或扩展新的功能, 以安全、可靠和实时的方式检测或者控制一个物理实体.当前, CPS系统被广泛应用于重要基础设施的监测与控制、航天与空间系统、电力系统、铁路系统和智能家居等诸多领域[1, 2].CPS系统中, 不同异构组件的组合引起系统行为极为复杂[3].作为使命攸关与安全攸关的系统, CPS系统需要满足实时性、安全性和可用性等特性.然而, CPS系统的基本组件多由嵌入式系统构成, 该系统主要是由以C/C++语言为代表的编程语言实现.但是, 由于开发者在C/C++语言程序中对于指针的错误使用, 可能会导致CPS系统行为不符合预期.其中, 类似于缓冲区溢出、use-after-free和double-free的漏洞, 会导致系统崩溃甚至执行任意恶意代码.在这3种缺陷中, 后两个缺陷都是由垂悬指针引起的.

垂悬指针(dangling pointer)是由于指针或其别名所指向的内存区域被释放但指针本身未被置空.虽然垂悬指针未被访问(解引用或释放)时是安全的, 但开发人员可能会无意中使用垂悬指针, 在运行时导致潜在的可利用程序状态.攻击者可以利用use-after-free或double-free漏洞来危害程序的正确性和安全性, 如程序崩溃、信息泄露、权限提升, 甚至执行恶意代码[4].自2008年以来, 此类漏洞的数量每年翻倍, 并且由垂悬指针引起的漏洞越发普遍和危险[5].但是, 由于复杂的函数调用和指针指向关系, 开发人员很难杜绝垂悬指针.

当前, 针对垂悬指针的方法有3种.

●    首先, 基于静态分析的方法[4-6]一般有较高的误报率和可扩展性问题(即仅适用于小规模程序);

●    其次, 基于动态分析的方法[7-9]十分依赖于给定的一组测试用例, 并且会面临代码覆盖问题, 可能有较高的漏报率.此外, 这些方法通常需要大量的工作来监控程序运行时的行为, 从而导致过高的额外性能开销(> 100%);

●    最后, 运行时防御方法[10, 11]可以通过在运行时置空所有释放内存区域的指针来动态地保护垂悬指针, 以免受攻击.但是, 这些方法需要额外的内存和计算资源在运行时记录和分析指针指向关系, 然后根据指针指向关系置空潜在垂悬指针及其别名.因此, 这些方法也会产生较高的额外性能开销(> 25%).

为了避免CPS系统的中的垂悬指针被攻击, 开发者可以借助运行时防御方法, 在运行时发现漏洞, 从而避免该漏洞造成恶性影响.但是, 当前主流[10, 11]的运行时防御方法引入了较大的额外性能开销.为了解决该问题, 在本文中, 我们提出了名为DangDone的垂悬指针防御方法, 通过在编译时的程序转换来定位潜在的垂悬指针, 并防御use-after-free或double-free漏洞.确切地说, 该方法先通过静态方法分析潜在垂悬指针, 然后通过在这些检测出的指针(即潜在垂悬指针及其别名)和它们指向的内存区域之间插入中间指针来保护每个潜在的垂悬指针.因此, 将中间指针置空, 能使潜在垂悬指针及其别名置空, 消除垂悬指针的同时避免use-after-free或double-free漏洞造成的攻击.

我们基于LLVM[12]实现了DangDone, 该方法不需要开发人员的干预, 并且能在编译时自动运行.在实验研究中, DangDone成功防护了11个开源项目中的use-after-free和double-free漏洞, 体现出DangDone的有效性.此外, 我们使用SPEC CPU Benchmark[13]评估了DangDone引入的运行时开销.结果表明, 运行时的开销可以忽略不计(即平均约为1%).这使得它比目前的方法[10, 11]更加有效.

总而言之, 本文的创新贡献如下.

●    提出基于静态分析的垂悬指针检测方法;

●    提出了一种通过程序转换的轻量级方法来防御由垂悬指针引起的use-after-free和double-free漏洞;

●    开发了原型工具DangDone, 通过实验, 体现DangDone的有效性和高效率.

本文第1节介绍垂悬指针及其相关漏洞.第2节介绍DangDone的方法.第3节通过实验评估DangDone的有效性和性能, 并讨论DangDone的局限性, 第4节回顾相关工作.第5节对本文进行总结.

1 垂悬指针及其相关漏洞

如果指针指向已被释放的内存区域(包括栈空间和堆空间内存), 则它是一个垂悬指针.如果一个垂悬指针永远不被解引用, 那么它是一个良性的垂悬指针; 否则是一个不安全的垂悬指针[10].垂悬指针可以是堆空间垂悬指针或栈空间垂悬指针.如果指针指向一个对象并且该对象已被释放, 则指针变为堆空间垂悬指针; 如果指针指向栈变量并且栈变量超出其范围, 指针仍然指向栈上不再有效的地址, 这时指针变为栈空间垂悬指针.图 1显示了堆空间垂悬指针的示例.在图 1(a)中, p1和p2在第8行代码后变成垂悬指针, 因为在第8行的内存区域被释放后p1和p2没有置空.

Fig. 1 An example of heap space dangling pointer 图 1 堆空间垂悬指针示例

虽然垂悬指针分为栈空间指针和堆空间指针, 但是本文主要关注堆空间指针, 因为栈空间指针很难被利用[10].但是为了程序的正确性, DangDone在必要的时候会转换栈空间指针(在第2.3.1节讨论).堆空间中的垂悬指针是导致use-after-free和double-free漏洞的根本原因.释放垂悬指针时会导致use-after-free漏洞, 并可能会导致不可预测的行为, 因为内存区域可能已被其他函数重用或存储完全不同的数据.如果垂悬指针指向的内存区域被重复使用, 则use-after-free漏洞可能会泄漏关键信息.特权升级攻击是通过与信息泄漏类似的方式实现.即:垂悬指针指向的内存区域用于检查权限, 并且该垂悬指针可以对内存区域进行写入.当函数指针使用垂悬指针时, 攻击者可以利用use-after-free漏洞来执行任意函数.常见的劫持技术是扩散恶意内容以覆盖垂悬指针所指向的堆中的函数指针或对象的虚表地址[14].这种劫持技术在Linux内核中容易成功, 因为内核为了效率起见会更倾向于使用内存分配的重用机制, 这使得攻击者更容易借助垂悬指针攻击内核[4].

use-after-free漏洞的一个特例是double-free漏洞, 它是由两次调用free函数引起的.double-free漏洞可能导致内存分配函数覆盖存储在块中的内存管理信息.用double-free漏洞可能与利用use-after-free漏洞一样具有破坏性, 可能导致远程代码执行[15]等问题.

2 DangDone 2.1 方法概述以及示例 2.1.1 研究框架

DangDone建立在对垂悬指针根本原因的观察基础上:指针在它们指向的内存区域被释放后没有置空.为了将这些指针置空, 我们可以在释放内存区域的语句之后插入置空操作.但是因为指针可能有许多别名, 所以难以精确有效地对其进行分析.特别是静态指针指向分析不能保证准确性; 动态分析需要为每个指针操作设置跟踪指令, 会导致高开销[7, 10].因此, 在指针被释放后直接置空指针本身及其每个别名的方法不适用于实际的程序.为了解决这个问题, 我们的主要想法是:在内存区域和指向内存区域的潜在垂悬指针之间插入一个中间指针, 强制转换中间指针的类型, 使其对潜在的垂悬指针不可见, 并使所有潜在垂悬指针指向中间指针.因此, 在释放内存区域的语句之后对中间指针插入置空操作, 将使所有潜在垂悬指针在执行置空操作后指向NULL.此时, 程序在尝试解引用垂悬指针及其别名时会直接崩溃, 而不会给攻击者利用use-after-free的机会.

基于上述观察, 我们提出了一种名为DangDone的方法, 通过编译时的程序转换自动消除垂悬指针.图 2展示了DangDone的框架, 它将目标程序和配置文件的源代码作为输入, 并返回受保护的二进制程序.配置文件给出内存操作函数的信息(例如内存分配、释放和重新分配相关的函数名称和参数信息).随后进行缺陷检测, 以静态地分析潜在垂悬指针.这些潜在垂悬指针将会作为输入传递给程序转换模块.DangDone的核心是通过程序转换实现的, 该转换在LLVM中间表示层[16]上进行, 将目标程序变换为受保护的二进制程序.如图 2所示, DangDone先通过缺陷检测模块静态地分析潜在垂悬指针, 再根据其结果执行指针传递和指针变换.在程序转换时, DangDone先通过指针传递查找由其他指针传递的指针, 然后基于指针变换规则在指针变换模块转换这些指针.以这种方式, DangDone的程序转换将转换与潜在垂悬指针具有指针指向关系的所有指针.即:若别名是传递指针的子集, 则该潜在垂悬指针的所有别名都将被转换.

Fig. 2 Framework of Dangdone 图 2 DangDone的框架图

具体来说, 程序变换模块为每个潜在垂悬指针执行4个步骤.

(1)     将分配的指针标识为潜在垂悬指针;

(2)     构造新的分配函数, 分配一个中间指针, 将分配的内存地址分配给中间指针, 并返回中间指针;

(3)     用新的分配函数替换原来的分配函数(如malloc), 转换指针解引用指令, 使内存访问与原始程序一致;

(4)     最后将被释放的指针置为空.

DangDone不需要别名分析或指针指向分析, 因为所有与潜在垂悬指针相关的指针都将被转换.通过这种方式, 对潜在垂悬指针的防御被缩小到对中间指针的防御, 这可以简单地通过置空中间指针来实现, 因为潜在垂悬指针和它们的别名都指向那些中间指针.这些转换由DangDone自动完成, 不需要用户干预.转换可以在源代码层、中间层或二进制层进行.虽然当前DangDone是在中间层实现, 但是为了便于演示, 我们在文中使用源代码级的示例.

2.1.2 示例

图 1(a)显示了一个具有use-after-free漏洞的程序.第10行的漏洞源于第8行的内存释放.它的源码级转换程序如图 1(b)第2行~第12行所示.这里, 变量p1是潜在垂悬指针, 因为它指向由malloc分配的内存区域, DangDone将内存分配函数替换为第14行~第18行定义的新分配函数.新的分配函数包装传统的分配函数, 并在分配的内存区域和用户定义的指针之间插入一个中间指针.第6行的指针传递语句p2=p1表示p2是p1的使用点(也是别名), 因此应转换p2.结果, 两个指针都指向相同的中间指针.然后, DangDone使用中间指针跟踪原始指针的别名(第6行), 这样指针p1和p2上的任何读取或写入的引用都将首先访问中间指针*(char * *)p1, 然后是*(char * *)p1指向的内存区域.由于库函数strcpyfree的参数类型是char *, 因此, 参数分别由*(char * *)p1和*(char * *)p2替换(第5行、第8行和第7行、第10行), 以避免修改函数定义和维护原始程序的语义.DangDone在第9行插入一个置空语句, 以使指针*(char **)p1置空, 该指针的目标内存区域被显式释放.因此在第10行, 由于p2指向*(char * *)p1且*(char * *)p1指向NULL, *(char * *)p2等于*(char * *)p1, 程序将崩溃而不是触发use- after-free漏洞以及给helloworld加上?.

图 3图 4展示出了原始程序和图 1中的变换程序的别名指向关系.这里, 我们在图 3中添加置空指针的操作(即内存释放后增加一个p1=null)以更好地说明最终的指向关系.在图 3中, 在使p1置为NULL之后, p2仍然指向被释放的内存.程序员应该在释放对象时使所有别名无效, 所以仅为被释放的对象提供自动置空的方法不能消除垂悬指针.相反, 在图 4中, 使两个指针中的任何一个置空(即*(char * *)p1或*(char * *)p2)都可以消除两个垂悬指针.原因是p1和p2都指向相同的中间指针IP, 并且置空被释放的对象将消除垂悬指针.

Fig. 3 Point-to relations for aliases in the original program in Fig. 1 图 3 图 1中原始程序的别名指向关系

Fig. 4 Point-to relation for aliases in the transformed program in Fig. 1 图 4 图 1中修改后程序的别名指向关系

为清楚起见, 在下文中, 我们将用原始指针指代那些易受攻击的指针, 并以中间指针指代插入内存区域和原始指针之间的指针.以DD开头的函数表示这些函数是由DangDone定义的.变量的使用点表示直接使用变量的指令.

2.2 基于静态分析的垂悬指针检测

DangDone进行保守的静态分析, 以确定需要转换的潜在易受攻击的指针.此步骤减少了在其后的程序转换中需要考虑的指针数目.实际上, 这一步是可选的, 因为DangDone可以以效率为代价转换程序中的所有指针.

静态分析算法在算法1中显示, 它主要作用是排除不是垂悬指针的指针.首先, 它读取配置文件以获取内存释放和重新分配函数(系统的和自定义的)的列表(第1行), 因为这些函数返回的指针可能是垂悬指针.这些内存释放和重新分配函数也会用于程序转换.然后分析程序的调用图并获得调用图的逆拓扑排序(第2行、第3行), 这是为了确保在函数的调用点中, 被调用者始终是被分析过的.接下来, 对于每个函数, 它获取此函数的所有函数调用, 并标识作为内存释放或重新分配的函数调用的参数的指针(第5行~第9行).这些指针可能是垂悬指针, 因为它们涉及内存释放或重新分配.

在第2个循环中, 对于每个可疑指针, 分析其别名, 如果有一个指针的所有别名未都被置空, DangDone将该指针标记为垂悬指针(第10行~第16行).在第11行、第12行, 因为全局指针分析复杂, DangDone直接将全局指针视为垂悬指针.第16行的updateMemFuns用于分析过程间垂悬指针.这种类型的垂悬指针是由在被调用者内, 作为参数的指针被释放但没有在该函数内被置空引起的.为简单起见, DangDone将这些函数视为一种特殊类型的内存释放函数.因此, DangDone将在其他函数调用它们时检查对应的实参是否在调用者内被置空.第13行的别名分析基于Steensgard的算法[17].最后, 在第17行、第2行, 它提取潜在垂悬指针的信息并输出.所输出的潜在垂悬指针的信息包括指针名称、函数名称、代码行和文件名称.开发人员可以进一步验证这些指针, 以减少误报的数量.

算法1.    保守的静态分析.

Input: Program;

Output: VulnerablePointers.

(1)    memFuns=readConfig(·);

(2)    functions=getCallGraph(Program);

(3)    functions=getInverseTopologicalSort(functions);

(4)    for function in functions do

(5)       for callExpr in function.allCallExpr do

(6)         if callExpr not in memFuns then

(7)           continue;

(8)         pointer=getPointer(callExpr);

(9)         tmpPointers.push(pointer);

(10)       for pointer=tmpPointers.pop(·) do

(11)         if pointer is global pointer then

(12)           PointerStack.push(pointer);

(13)         aliasSet=analyzeMayAlias(pointer, function);

(14)         if not ifAllPointersNullified(aliasSet, function) then

(15)           PointerStack.push(pointer);

(16)           updateMemFuns(·);

(17)       warningList=extractPointerInfo(pointerStack);

(18)       VulnerablePointers=ReportWarning(warningList);

在设计静态分析时, 更准确的静态分析也是可行的.例如, 像Andersens算法[18]这样更精确的别名分析可以进一步减少误报.DangDone也可以在没有静态分析的情况下转换所有指针, 目前的保守静态分析会引入许多误报, 但可以避免漏报.没有漏报的原因是垂悬指针的漏报来源于不精确的别名分析, 而基于Steensgard的别名分析是没有漏报的.我们设计这种保守的静态分析的原因是误报只会增加开销, 而漏报可以绕过DangDone的策略.因此在这里, 我们尝试通过排除不是垂悬指针的指针来减少开销.

2.3 基于多级指针的垂悬指针防御策略

在现实世界的程序中, 指针非常复杂.因此, 我们研究了SPEC CPU Benchmark[13]测试中指针的使用统计数据:27.3%的指针指向基本类型, 41.7%的指针指向类或结构, 16.3%的指针指向结构中的元素, 6.4%的指针指向模板(包括用户定义的模板和容器), 8.3%的指针是二级指针.

此外, 我们根据操作是否对指针和内存产生副作用对操作进行分类:指针的副作用意味着指针指向不同的内存区域; 对内存的副作用意味着内存区域被改变.

●    对指针和内存没有副作用:例如指针传递和解引用;

●    仅对指针产生副作用:例如malloc或第三方用户定义的分配函数以及指针运算;

●    副作用仅限于内存中:例如free或第三方或用户定义的释放函数;

●    对指针和内存的副作用:例如, realloc或第三方或用户定义的重新分配函数.

基于上述统计和类别, 我们通过图 1图 5中的示例展示变换规则.其中, ele表示复杂结构的类型.

Fig. 5 Examples for pointer modification rules 图 5 指针变换规则的例子

2.3.1 指针传递和解引用的规则

我们首先介绍指针传递和解引用的变换规则, 它们对指针和内存没有任何副作用.

规则1.   DangDone通过检查被分配的内存区域是否被访问来区分指针传递和解引用.如果未访问所分配的存储区, 则视为指针传递; 否则视为指针解引用.例如, 指针传递包括指针赋值、函数参数和获取指针的地址.

规则2.  如果指针由潜在垂悬指针传递, 则指针也是潜在垂悬指针.示例如图 1中的第6行, 其中p2=p1表示p2也是潜在垂悬指针.

规则3.  如果原始指针被解引用, DangDone将指针转换为二级指针, 并将其使用替换为额外的解引用.此时, 原始指针变成了二级指针, 因为新的分配函数(参见图 1DDMalloc)与原来的分配函数不同.如图 1中的例子所示, 第10行中的p2由*(char **)p2代替.附加的引用可以使原始程序和转换程序之间的内存保持一致.因此, 如果解引用原始一级指针, DangDone将用类型转换和原始指针的双重解引用替换原先的使用点.

对于变换后的程序, 原始指针的指针传递不会改变, 但是原始指针的解引用会被类型转换和附加的解引用替换.因此, 中间指针对于程序的其余部分是不可见的.

然后, 我们将展示如何将这些规则应用于复杂的指针, 包括过程间指针、函数指针、间接指针、结构中的指针和栈空间指针.

●    过程间指针

过程间指针可以是两种类型:全局指针和作为函数参数传递的局部指针.全局指针的转换规则同局部指针.

如果原始指针作为函数参数传递, DangDone传递原始指针而不是指向参数的间接引用指针, 以使指针指向关系保持与原始程序一致.例如:在图 5中的第6行有一个函数调用foo(p), p是潜在垂悬指针; 并且转换后的函数调用仍然是foo(p).但是, foo的参数p与原始语义不同.因此, 如果函数的参数已经转换, 则需要转换相应的函数参数.在这种情况下, 还应转换foo的参数以保持内存访问的一致性.然后, DangDone搜索定位函数的所有使用点, 以使它们的调用者的相应参数也被转换.这里有一个例外是处理库函数的参数时, DangDone将参数替换为对原始指针的强制转换和解引用(如图 1中的第10行).原因是当函数的参数改变时, 这些参数的使用点需要进行相应的修改.因此, DangDone不能转换没有源代码的库函数.

●    函数指针

C/C++中的一个特殊结构是指向函数的指针, 这些函数指针为攻击者提供了一种通过利用use-after-free漏洞来注入代码的方法[4, 19].据我们所知:尽管函数指针可以允许攻击者执行任意代码, 但函数指针不会直接导致use-after-free或double-free漏洞, 但其他类型的指针(如指向结构的指针)确实会引发此类漏洞.因此, DangDone不处理函数指针.但是, 转换的指针可以用作函数指针的参数.在这种情况下, 定义满足函数指针类型的函数将进行相应的转换.

●    间接指针

我们将具有两个或以上级别的指针称为间接指针.如果我们对所有一级指针(即直接指针)进行保护, 那么间接指针不会变成垂悬指针.但是, 尽管间接指针不需要关心内存区域, 但DangDone应该对它们进行转换, 因为它们可以通过两个或多个解引用来访问中间指针.因此, DangDone会分析间接指针并在它们访问中间指针时对它们进行转换, 使得访问期望的内存空间.

●    结构体中的指针

对于结构体中的指针(例如数组、结构、类和模板), 它们的转换遵循与其他指针相同的规则.不同之处在于解引用操作, 因为访问结构的方式是多种多样的.为了使内存访问与原始程序保持一致, 我们应该考虑内存访问的特性.数组中指针变换的一个例子如图 5中的第8行~第13行所示.当libFun调用它时, DangDone应转换指针a1[i], 因为它是一个库调用.另一个显示结构中指针转换的例子如图 5中的第14行~第18行所示.指针pvar被转换为二级指针并解引用, 以便libFun的参数与原始程序保持一致.

●    栈空间指针及常量地址

虽然我们专注于堆空间垂悬指针, 但由于指针传递, 栈空间指针也可能会被转换.例如:指针在一个分支中, 它指向的是分配的内存; 在另一个分支中, 它指向的是一个栈变量.DangDone通过为潜在垂悬指针传递的指针构造一个中间栈指针来转换栈空间指针.中间指针的生命周期与它指向的内存区域相同.因此, 栈和堆空间指针的操作是一致的.在CPS系统中, 常量地址相较于常规应用程序更为常见.它是指针传递过程中其中一个别名被赋值成一个常量地址.针对常量地址的赋值, 我们也应用相同的处理方式.示例代码如图 6所示.

Fig. 6 Examples of stack space pointer and pointer from constant address 图 6 栈空间指针和常量地址例子

2.3.2 内存分配函数的规则

现在我们介绍对指针和内存有副作用的操作规则, 以便在转换后指针和内存仍然保持一致.潜在垂悬指针被释放或重新分配时, 如果它们指向释放的内存, 我们应该将它们置空.此外, 还应考虑一个指针的多重分配.指针运算将在第3.4节讨论.

规则4(内存释放).  为了防护潜在垂悬指针, 如果释放了一个内存区域, DangDone会中间指针插入置空语句.如果此时程序执行到内存释放语句和置空语句后, 原始指针及其别名都最终指向NULL.图 1中的第9行显示了一个示例.由于所有别名都指向同一个中间指针, 因此无论它们是显式释放还是隐式释放, 所有别名都将置空.从这个意义上讲, DangDone只需要为一次内存释放插入一个置空语句.

规则5(内存重新分配).  DangDone对重新分配进行包装, 以便在重新分配函数返回的指针与传递给函数[11]的原始指针不同时, 置空原始指针并为新返回的指针添加一个中间指针.这个规则是由于类似于realloc(·)的内存分配函数可以增加或减少所分配的内存空间.但是如果所需的内存空间太大, 则重新分配的地址可能与原始地址不同.所以DangDone在所分配地址和原始地址不同时, 置空原始指针并为新指针加入中间指针.

规则6(内存分配和多重分配).  对于每个潜在垂悬指针, 我们用DangDone定义的新内存分配函数替换原来的内存分配函数(包括堆和栈空间内存).新的内存分配函数在调用原内存分配函数的同时, 还分配了指向内存区域的中间指针.然后返回中间指针的地址, 而不是所分配的内存区域的地址.请注意:此时原始指针变为二级指针, DangDone将类型转换为一级指针.使用新分配替换分配的目的是强制所有指针及其别名都指向中间指针.因此, 该替换应该分析指针的级别(如是否是间接指针), 除了在内存区域和一级指针之间插入中间指针外, 应避免在其他指针之间插入中间指针.例如, 图 1中的DDMalloc展示了这种新分配的过程.关于分析指针级别的例子, 如例子程序char * * p=malloc(size_t); char * p=malloc(32), 此转换应只替换第2个内存分配(即malloc(32)).

多重分配是指一个程序分配了一个新的内存区域并将其分配给一个已经指向内存区域的指针.处理方式依旧遵循规则6以避免语义上的差异.换句话说, DangDone不去区分某处的内存分配是否是多重分配.但是, 替换分配的副作用是内存泄漏.分配引入的潜在内存泄漏问题将在第3.4节中讨论.

2.4 指针传递和指针变换 2.4.1 指针传递

DangDone跟踪从原始指针传递的每个指针的指针传递, 并递归地变换这些指针.它通过记录原始指针和转换原始指针的使用点来实现.原始指针的指针指向结果存储在它们相应的中间指针中, 因此, 指针保持着与原始程序相同的指针指向关系.

算法2介绍了递归变换指针的过程.输入程序是需要保护的代码.DangDone首先分析程序, 找出指向已被分配的内存区域(第1行)的潜在垂悬指针.然后将潜在垂悬指针存储在栈PointerStack(第2行)中.对于栈顶部的每个指针, 首先替换指针oriPointer(第5行)的分配函数(若有)(第4行).相反, 如果它指向栈空间(第7行), 则为指针oriPointer(第6行)插入一个中间指针, 然后, DangDone将原始指针标记为已修改以供进一步使用(第8行).然后, DangDone基于Def-Use Chains和Use-Def Chains[16](第10行)识别所有使用点(例如使用原始指针的指令).对于每个使用点, DangDone通过函数getOtherHandSide获得使用点(第11行)也使用的指针otherPointer, 该函数确定与原始指针相关的指针(例如别名和解引用).由于某些指令只有一个操作数, 因此otherPointer可以为NULL.如果存在otherPointer, 则检查otherPointer是否已经转换(第13行); 如果没有, 则将此指针存到PointerStack.请注意:如果未转换otherPointer, 则不会转换此使用点; 但当两个指针都被标记为已修改时, 它将被转换.在添加潜在的潜在垂悬指针后, DangDone使用第18行的函数transformPointer来转换指令.如果指令是指针传递, 它将保持指向关系; 如果指令中未引用指针, 则它将替换为其他引用.详细的转换算法见第2.4.2节.由于DangDone是在潜在垂悬指针使用到这些指针时变换这些指针, 因此它不需要进行额外的别名分析.虽然递归变换可能会导致指针别名的大量转换代码带来的额外开销, 但实验表明这个开销是很小的.

算法2.    递归变换指针.

Input: Program;

Output: Secured Program.

(1)    Pointers=analyzeAllocatedMemory(·);

(2)    PointerStack=pushOntoStack(Pointers);

(3)    for oriPointer in PointerStack.top(·) do

(4)      if ifHasAllocation(oriPointer) then

(5)         replaceAllocation(oriPointer);

(6)      if pointsToStack(oriPointer) then

(7)         insertPointer(oriPointer);

(8)      markPointersAsModified(oriPointer);

(9)      //users(·) returns all users for a pointer.

(10)      for user in oriPointer.users(·) do

(11)         otherPointer=getOtherHandSide(user);

(12)      if otherPointer is not null then

(13)         if not isPointerModified(otherPointer) then

(14)            //This user will be modified in otherPointer.

(15)            PointerStack.push(otherPointer);

(16)            continue;

(17)      //Transform the pointer to keep memory access consistent.

(18)      TransformPointer(user, oriPointer);

(19)    PointerStack.pop(·);

例:对于图 1中的程序, PointerStack只包含一个指针p1, 因为只有这个指针被显式分配.DangDone首先替换内存分配函数, 以使原始指针p1指向中间指针.然后搜索p1的所有使用点, 发现p1有4个使用点.因为其中3个没有对应的操作数, 所以将他们传递到transformPointer.与之对应, 由于p2=p1有对应的操作数p2, 并且发现该指针尚未被修改, 即p2未标记为已修改, 因此, DangDone将它存到PointerStack, 以便之后的指针变换.注意:即使p2不在初始的PointerStack中, DangDone也可以保护这个指针, 因为当p1的转换完成时, PointerStack上的顶部变量是p2, 此时指令p2=p1将被转换.

2.4.2 指针变换

为清楚起见, 算法3仅显示常见场景的转换.输入包括指令、指令中使用的原始指针; 算法的输出是转换后的指令.首先对指令进行分析, 确定操作类型.如果指令用于指针传递, 则不需要转换指令.这是因为转换的一个目标是使内存访问与原始程序保持一致, 并且一级指针的指针传递与二级指针相同.换句话说, 对于指针传递, 无论指针的级别如何, 指向关系都是相同的.如果指令解引用原始指针或调用库函数, 它将用另外的解引用替换原始指针的解引用(第1行~第3行).由于解引用指针的目的是访问指针所指向的内存区域, 因此它添加了原始指针的额外的解引用, 以使原始指针的内容与原始指令想要访问的内容一致.大多数情况下, 如果原始指针解引用一次, 则只需要对原始指针进行两次解引用.对于对指针或内存有副作用的操作, 转换策略显示在第4行~第8行.如果指令释放原始指针, 则它插入指令将中间指针置空(第6行).如果指令重新分配原始指针, 它将使用DangDone定义的新的重分配替换.它添加了置空语句, 根据重新分配的指针是否等于原始指针(第8行), 将指针置为空并插入一个中间指针.

算法3.    变换指针.

Input: user, originalPointer;

Output: transformed instructions.

(1)    if isPointerDereference(user) or isLibFunction(user) then

(2)       //Pointer transformation.

(3)       addAdditionalDereference(originalPointer);

(4)    else if isMemoryOperation(user) then

(5)       if isDeallocation(user) then

(6)         instrumentNullifyIns(user);

(7)       else if isReallocation(user) then

(8)         replaceReallocation(user);

例:继续图 1中的程序.DangDone分析到在转换p1时, p1的3个使用点被传递到这个阶段.因为它使用*(char **)p1作为中间指针, p1=malloc(32)转换为p1=DDMalloc(32);strcpy(p1, “hello”)转换为strcpy(*(char * *)p1, “hello”); free(p1)被free(*(char **)p1)替换, 并插入额外的置空操作.在转化p1之后, p2的3种用途被传递到该阶段.两个函数调用的转换类似.p2=p1不需要变换, 因为它属于指针传递操作, 并且两个指针被标记为已修改.

3 实现与实验

该方法适用于源代码、中间表示、甚至二进制.目前, 为了提高效率和跨语言支持的能力, 我们在基于LLVM[12]的中间表示(IR)级别实现了DangDone(https://bitbucket.org/fff000147369/nodang).DangDone中的转换被实现为LLVM Pass.LLVM Pass执行构成了编译器的转换和优化[20].因此, DangDone的核心流程是:通过Clang的AST分析潜在垂悬指针; 开发者选择潜在垂悬指针用于被防护; Clang[21]将目标项目编译为中间表示(LLVM IR[16]); DangDone应用LLVM Pass来转换LLVM IR; Clang将转换后的LLVM IR编译为二进制.

3.1 实验设置

我们对DangDone的实验旨在回答以下3个研究问题.

(1)     防护能力:DangDone是否有效地防护use-after-free和double-free漏洞?

(2)     运行时开销:就时间和内存而言, DangDone是否有可接受的运行时开销?

(3)     垂悬指针检测效果:DangDone的静态分析是否有足够的效果?

我们使用了11个真实的漏洞来评估DangDone的安全性, 并使用SPEC CPU Benchmark[13]来评估DangDone的静态分析效果和运行时开销.SPEC CPU Benchmark是一系列CPU密集型程序的集合, 着重于CPU、内存以及编译器的评估.实验是在运行64位Ubuntu 14.04的8核Intel Xeon CPU E5620(2.40GHz)和16 GB RAM的PC上进行的.使用LLVM 3.6.0以无优化(-O0)的方式编译SPEC CPU Benchmark.并且为了展示独立的实验效果, 静态分析和代码转换模块是各自单独评估的.即, 此次实验的程序变换模块将所有指针视为潜在的垂悬指针.

3.2 安全性评估(RQ1)

为了评估DangDone对实际程序存在的漏洞的检测预防措施, 我们选择在开源程序中并且有公开恶意输入(如Exploit Database[22])的CVE漏洞作为基准程序.选择了11个CVE漏洞.这些漏洞的详细信息列在表 1的前6列中, 包括CVE ID、受影响的程序和版本、漏洞类型以及使用的程序版本和缺陷位置.

Table 1 Evaluation results for real-world vulnerabilities 表 1 基于真实漏洞的评估结果

与此同时, 为了评估DangDone在CPS系统中的有效性, 我们选取了5个开源CPS系统中注入特定的缺陷, 并借助DangDone进行防护.所选取系统的相关信息见表 2的前3列所示.所注入的缺陷信息如第4列~第6列所示.针对Use-after-free, 注入缺陷的方式是在第6列所指定的位置释放指针, 在下个使用点之前加入指针是否为空判断语句.针对Double free, 注入缺陷的方式是在指定位置插入free语句, 并在插入的语句之前加入指针是否为空判断语句.

Table 2 Evaluation results for real-world cyber-physical systems 表 2 基于CPS系统的评估结果

实验结果在表 1表 2的最后两列中报告, 包括执行恶意输入时程序的行为以及由DangDone转换的程序在执行恶意输入后的行为.在表 1中, 我们发现其中3个程序, 在其代码被DangDone保护后导致了段错误.原因是对空指针的解引用导致的.其余情况出现的原因则是因为在加入指针置空语句之后, 原来的垂悬指针指向了NULL.而这些程序会判断所使用的指针是否为NULL, 如果发现为NULL则会进行相应处理, 如直接退出或者报错.在DangDone转换易受攻击的程序之后, 所有的漏洞都可以在执行恶意输入之后直接退出而不会被攻击.在表 2中, 由于注入的缺陷的特征, 直接执行有缺陷的结果都是程序崩溃, 相比之下, 借助DangDone防护的程序则是能够正常退出.因此, 我们的实验回答了RQ1, 即DangDone在防护垂悬指针引起的use-after-free和double-free漏洞方面是有效的.并且, 在实验过程中, 我们没有发现DangDone在指针变换时的误报.

3.3 运行时开销(RQ2)

当DangDone向目标程序插入新的指令时, 它会增加目标程序的执行时间和内存消耗.理论上, 如果程序的所有变量都是指针, 并且除了指针解引用和指针传递之外没有其他语句, 那么最大运行时开销接近100%.为了评估DangDone对实际应用程序的运行时开销, 我们使用SPEC CPU Benchmark分别运行3次原始程序和转换后的程序来测量时间开销和内存开销.注意:由于编译问题, 没有使用PerlBench和Deall这两个基准程序.

图 7显示了DangDone对特定CPU Benchmark施加的时间开销, 这些Benchmark的输入是在Benchmark中名为“Ref”目录中.我们使用几何平均数计算平均开销.平均而言, Dangdone的时间开销为0.3%.这表明DangDone引入了可忽略的运行时开销.

Fig. 7 Overhead for the SPEC CPU Benchmarks 图 7 SPEC CPU Benchmarks的运行时开销

图 7还显示了DangDone引入的内存开销.我们通过当前内存使用率的频繁快照收集内存使用率, 并比较它们的最大内存使用率.所有Benchmark的内存开销都小于0.8%, 这表明DangDone引入的内存开销可以忽略不计.这是因为我们创建了很小的指针来跟踪指针.内存开销由中间指针引入, 因此, DangDone引入的内存开销取决于内存分配的数量.在大多数程序中, 分配内存的大小远远大于指针的大小, 导致内存开销较低.

此外, 我们在表 3中报告了规范CPU Benchmark的转换、指针覆盖率和编译统计信息.#m_ptr表示变换的指针数, #m_ins表示变换的指令数.由于指针可能有许多用途, 因此变换过的指令的数量是一个更好的运行时开销指标.结合表 3中的变换的指针、指令数和图 7中的时间开销, 我们可以看到, 增加的时间开销和边换的指针、指令数都是正相关的.此外, Dyn.Cov报告那些执行代码中由DangDone创建的所有指针中覆盖的堆指针的比例.注意, 我们使用动态覆盖率是因为很难静态地区分栈指针和堆指针.平均而言, DangDone覆盖了83%的堆指针, 同时仍然保持着较低的时间开销.编译时, 大多数Benchmark的开销可以忽略不计; 平均而言, DangDone在编译时预防漏洞的时间开销为0.9%, 这表明DangDone在编译时引入的额外开销也低.

Table 3 Pointer coverage and compilation statistics for the SPEC CPU Benchmarks 表 3 基于the SPEC CPU Benchmarks的指针转换覆盖率和编译数据

总之, 运行时开销表明了DangDone的正确性, 因为SPEC CPU Benchmark有结果比较机制, 以确保结果与预先定义的结果相同.所以这部分实验表明, DangDone的方法在SPEC CPU Benchmark不会对程序执行有影响.同时, 图 7表 3中的结果肯定地回答了RQ2, 即:DangDone对程序的开销可以忽略不计, 编译的时间开销也可以接受.

3.4 垂悬指针检测效果评估(RQ3)

表 4展示了静态分析的效果.该分析方法分为两部分:读取AST的时间和分析时间.为了提升效率, DangDone通过动态读取AST以优化内存开销.在该实验中, 我们依旧使用SPEC CPU 2006 Benchmarks, 配置的动态读取AST的数目分别为1 000和10个.#ASTs和#KLOC是AST的数目和程序行数.针对这两个AST读取数目的配置, 我们分别统计他们的读取时间, 分析时间和内存消耗.#Warnings是静态分析报告中潜在垂悬指针的数目.在此次实验中, 我们只考虑freedelete这两个内存释放函数.FP Rate是DangDone在特定程序下的误报率.误报是通过人工检查每个报告统计的.

Table 4 Performance of static analysis for the SPEC CPU 2006 Benchmark 表 4 静态分析方法在SPEC CPU 2006 Benchmark上的性能

我们可以看到:如果内存足够, DangDone的静态分析是轻量级的, 并且它的平均分析速度是7KLOC/s左右.尽管该静态分析工具可能消耗4GB的内存, 但所有的程序都可以在22s内进行分析.相比而言, 当加载在内存中的AST数被设为10时, 分析速度会降为1.8KLOC/s, 但是内存消耗会降到380Mb, 几乎所有现代PC都可以提供该要求.此外, 不同的程序消耗不同的内存.一个原因在于AST的大小是不同的, 例如有的AST大小为30MB而有的为0.1MB.如果不限制内存中加载的AST的数量, 则遇到超大规模的项目时, 很容易消耗所有内存.

此外, 为了评估静态分析的误报率, 如果warning(告警)数小于50, 则人工验证所有warning; 否则, 随机抽取50个进行人工验证.我们发现, 静态分析的误报率约为32.657%.此外, 为了评估静态分析的漏报率, 我们使用Fortify SCA[23], Splint[24]以及Coverity[25]作为基准, 共报告了106处warning.具体来说, Fortify报告了23处warning, 包括12个double-free和11个use-after-free.Splint和Coverity分别报告了72个和11个warning.我们人工验证这些warning并发现DangDone的静态分析部分也会报告所有warning, 这说明DangDone的静态分析的漏报率为0.

综上所述, 表 4的结果可以回答RQ3, 静态分析可以通过减少潜在垂悬指针数量来减少DangDone的时间开销; 这需要一定的时间开销以及内存开销; 并且具有可接受的误报率和非常低的漏报率.

3.5 讨论

●    漏报

据我们所知:当应用到现实项目中时, DangDone会引入漏报, 即无法防御指针计算生成的指针.因为DangDone试图使所有别名指向中间指针, 而指针算法不遵循此规则.例如, 图 8中的转换程序创建一个生命周期与p2相同的指针, 然后将指针运算的结果赋给新创建的指针.最后, 它将地址存储到p2, 使所有转换的指针具有相同的级别.但是, 第10行的引用仍然是use-after-free.尽管DangDone无法保护通过指针运算创建的潜在垂悬指针, 但比起不转换, 不如对其进行转换, 因为我们仍然可以保护部分漏洞(例如指针的指针算法位于分支中时).在我们的实验中, 尚未发现与指针运算相关的垂悬指针.

Fig. 8 Dangling pointer caused by pointer arithmetic 图 8 指针计算导致的垂悬指针

●    间接赋值

别名可以通过间接赋值引入.对于存储在模板中的原始指针(即用户定义的模板和容器), 我们递归地转换模板中使用的所有指针(即模板的使用点).如果不转换所有指针, 会导致程序语义发生变化.

●    类型转换

非指针类型变量之间的指针传递可能导致特殊别名, 但是DangDone可以识别这样的指针传递; 这些类型的指向关系可以被它们对应的原始指针跟踪, 它们的指针类型可以由DangDone确定.

●    释放中间指针

虽然DangDone通过替换内存分配函数分配了中间指针, 但是不能通过替换内存释放函数释放中间指针.这可能导致内存泄漏.为了解决这个问题, 我们通过静态分析得到的保守的指针分析结果, 在某个潜在垂悬指针及其别名中分析出生命周期最长的变量, 确定其最后一次使用点所在的函数.在该函数的返回之前, 插入中间指针释放语句.

●    CPS系统中的防护结果

虽然DangDone针对use-after-free和double free漏洞的防护结果是导致程序崩溃, 而CPS系统中的可用性是一个重要指标, 直接导致程序崩溃会影响其可用性, 但是值得注意的是:在借助于来自CVE的漏洞的实验中, 有一些程序能安全退出是通过检测被使用的指针是否为NULL.所以在CPS系统中, 虽然我们能够避免更严重的攻击后果(如权限提升、执行恶意程序等), 但是依旧需要开发者有一定的容错机制.例如, 在指针的使用点需要检测该指针是否为NULL:如果是, 则进入错误处理.

4 相关工作 4.1 改进内存分配函数

我们改进了内存分配函数来保护垂悬指针.Cling[26]替换了原来的内存分配函数, 因为一些攻击手段依赖于释放内存的重复使用, 它会避免重复使用同一类型的内存对象.DangDone也用自定义的内存分配函数替换了内存分配函数, 但是Cling的内存分配函数侧重于只复用特定被释放的内存, 这段内存的对象类型和释放之前的对象类型一致.Cling的时间开销与DangDone相似, 但Cling的内存开销略高一些.此外, Cling不能处理由垂悬指针引起的所有类型的漏洞, 例如无法防止由垂悬指针引起的信息泄漏.并且, 该方法无法及时检测到出现了use- after-free等漏洞, 相比之下, DangDone在预防这类漏洞的同时, 也能够通过程序崩溃报告所出现的漏洞. Diehard[27]是一个运行时系统, 可以通过随机化和复制来容忍内存错误.可以通过内存随机化和复制来容忍内存错误.它提供了一个近乎无限大的堆空间, 使对象可以永远不被释放, 并且各个对象可以无限远地被分配.以此消除了垂悬指针, 并且不易受到缓冲区溢出攻击.但是空间消耗比传统的内存分配函数大, 因为它需要至少两倍的额外内存空间去防止堆被破坏.Dieharder[28]通过改进随机化操作扩展了这种方法, 这使得开发变得更困难, 并且降低了性能开销.DieHarder具有20%的几何平均性能影响, 并且该方法可能面临能够控制内存分配的攻击者的攻击.相比之下, 内存开销高于DangDone, 少于DieHarder.

4.2 静态方法

与动态方法或运行时保护相比, 检测垂悬指针的静态方法相对少见.大多数静态方法关注一组软件缺陷, 垂悬指针只是其中的一个.Musuvati等人[29]实现一种新的模型检查器, 可以直接检查C/C++程序, 无需人工抽象系统行为.自动抽象模型是通过捕获系统的所有行为来实现的[30].因为系统应该是一个面向行为的程序, 这种技术的一个局限性是灵活性.Slayer[31]是一个验证工具, 用来证明系统代码不存在内存安全错误, 例如垂悬指针解引用和double-free问题.但是与我们的方法相比, 它是一个重量级的工具, 因为slayer的可扩展性受到SMT解算器的限制.

4.3 动态方法

以检测是否出现垂悬指针或其漏洞的方法可以分为两种类型:一种关注use-after-free漏洞; 另一种是内存错误检测器, 它还可以检测垂悬指针或use-after-free漏洞.Valgrind Memcheck[32]是一种内存错误检测器, 可以检测堆中的use-after-free和double-free漏洞.与purify[33]类似, 这两种工具都会产生很高的内存和CPU开销(代价是2倍~25倍的性能损失), 并且无法检测到重新分配数据位置的垂悬指针.Mudflap[34]使用编译工具在指针使用(解引用)时插入判断内存区域是否被合法引用的断言.为了判断内存访问的合法性, 它还需要检测来维护有效内存对象的列表.然而, Mudflap在SPEC CPU Benchmark上的额外开销范围从2x~41x, 在复杂的C++代码中也有误报率.一种类似于Mudflap的方法是AddressSanitizer[35], 它是一个基于Clang的内存错误检测器.它使用影子内存记录应用程序内存的每个字节是否可以安全访问, 并使用检测工具检查每个应用程序加载或存储时的影子内存.但是, 为了性能考虑, 影子内存没有映射到所有内存分配函数.该优化导致漏报, 如果访问重新分配的内存块, 将不会检测到use-after-free.DangDone会在释放对象时置空垂悬指针, 使这些漏洞无法利用.此外, 由AddressSanitizer所引入的减速是2倍, 这也高于DangDone.DangSan[36]方法则是侧重于在多线程下高效率地通过日志检测垂悬指针.它主要优化了记录指针指向、释放信息所需要的时间, 以此提高效率.

相比于RAII和C++11及其之后版本中引入的智能指针(如shared_ptr), 这两种方法都能更方便地管理指针.但是相比之下, DangDone有着更广泛的适用性.这是因为一些系统如CPS系统中, 不能引入RAII或智能指针.除此之外, DangDone着重于预防和检测垂悬指针, 这是RAII和智能指针无法直接支持的, 需要开发者自行避免垂悬指针及其使用.

4.4 基于程序转换的运行时防御方法

通过程序转换[10, 11, 37-40]确保空间安全错误的方法与我们的方法类似.为了减少空间安全误差、提高效率, 不同的转换策略被提出.以前的方法存在一个或多个不足, 很难被广泛采用, 例如运行时开销高或需要对现有代码进行很大更改.

DangNull[10], FreeSentry[11]和pSweeper[39]是最近并且与DangDone最相似的预防措施, 通过动态运行时检查防止use-after-free漏洞.DangNull由两个主要组件组成:静态检测和运行时库.第1个组件通过插入对跟踪例程的调用来标识指针传递, 以跟踪指针和内存对象之间的指针指向关系.所有内存操作函数(如mallocfree)都会被替换, 以跟踪内存对象和指针的生命周期.第2个组件为静态检测函数提供运行时库, 以便动态跟踪指针并将释放的指针及置空其别名.基于SPEC CPU Benchmark的评估显示, DangNull增加了80%的平均性能开销, 这比DangDone的开销高得多.FreeSentry通过将对象链接回其指针来防止use-after-free漏洞.在这些链接的帮助下, 当对象容易受到垂悬指针的攻击时, FreeSentry可以使链接到已释放对象的所有指针失效.为了实现这一点, freesentry注册了创建或修改指向新对象的指针的地址.释放对象后, 释放函数将查找该对象所驻留的内存区域的所有指针.如果这些指针仍然指向释放的对象, 则将指针置空; 如果某些指针试图对失效的内存地址进行解引用, 将导致程序崩溃.SPEC CPU Benchmark[13, 41]的评估显示:如果禁用栈空间保护, 则平均开销为25%;否则, 平均开销为42%.pSweeper则是借助另一个线程去扫描已有的指针, 判断这些指针是否是垂悬指针, 实验表明: pSweeper的开销可低至12.5%.

综上所述, 可以发现, 三者的开销都比DangDone高得多.但是DangDone依赖于转换规则, 而例如DangNull等方法的规则则相对更加通用.例如:如果存在嵌套的struct结构, DangDone则还需要增加对应转换规则.本文之前的工作[42]针对所有指针进行防护, 但是实际程序它们不一定都是垂悬指针.所以为了减少不必要的转换, 我们加入静态分析技术以减少所需要转换的垂悬指针数量.相比之下, 前期工作只能够预防垂悬指针而不能通过静态分析检测垂悬指针.

4.5 针对CPS系统的攻击预防措施

CPS系统在应用层面临的攻击主要通过软件系统漏洞发起的, 而在这方面的攻击检测、预防技术主要通过专门模块(如入侵检测模块)实现[43, 44].不同于常规软件, CPS系统可以有独立的模块检测攻击者的恶意输入.如通过神经网络或支持向量机等分类器, 采用数据挖掘、数据融合等方式对不同来源的数据进行分析.利用模式识别方法分析当前输入[43].在攻击检测中, 由于恶意输入的特征一般会与常规输入有着较大的差异, 该方法也能够在一定程度上检测并预防use-after-free或double free攻击.第2类方法[44]则是侧重于身份认证, 该方法基于的前提是攻击者无法获得证明用户身份的证据, 如密码、认证证书等.相比于针对CPS特征的攻击防御方法, DangDone则是侧重于在代码层面加固系统本身, 避免CPS系统内部的缺陷、漏洞暴露出来.

5 总结

在本文中, 我们针对CPS系统的垂悬指针问题提出了基于中间指针的垂悬指针预防方法.它先通过静态和动态的方法检测潜在的垂悬指针, 再在编译时通过程序转换防止这些指针被利用.我们通过测试11个实际漏洞和5个CPS系统中人工植入的漏洞评估了它在预防use-after-free和double-free方面的有效性, 并使用SPEC CPU Benchmark测试评估了运行时开销.实验表明了该方法的有效性和可以忽略不计的运行时开销.在未来, 我们计划进一步检测垂悬指针和未被发现的缺陷, 如通过模糊测试的方式检测垂悬指针.此外, 与大多数现有的预防措施类似, 都可能将use-after-free和double-free问题被转换为空指针解引用问题, 这也会影响CPS系统的可用性, 所以我们计划通过容忍空指针解引用来增强该方法.

参考文献
[1]
Sridhar S, Hahn A, Govindarasu M. Cyber-Physical system security for the electric power grid. Proc. of the IEEE, 2011, 100(1): 210-224. http://d.old.wanfangdata.com.cn/Periodical/dlzdhsb201712010
[2]
Bu L, Zhang T, Chen X, et al. Model-Based construction and verification of cyber-physical systems. ACM SIGSOFT Software Engineering Notes, 2018, 43(3): 6-10. [doi:10.1145/3229783.3229793]
[3]
Lee E. Cyber physical systems: Design challenges. In: Proc. of the 11th IEEE Int'l Symp. on Object and Component-Oriented Real-Time Distributed Computing (ISORC). IEEE, 2008. 363-369.
[4]
Xu W, Li JR, Shu JL, Yang WB, Xie TY, Zhang YY, Gu DW. From collision to exploitation: Unleashing use-after-free vulnerabilities in linux kernel. In: Proc. of the 22nd ACM SIGSAC Conf. on Computer and Communications Security. ACM, 2015. 414-425.
[5]
Ye JY, Zhang C, Han XH. POSTER: UAFChecker: Scalable static detection of use-after-free vulnerabilities. In: Proc. of the ACM SIGSAC Conf. on Computer and Communications Security. 2014. 1529-1531.
[6]
Feist J, Mounier L, Potet ML. Statically detecting use after free on binary code. Journal of Computer Virology and Hacking Techniques, 2014, 10(3): 211-217. [doi:10.1007/s11416-014-0203-1]
[7]
Caballero J, Grieco G, Marron M, Nappa A. Un-Dangle: Early detection of dangling pointers in use-after-free and double-free vulnerabilities. In: Proc. of the Int'l Symp. on Software Testing and Analysis. ACM, 2012. 133-143.
[8]
Dhurjati D, Adve V. Efficiently detecting all dangling pointer uses in production servers. In: Proc. of the Int'l Conf. on Dependable Systems and Networks. IEEE, 2006. 269-280.
[9]
Pattabiraman K, Kalbarczyk ZT, Iyer RK. Automated derivation of application-aware error detectors using static analysis: The trusted illiac approach. IEEE Trans. on Dependable and Secure Computing, 2011, 8(1): 44-57.
[10]
Lee BY, Song CY, Jang YJ, Wang TL, Kim TS, Lu L, Lee WK. Preventing use-after-free with dangling pointers nullification. In: Proc. of the Network and Distributed System Security Symp. (NDSS). 2015.
[11]
Younan Y. FreeSentry: Protecting against use-after-free vulnerabilities due to dangling pointers. In: Proc. of the Network and Distributed System Security Symp. (NDSS). 2015.
[12]
The LLVM Compiler Infrastructure. 2020. http://llvm.org
[13]
Standard Performance Evaluation Corporation. SPEC CPU 2006. 2020. https://www.spec.org/cpu2006/
[14]
Afek J, Sharabani A. Dangling Pointer: Smashing the Pointer for Fun and Profit. Black Hat USA, 2007. 24.
[15]
Jackson T, Salamat B, Wagner G, Wimmer C, Franz M. On the effectiveness of multi-variant program execution for vulnerability detection and prevention. In: Proc. of the 6th Int'l Workshop on Security Measurements and Metrics. ACM, 2010.
[16]
Lopes BC, Auler R. Getting Started with LLVM Core Libraries. Packt Publishing Ltd, 2014.
[17]
Steensgaard B. Points-to analysis in almost linear time. In: Proc. of the 23rd ACM SIGPLAN-SIGACT Symp. on Principles of Programming Languages. ACM, 1996. 32-41.
[18]
Andersen LO. Program analysis and specialization for the C programming language [Ph.D. Thesis]. København: University of Cophenhagen, 1994.
[19]
Szekeres L, Payer M, Wei T, Song D. Sok: Eternal war in memory. In: Proc. of the IEEE Symp. on Security and Privacy (SP). IEEE, 2013. 48-62.
[20]
Writing an LLVM Pass. 2020. http://llvm.org/docs/WritingAnLLVMPass.html
[21]
Clang: A C language Family Frontend for LLVM. 2020. http://clang.llvm.org/
[22]
Exploits Database by Offensive Security. Exploits database. 2020. https://www.exploit-db.com/
[23]
[24]
Splint-Secure Programming Lint. 2020. http://www.splint.org/
[25]
Coverity Scan-static Analysis. 2020. https://scan.coverity.com/
[26]
Akritidis P. Cling: A memory allocator to mitigate dangling pointers. In: Proc. of the USENIX Security Symp. 2010. 177-192.
[27]
Berger ED, Zorn BG. DieHard: Probabilistic memory safety for unsafe languages. ACM SIGPLAN Notices, 2006, 41(6): 158-168. [doi:10.1145/1133255.1134000]
[28]
Novark G, Berger ED. DieHarder: Securing the heap. In: Proc. of the 17th ACM Conf. on Computer and Communications Security. ACM, 2010. 573-584.
[29]
Musuvathi M, Park DYW, Chou A, Engler DR, Dill DL. CMC: A pragmatic approach to model checking real code. ACM SIGOPS Operating Systems Review, 2002, 36(SI): 75-88. [doi:10.1145/844128.844136]
[30]
Engler D, Musuvathi M. Static analysis versus software model checking for bug finding. In: Proc. of the Verification, Model Checking, and Abstract Interpretation. Springer-Verlag, 2004. 191-210.
[31]
Berdine J, Cook B, Ishtiaq S. SLAyer: Memory safety for systems-level code. In: Proc. of the Int'l Conf. on Computer Aided Verification. Springer-Verlag, 2011. 178-183.
[32]
Morgado A. Understanding valgrind memory leak reports. 2020. https://aleksander.es/data/valgrind-memcheck.pdf
[33]
Hastings R, Joyce B. Purify: Fast detection of memory leaks and access errors. In: Proc. of the Winter 1992 Usenix Conf. Citeseer, 1991. 125-136.
[34]
Eigler FC. Mudflap: Pointer use checking for C/C+. In: Proc. of the 1st Annual GCC Developers' Summit. 2003. 57-70.
[35]
Serebryany K, Bruening D, Potapenko A, Vyukov D. AddressSanitizer: A fast address sanity checker. In: Proc. of the USENIX Annual Technical Conf. 2012. 309-318.
[36]
Van Der Kouwe E, Nigade V, Giuffrida C. Dangsan: Scalable use-after-free detection. In: Proc. of the 12th European Conf. on Computer Systems, ACM, 2017. 405-419.
[37]
Nagarakatte S, Zhao JZ, Martin MMK, Zdancewic S. SoftBound: Highly compatible and complete spatial memory safety for C. In: Proc. of the ACM Sigplan Notices. 2009. 245-258.
[38]
Xu W, Duvarney DC, Sekar R. An efficient and backwards-compatible transformation to ensure memory safety of C programs. In: Proc. of the ACM Sigsoft Int'l Symp. on Foundations of Software Engineering. ACM, 2004. 117-126.
[39]
Liu DP, Zhang MW, Wang HN. A robust and efficient defense against use-after-free exploits via concurrent pointer sweeping. In: Proc. of the 2018 ACM SIGSAC Conf. on Computer and Communications Security. ACM, 2018. 1635-1648.
[40]
Shin JS, Kwon DH, Seo JW, Cho YP, Paek YH. CRCount: Pointer invalidation with reference counting to mitigate use-after-free in legacy C/C++. In: Proc. of the Network and Distributed System Security Symp. (NDSS). 2019.
[41]
Standard Performance Evaluation Corporation. SPEC CPU 2000. 2020. https://www.spec.org/cpu2000/
[42]
Wang Y, Gao FJ, Situ LY, Wang LZ, Chen BH, Liu Y, Zhao JH, Li XD. Dangdone: Eliminating dangling pointers via intermediate pointers. In: Proc. of the 10th Asia-Pacific Symp. on Internetware. ACM, 2018. 6.
[43]
Su CJ, Wu CY. JADE implemented mobile multi-agent based, distributed information platform for pervasive health care monitoring. Applied Soft Computing, 2011, 11(1): 315-325. [doi:10.1016/j.asoc.2009.11.022]
[44]
Liang HY, Jagielski M, Zheng BW, et al. Network and system level security in connected vehicle applications. In: Proc. of the IEEE/ACM Int'l Conf. on Computer-Aided Design (ICCAD). IEEE, 2018.