Group of Software Security In Progress

GoSSIP @ LoCCS.Shanghai Jiao Tong University

CRCount: Pointer Invalidation With Reference Counting to Mitigate Use-after-free in Legacy C/C++

作者:Jangseop Shin∗, Donghyun Kwon∗, Jiwon Seo∗, Yeongpil Cho†‡ and Yunheung Paek∗

单位:Seoul National University

会议:NDSS 2019

原文:https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_05A-4_Shin_paper.pdf


本文提出了一个基于引用计数的内存管理系统CRCount来缓解C语言或者老式C++代码中存在的use-after-free漏洞

Use-after-free

use-after-free漏洞是近年来影响最为广泛的内存安全漏洞之一,use-after-free漏洞指的是程序试图访问一块已经被释放掉的内存从而导致数据破坏,敏感数据读取及其后续危害。

相关工作

显式地使指针失效 在检测到某一块内存被释放时,立刻将指向这一块内存的所有指针标记为失效,任何通过这些指针来访问数据的行为表示检测到use-after-free。缺点是性能较差,内存开销比较大。

隐式地使指针失效 在检测到某一块内存被释放时,并且不会立马将这块内存释放掉,而在一段时间后或者积压的内存到达一个阈值时再释放。这使得即使产生Use-after-free也避免了对新申请的内存块的操作。缺点是内存开销大。

锁机制 对申请的每一块内存加锁,只有正确的拥有访问权限的指针才可以访问该块内存。缺点是会产生很多误报。

安全的内存管理框架 被释放掉的内存被确保只会被分配给同类型的内存对象,这样即使产生use-after-free,危害也被限制在一定范围。缺点是不能根除use-after-free漏洞。

垃圾回收机制 由C#,java等托管代码提出了内存管理方式,缺点是不能部署在C,C++代码上。

智能指针 由C++11提出来的能适用于C++的内存管理方案,缺点是老版本的C++代码需要重写才能支持智能指针,也不能部署到C代码上。

污点跟踪 将内存分配产生的指针作为污点源,跟踪被污点所污染到的所有指针从而找到与该块内存相关的所有指针。在内存释放时将这些指针标记为失效。缺点是开销大。

基于硬件的方法 依赖于硬件去跟踪每个指针的传播或者依赖于硬件去跟踪每块内存的状态。

系统设计

reference counting

这是内存管理中一个十分常见的方法,即为每个内存对象分配一个引用计数,每当增加一个指针指向这个内存对象时,这个引用计数会加1,当引用计数减少为0时,代表该程序无法通过任何一个指针访问到该内存对象,则该内存对象可以被释放掉。引用计数方法的关键在于,如何准确地实时记录指针的失效和传播。

上面这个例子展示了在记录内存对象引用计数时的两个难点。第5,8行分别申请了两块内存对象A和C,第7行的内存对象B为第4行分配的内存的一部分。这时ptrA,ptrB,ptrC分别指向这三个对象,ABC的引用计数为1。第10行ptrA被赋值给ptrB, ptrC中的成员,这时A的引用计数为3。第15行ptrA被改写为NULL,这时A的引用计数减少1,变为2。第19行,第4行分配的内存被释放,则ptrB指向的内存无效,第20行ptrC的成员被覆盖。这时A的引用计数应该减少为0。但是由于第19行释放的内存并不等于ptrB指向的内存,所以无法直接知道ptrB失败。第20行赋值的时候使用的共用体中的另外一个成员,虽然在实际上是同一块内存,但是由于别名问题的困难,导致识别这种赋值也成为一个难点。

系统概况

作者的系统设计如上图所示,作者使用llvm对源代码生成的程序进行插桩,插桩的目的为尽可能准确的发现所有的指针传播与销毁。在运行时,这时插桩的代码与作者开发的运行库进行交互来检测user-after-free。运行库维护了一个位图用来记录所有8字节对齐的内存处是否存放着一个指针以及内存对象的状态,如内存对象引用记数,内存对象是否应该被释放的标志位。

llvm instrumentation

