如无必要,勿增实体
今天要给大家推荐的论文 Don’t Waste My Efforts: Pruning Redundant Sanitizer Checks of
Developer-Implemented Type Checks 来自USENIX Security 2024,作者包括了G.O.S.S.I.P的老朋友钱志云教授。这篇论文讨论了当前的二进制代码中的sanitizer(特别是用来检查类型转换的sanitizer)是否存在冗余,以及如何去除冗余的检查这一主题。
先回到当年被C++老师支配的恐惧之中,来看一下经典的C++考试里面最喜欢出现的片段之类型转换错误(在安全研究更多地被叫做type confuison):
年轻的时候不懂C++的内存模型,对于类型转换特别是那四大关键字(reinterpret_cast
、static_cast
、dynamic_cast
、const_cast
)颇感头痛。可能是安全研究的需求不同,开发过程中很少会涉及到复杂的对象层级结构(hierarchy),但是如果你读过Clang的代码就会发现,编译器里面对源码中各种对象层级结构的组织,堪比一个巨型企业的管理,复杂而充满了危机。在开发过程中,程序员可能经常会拿到一个对象而不知道它的真实类型是什么,不知道对象的类型就去访问它,就好像庖丁本来是要解牛,结果你拖来的是一只兔子。死板的计算机在这里可不会随机应变,而只是会按照它认为的(实际上是错误的)类型格式去解析这个对象(对应的内存地址上的数据),可想而知,轻则是造成越界读写,重则可能被利用而造成data only attack甚至任意代码执行。
为了解决type confuison这类问题,我们会很自然地想到两种思路,一种是要求程序员不要犯错,错了就拖出去祭天,但这会导致IT企业的人口大面积减少,显然是不合适的;另一种就是执行“防御性编程”,也就是对代码中的错误进行检查,这里检查也分两种类型(这段话怎么这么像领导发言:“我只说两点,第一点可分为2个小点”),一种是用静态代码分析来识别所有可能的转型操作,看其中哪些出错了,但我们都知道静态代码分析的理解能力并不会比程序员强太多,所以第二种方法就是在每个转型操作之前都进行代码插桩,执行精确的动态检查,也就是本文要讨论的sanitizer based check操作。不过想想也知道,动态插桩检查这种方法明显成本太高,堪比出门前就要做个某suan检查一样。那么,有没有什么好的方法来减少这种开销呢?
很简单,并不是任何转型操作前面都需要加入sanitizer based check,如果配合上静态代码分析,把那些能够确认不会有问题的类型转换操作给放过,就会减少很多的开销。不过,传统的静态分析方法还是data flow那老一套,本文的创新之处就在于利用了一类新的信息——代码开发者自定义的custom runtime type information(论文里面写的还是RTTI,但我们觉得应该可以缩写为CRTTI)来辅助进行分析,进一步去除那些冗余的sanitizer based check操作。
RTTI对于熟悉Java的读者应该一点也不陌生,实际上在C++的class对象里面也同样存在,dynamic_cast
就是依赖于RTTI来进行类型转换的一致性检查。但是RTTI在使用中性能比较差,主要原因是它把类型信息按照字符串形式保存,在检查的时候搞了一个string comparison,所以有一些改进的工作会把类型信息转成一个hash integer来提升比较的性能。此外RTTI只支持class而不支持struct。因此,一些第三方的sanitizer自己维护了一套信息来进行类型转换的检查。
下图中的(2)和(3)分别是标准的dynamic_cast
的检查方式和一些第三方的sanitizer的类型转换检查的抽象示意图,注意到这里为了不去改动原有的内存布局(保证二进制兼容性),第三方的sanitizer会另外找一个地方来维护对象的类型信息记录,也就是维护了一张记录表,在执行检查的时候去查这张表。不过这个查表过程也是主要的性能损耗来源,在此前的研究工作HexType中,针对Chromium benchmarks产生了从10.2%到65.7%不等的运行时开销。
仔细看上图,你会发现还有一个情况(4),也就是本文要讨论的核心,这就是前面说过的CRTTI信息。本文主要针对的是Chromium,而作者观察到(大概是Chromium里面有太多潜在的type confusion情况)Chromium开发者实际上已经在很多对象里面手工加入了很多type信息,完全可以用来帮助进行类型转换时期的检查,例如下面这个CVE对应的patch的例子,实际上第15行这个webassembly_obj->IsJSObject()
函数并不是打补丁的时候才加入的,而是在开发过程中就已经加进去了,如果善于利用这类特征,就可以实现非常高效的类型转换检查。
接下来就是“把大象放进冰箱”的介绍:首先找到CRTTI信息,然后把它用起来,结束。开个玩笑,虽然这确实是作者设计的T-PRUNIFY
工具的工作流程(下图)的极简描述,但是我们还是好好深入去了解一下重要的实现细节。
第一步,如何识别CRTTI信息
要识别CRTTI信息,首先请熟读代码!作者去调查了Chrome代码里面100多种对象继承关系,总结了3类CRTTI信息:
存储在基类(base class)的成员变量中:如下图所示,声明了一个
class_type_
成员变量;
直接作为一个常量:如下图,直接通过每个class的
GetType
函数来返回类型信息;
用(一组)函数来返回对象类型(返回的是一个布尔值):如下图(看起来这个风格好繁琐啊)。
第二步,如何利用CRTTI信息
在总结了三类典型的CRTTI信息之后,接下来就是怎么在源代码里面找到这些信息(变量、函数)以及它们被使用的情况。论文的4.2章和4.3章进行了介绍,但是这部分有点太high level了,可能没有接触过Clang(特别是Clang-tools)的同学读起来会觉得有点抽象到云里雾里,所以这就是为什么大家要阅读我们专栏的原因(呸呸呸不要脸):给大家安利一下微软开发者博客的一系列文章,看完了你马上就能理解这部分(实际上我们读到论文第6章讲到具体实现的时候,基本上也验证了这里确实利用了Clang的AST匹配的强大能力)。
https://devblogs.microsoft.com/cppblog/exploring-clang-tooling-part-1-extending-clang-tidy/
https://devblogs.microsoft.com/cppblog/exploring-clang-tooling-part-2-examining-the-clang-ast-with-clang-query/
https://devblogs.microsoft.com/cppblog/exploring-clang-tooling-part-3-rewriting-code-with-clang-tidy/
当然,定位到源代码中的CRTTI信息和它们被使用的情况,只是迈出了找到真相的第一步(最近在听一个儿童侦探广播剧被台词洗脑了),我们还需要进一步确认,在某个类型转换操作执行之前,代码中是否已经进行过(基于CRTTI的)type check(可能是一个,也可能是一组),同时这些check是否完备。这里就需要引入一套fl ow-sensitive and field-sensitive intra-procedural pointer analysis,如果你之前一直关注钱老师组的研究工作你肯定会对这个名字不陌生,实际上在我们介绍过的钱老师的学生、目前在IUB工作的张航老师【印第安纳大学布卢明顿分校(IU Bloomington)张航课题组招生】的诸多研究工作中,这套分析技术一直都在使用。
在实验环节,作者首先调查了典型的C++程序里面CRTTI是否(普遍)存在。注意,下表并不是对代码中所有的class对象进行的统计,而是每个项目随机抽取了10个class来检查,可以看到除了妖娆的Boost,其他这种大型工业化项目基本上都包含了CRTTI信息。同时,表里面有4个项目都存在和type confusion相关的CVE,说明对其进行防御是有必要的(Boost:说什么,没听到,再说一遍?)
接下来是T-PRUNIFY
的准确率,这部分太长,我们总结一下,T-PRUNIFY
在识别CRTTI信息及其使用以及确认一个类型转换操作执行之前是否已经有足够安全的check上都没有误报,也就是说,T-PRUNIFY
不会把无关的变量和函数识别为CRTTI信息,也不会把安全的类型转换操作判定为不安全的,当然,T-PRUNIFY
会有一些漏报,但是因为sanitizer based check的存在,这种漏报也问题不大(还有双重保险)。
也许你要问,T-PRUNIFY
到底减少了多少不必要的sanitizer check?我们从Table 6可以看到,如果和此前state-of-the-art工具也就是HexType
相比,使用T-PRUNIFY
之后基本上能减少一半到75%的动态检查,这样就会带来很明显的性能提升。
我们从下面两个比较中也可以看到,使用了T-PRUNIFY
进行优化后,可以达到和HexType
差不多相同的保护效果,而运行时开销明显减少了。
最后,还是要吐槽一下现在的程序员,动不动就觉得堆硬件堆算力就解决一切问题,像本文这样优秀的优化工作越来越少,所以到时候祭天裁员的时候不挑你挑谁?
论文:https://www.cs.ucr.edu/~zhiyunq/pub/sec24_tprunify.pdf
代码:https://github.com/seclab-ucr/TPrunify (现在还是空的!!!)