二进制文件静态分析漏洞挖掘技术-BinAbsInspector
前言
关于如何对源代码进行静态分析漏洞挖掘的工具和教程还是比较多的,至于二进制文件就比较少。去年科恩实验室发了一个开源的针对二进制文件的漏洞扫描工具BinAbsInspector,所以前一段时间也就简单看了一下。我这里会先总结一下《WYSINWYX:What You See Is Not What You eXecute》这篇论文,这篇论文非常经典,BinAbsInspector也是参考它实现的,然后再简单分析一下代码。这篇blog真的非常难写,花了很多时间感觉也不是写的很好,因为这一块的知识比较欠缺也不好学。
论文导读
论文感觉写的有点晦涩,本来其实用大白话说还是比较好理解的,乱七八糟的公式和符号一堆看的头疼,我就尽量提炼一下。
我们首先想分析二进制程序比分析源代码难在什么地方?二进制文件里面全是汇编代码,没有源代码中的变量,因此不好分析。所以第一步就是从汇编代码中恢复出抽象变量(ALoc),通过{区域,长度,起始地址}的三元组来表示。这里“区域”可以有:堆,栈,全局等几种。抽象变量会有一个可能的抽象值(AbsVal)的集合(KSet)。从ALoc到KSet的映射组成了AbsEnv(Abstract environment)。
这里用论文中的例子:
对于这样一个程序,ALoc如下:
在指令L1, 8和指令14处的KSet如下所示:
语法:
(2[1, 9], ⊥)代表数值集{1, 3, 5, 7, 9}以及地址集{(Global, 1), (Global, 3), … , (Global, 9)}
(⊥, 8[-48, -40])代表地址集{(AR_main, -48), (AR_main, -40)}
KSet是如何计算的呢?下图是具体的算法,其中R1和R2是两个大小相同的寄存器,c、c1和c2是显式整数常量,≤和≥表示有符号比较:
比如R1 = R2 + c,假设R2的KSet是(4, 4[4, 12]),c=12,那么R1的KSet就是(16, 4[16, 24])。这个计算的过程在论文中叫做抽象转换器(AbstractTransformer)。
对于单个函数且不存在间接跳转的情况,算法如下:
简单来说就是按照CFG的顺序一直算,算到不动点为止。
接下来就开始考虑存在间接跳转的情况(非上下文敏感)。这个时候需要在调用、结束调用、进入和退出节点之间添加边。例如P的起始地址分别为A,B,在C处调用P,就需要添加三条边:C到A(call→enter);B到C的下一条指令(exit→end call);C到C的下一条指令(call→end call)。
我们再用下面这个程序作为例子:
对于这样一个程序,ALoc如下:
1.call→enter
算法如下图所示(把调用方的实参复制到被调用方的形参):
例如在进入initArray时,此时的KSet如下图所示:
(区域的顺序为Global, AR_main, AR_initArray)
2.exit→end call/call→end call
算法如下图所示(进行一个合并操作):
最终非上下文敏感的过程间的Propagate函数如下图所示,第一个参数从Node n修改成了n到succ的边。
非上下文敏感的精度肯定是远远不如上下文敏感的,那在考虑上下文敏感的情况下如何计算呢?论文中用的是callstring的方法。例如考虑下面这样一个程序:
main—yyy—aaa/bbb—ccc
main函数在0xY处调用yyy函数,yyy函数根据不同的条件会在0xA处调用aaa函数或者0xB处调用bbb函数,aaa函数和bbb函数分别在0xAC处和0xBC处调用ccc函数。
此时就用两个callstring:[0, 0xY, 0xA]和[0, 0xY, 0xB]来表示ccc函数被调用时不同的上下文。
之后论文还谈到了ASI算法(Aggregate Structure Identification),ASI算法是将每一个结构体当成一个给定长度的一系列字节集,依据内存访问方式分解。回到我们最开始的图中的程序,ASI算法恢复出的结构体如下图所示(这个应该还是比较好理解)。和前面的算法结合迭代,得到更精确的AbsEnv。
BinAbsInspector
先参考一下github上的技术细节:https://github.com/KeenSecurityLab/BinAbsInspector/wiki/Technical-Details
前面已经说了这是一个ghidra的插件,ghidra的中间语言是pcode,通过varnode概括寄存器或内存位置,pcode对varnode进行操作,varnode由:地址空间、偏移量、大小组成,是不是听起来非常熟悉?BinAbsInspector里面的ALoc就是把varnode包装了一下,基本可以理解成一个东西,所以其实已经简化了一些工作量了。而AbsEnv就是一个hashmap,把ALoc和KSet关联起来。
先简单过一下每个class的大致功能:
CallGraph.java: 调用图
CFG.java: CFG
ConstraintSolver.java: 通过Z3进行约束求解(感觉作用不是很大,本来也可以disable掉)
GraphBase.java: 提供对图进行操作的函数
InterSolver.java: 过程间分析
Worklist.java: 保存需要处理的CFG
region目录:表示不同的”区域”,例如Heap.java表示Heap,其中boolean类型的valid表示Heap空间是否有效
funcs目录:对C库函数和std函数调用的处理,比如调用free之类释放内存的函数时就需要把Heap空间的valid设为false,而调用malloc之类分配内存的函数时就需要把Heap空间的valid设为true,以检测UAF等漏洞
Context.java: 基于callstring实现上下文敏感的分析
有两个Stack用来存储context,active和pending。从popContext函数中可以看出只有当active stack处理完了才处理pending stack。整个程序运行的流程是从mainLoop中开始的,一个context中的worklist空了之后就调用popContext函数取下一个context。
还是考虑前面解读论文时举的例子:
main—yyy—aaa/bbb—ccc
mainLoop中context一共会switch 10次:(1)main调用yyy,(2)yyy调用aaa,(3)aaa调用ccc,(4)ccc返回,(5)aaa返回,(6)yyy调用bbb,(7)bbb调用ccc,(8)ccc返回,(9)bbb返回,(10)yyy返回。
首先是在initContext函数中将入口点地址插入worklist,正常情况下visit完一条指令将下一条指令加入worklist,除此之外还有处理RETURN指令的时候会将调用点的地址加入worklist;处理CALL指令的时候会将call指令所在的地址加入worklist(这两个一样);处理分支指令时将两个分支的地址加入worklist
ContextTransitionTable.java: 使用Address到callstring数组组成的TreeSet的HashMap,在call/return指令中维护context的转换关系
TaintMap.java: 使用taintSourceToIdMap进行污点跟踪。taintSourceToIdMap是一个source到integer的map,source由callSite,context和function组成
接下来简单解释一下漏洞具体时怎么检测的,一共有两个地方,PcodeVisitor.java对pcode进行visit的时候,以及visit之后(checkers目录)。
visit时(我们看几个比较关键的):
1.visit_LOAD(visit_STORE类似)
1.1: 检查是否存在空指针解引用漏洞
1.2: 对于input1对应的KSet中的AbsVal
1.2.1: region为heap,调用checkUseAfterFree函数检查UAF,如果heap的valid为false,则证明存在UAF漏洞(读取已经被释放的内存)
1.2.2: region为heap或者local,调用checkHeapOutOfBound函数或者checkStackOutOfBound函数检查OOB
1.2.2.1: checkHeapOutOfBound: 如果AbsVal的offset为负或者大于region的size,则证明存在越界漏洞(越界读取)
1.2.2.2: checkStackOutOfBound: 类似
1.3: 设置output的KSet
2.visit_CALL
2.1: 对于external函数(基本是一些C库函数)调用invokeExternal函数去调用env/funcs/externalfuncs中的实现
2.2: 对于std函数调用invokeStd函数去调用env/funcs/stdfuncs中的实现
2.3: 在调用invokeExternal函数或者invokeStd函数之前调用checkExternalCallParameters函数检查函数的参数,是否有MemoryCorruption类的漏洞
2.4: 调用adjustLocalAbsVal函数更新AbsVal
2.5: 新建context,将新context加入worklist,将当前callSite和context放入ContextTransitionTable,将新context或当前context加入active stack或pending stack
3.visit_RETURN
3.1: 设置context的exitvalue(在return时的AbsEnv)
3.2: 根据ContextTransitionTable取出所有callstring,根据不同的callstring获取对应的context并将callsite加入到worklist
还是前面的例子,当处理到ccc中的return指令时,此时的callstring如果是[0, 0xY, 0xA]将0xAC加入到worklist;此时的callstring如果是[0, 0xY, 0xB]将0xBC加入到worklist
4.visit_INT_ADD/visit_INT_LEFT/visit_INT_MULT
这三个指令进行污点跟踪,输入源来自scanf/sscanf/fscanf/fgets/fgetc/rand/recv就表示可能会发生整数溢出,其他算数指令基本上都是对input的KSet进行相应的计算并给output
visit后:
以CWE78为例:对于system/popen/execl/execlp函数(下面以system函数为例),获取toAddress(system函数地址)和fromAddress(BL system指令的地址)和对应的函数callee和caller,调用checkFunctionParameters函数,checkFunctionParameters函数中对于caller的每一个context的每一个函数参数,如果:1.kSet为null并被污点标记;2.kSet不为null,其中存在AbsVal,包含此AbsVal的AbsEnv中的kSet被污点标记。则说明存在命令注入漏洞
总结
其实分析完一遍就知道,如果你是想拿着这个去扫然后就报一堆CVE走上人生巅峰可以说是不可能的。比较现实的还是自己编写规则查找相似漏洞捡漏的玩法,对于开源软件有codeql为代表的工具;对于二进制文件也可以借助IDA等反编译工具提供的脚本。不过相信看到漏洞挖掘完全自动化的那一天不会太远。