使用静态分析和Clang寻找Heartbleed漏洞
背景
周五晚上我斟了一杯麦卡伦15年威士忌,决定编写一个能够检测Heartbleed漏洞的静态检查器。我决定将其实现为树外Clang分析器插件,先在包含Heartbleed漏洞特征的小型函数上进行测试,最后在存在漏洞的OpenSSL代码库上进行验证。
Clang项目随编译器提供了分析基础设施,通过scan-build调用。它会挂钩现有的make系统,将clang分析器插入构建过程,分析器使用与编译器相同的参数调用。这样,分析器可以"访问"在clang下编译的每个编译单元。Clang分析器存在一些限制,我将在讨论部分提及。
策略
Coverity最近提出了一种静态识别Heartbleed的方法,即将ntohl和ntohs调用的返回值标记为输入数据。对像OpenSSL这样的大型状态机进行静态分析时,分析器要么需要了解状态机以跟踪整个程序中受攻击者影响的值,要么需要在程序中添加注解来指示输入数据的使用位置。
我们的clang分析器插件应该:识别程序中通过ntohl写入变量的位置,对其进行污点标记,然后在这些污点值被用作memcpy的大小参数时发出警报。但这样可能产生误报,我们还需要在调用位置检查污点值的约束条件:如果污点值没有受到程序逻辑的约束,并且被用作memcpy参数,就报告漏洞。
Clang分析器细节
Clang分析器采用符号执行来分析C/C++程序。其底层对程序状态进行符号/抽象探索,这种探索是流敏感和路径敏感的。分析器为每条程序路径维护一个"状态"对象,其中包含该路径上程序执行的约束和事实。
当分析器遇到如下代码片段时:
int data = ntohl(pkt_data);
if(data >= 0 && data < sizeof(global_arr)) {// CASE A
} else {// CASE B
}
状态在if语句处分裂为两个不同状态A和B。在状态A中,data有特定范围的约束,在状态B中则有相反的约束。
实现
分析器作为C++类实现,通过定义不同的"check"函数来接收分析器探索程序状态时的通知。我们的实现分为三个阶段:
- 识别ntohl/ntohs调用
- 污点标记这些调用的返回值
- 识别污点数据的无约束使用
通过checkPostCall访问器实现前两个阶段:
void NetworkTaintChecker::checkPostCall(const CallEvent &Call,
CheckerContext &C) const {// 识别ntohl/ntohs调用并污点标记返回值
}
通过checkPreCall访问器实现第三阶段:
void NetworkTaintChecker::checkPreCall(const CallEvent &Call,
CheckerContext &C) const {// 检查memcpy调用中是否使用了无约束的污点值
}
实现陷阱
OpenSSL实际上不使用ntohs/ntohl,而是使用重新实现字节交换逻辑的n2s/n2l宏。此外,clang在创建OpenSSL的AST时会将ntohs调用替换为__builtin_pre(__x),这没有标识符信息。
解决方案输出
在演示程序和OpenSSL上的测试结果:
$ cat demo2.c
# 示例代码包含ntohl和memcpy调用$ ../docheck.sh
demo2.c:30:7: warning: Tainted, unconstrained value used in memcpy size
1 warning generated.
分析器成功在OpenSSL中存在Heartbleed漏洞的两个位置都发现了问题。
讨论
该方法需要改进,我们对污点值是否"适当"约束的推理还很粗糙。有时这是最好的选择——如果分析不知道特定缓冲区的大小,向分析人员显示"这个值可能大于5000并被用作memcpy参数,这样安全吗?"就足够了。
我不喜欢clang分析器基于AST操作的局限性,但很喜欢其路径约束接口。一旦理解如何将问题转化为询问状态新约束是否可行,编写新分析就变得很直接。
编辑:代码已发布到Github
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码