llvm插桩算法如上所示,作者遍历程序IR中的所有的内存写入指令。如果存入的值不是指针类型并且也不是从指针类型转换而来,作者认为这样的store指令不会增加引用计数。但是这样的指令可能会覆盖之前的指针从而导致指针被销毁,因此需要进行后向搜索来查看该位置是否存放着一个指针。此外如果写入的值和原来的值相同的话,则不需要进行操作。如果有新的指针产生或者旧的指针被销毁,则在这个store指令处插桩,实时的更新内存对象引用计数。但是在这里,我认为应该是在指令后插桩似乎更合乎常理。

runtime library

为了实时更新内存对象引用计数,作者另外hook了标准库的几个方法。他们的作用分别为:(a) 在申请内存时向metedata中添加内存对象条目 (b) 在memset,memcpy等可能发现内存写操作时检查是否有指针产生或者销毁 ©在内存被释放时不立刻释放内存而是减少引用计数 (d)在函数结尾处回收局部变量产生的指针。 Runtime library维护了一个位图来记录整个内存区中哪些位置存储着指向内存对象的指针。此外还维护着一个元数据结构用来记录每块申请的内存的状态,包括他的基地址,引用计数,以及一个标志位来批示该内存块是否应该被销毁。该位在引用计数减少为0时被置位。

评估

作者的实验环境为:Intel Xeon® CPU E5-2630 v4,64G内存,ubuntu16.04。

作者选取用来测试的benchmark SEPC CPU2006 如上图第一列所示。用来比较的其他工具包括:DangSan,Oscar,BDW GC。

ptr stores by inst

作者比较了CRCount和DangSan记录的写入指针写入指针的数量,可以看出CRCount记录的指针数量要比DangSan要多,这里可能反映出CRCount比DangSan更加准确地记录了指针的产生与销毁。

Performance

上图展示了CRCount及其他系统在benchmark SPEC CPU 2006上的单线程性能比较。可以发现在大多数的benchmark测试集中CRCount优于其他系统。对于一些测试集如povray中,性能比较差,作者认为原因是有大量的指针在store指针中被写入,导致插桩的开销巨大。

上图展示了CRCount及其他系统在benchmark PARSEC 18个测试集上的多线程性能比较。x轴为线程数。其中黑色线条为baseline,但是作者并没有指出baseline具体是什么,猜测为正常运行情况。对于barnes的异常情况,作者解释为barnes库只申请了少量的内存,但是有大量的指针产生销毁操作,导致大量的原子操作开销。对于其他大部分库,CRCount表现和其他系统差别不大。

memory overhead

上图为CRCount的内存开销对比。测试集为SPEC CPU 2006。CRCount的平均开销为9.7%,而DangSan为126.4%,Oscar为61.5%,BDW GC为125.6%。在内存开销方面,CRCount占优。

但是在多线程的测试中,CRCount在一些测试集中并没有太大的优势,作者在这里也没有作深入的探讨。

内存泄漏

作者还做一个关于内存漏洞的实验,从图中红色 线条可以看出,内存漏洞是可以忽略的。这里作者使用的方法是定时的扫描位图,然后确定是否还有指针继续指向该内存。但是有一个缺点是如果没有记录到,则该内存泄漏了也无法被跟踪到。

抵抗use after free

作者选择了6个已知的use after free或者double free的漏洞来观察该漏洞是否可以被CRCount发现。实验结果表明,在为CRCount加上检测后可以检测到这些漏洞。

评价

这篇论文没有什么太大的创新性,引用记数是已经被熟练使用的技术方法,作者不过是将这种方法重新部署到了C代码上。而且在应用上也没有什么太大的创新的地方,比如跟踪指针的产生和销毁处,使用的方法都是很容易想到的。

另外有一点,作者没有讲清楚如何对待指针参与的算术运算。比如对于”pp = p + offset”这种情况,作者未做过多阐述。

值得一提的是作者的实验方法,可以说是十分详细,无论是性能还是内存开销,都做得十分详细,值得我们在做实验中学习。

对于引用计数的方法,我们自己开发的工具孤挺花的的字符串加密中,感觉可以有应用的地方,可以尝试将这一方法应用到孤挺花中。