写在前面:自从7月份开始找工作之后,加上自己又有些其他的杂事,博客也一直没有更新或者查看评论。从今天开始恢复更新,我将不定期的发出我前一年的部分工作成果。本期就先来看一篇论文吧。这篇博客的写作风格参考GOSSIP。
作者:Kai Cheng, Qiang Li, Lei Wang , Qian Chen, Yaowen Zheng, Limin Sun, Zhenkai Liang
单位:Beijing Jiaotong University,UCAS, CAS,State Grid Corporation of China,National University of Singapore
原文:https://ieeexplore.ieee.org/document/8416504
出处:DSN 2018
物联网设备由于硬件结构各异,难以对其固件进行动态分析。因此在本文中,提出了一种静态二进制分析方法DTaint,主要用于检测taint-style漏洞(即输入数据通过某种不安全的路径到达了陷入点,例如缓冲区溢出)。DTaint能够识别指针别名,快进程数据流,数据结构相似性实际数据结构布局。DTaint分析了4个厂商的6个固件,并且发现了21个漏洞,其中14个是未知的0day。
本文贡献:
嵌入式固件的搜集:嵌入式固件不同于常规软件,大部分厂商选择保密。不过,一些基于linux系统的设备固件是可以公开下载的。本文通过网络爬虫,爬取这些网站的固件数据。从12个生厂商的网站中爬取了6529个固件。
嵌入式固件的动态分析:本文使用FIRMADYNE进行动态分析,然而只能运行6529中的670个固件。失败原因是动态分析中无法得到某些自定义的及私有的硬件组件,或者是在boot阶段无法初始化网络设置。
嵌入式固件的静态分析:本文又继续寻找固件镜像是否有对应的源码。即使像基于openwrt这种开源的Linux嵌入式系统开发的固件,公司为了保护商业秘密也选择闭源自开发的代码。数据集中的5023个固件镜像无法得到对应的源码或者文档。
taint-style漏洞的挖掘:一共分三步:1)识别攻击者控制的数据源,如recv();2)识别安全敏感的陷入点,如memcpy();3)找到漏洞以及不安全的数据路径,就是一条从数据源到陷入点的数据路径。困难:数据/代码结构丢失以及编译优化(内联或者宏扩展)
主要就是四个步骤:函数分析,指针别名识别,数据结构相似性识别,跨进程数据流恢复。函数分析阶段,DTaint每个函数通过静态符号执行单独分析,主要是完成提取内存描述变量、数据类型推断,以及定义对;指针别名分析阶段通过定义对以及数据类型推断指针别名;利用数据结构相似性推断两个进程(函数)间的数据流;使用自底向上的方式遍历调用流生成过程内和过程间数据流。
函数分析:通过静态分析将二进制代码转换为中间语言IR(本文中使用VEX IR),并且生成调用流图,图中每个函数都是一个独立的树,树上的节点为block,生成过程遵循每个循环只执行一次的规则防止状态爆炸。同时注意处理函数调用约定,把每个函数的入参和返回值都设置为符号值,在后续的分析过程中进行入参和个数以及类型的推断。
变量描述:DTaint使用内存地址表达方式来描述一个变量。如果通过直接寻址方式使用变量,那么就使用变量的绝对地址进行描述;如果是间接寻址,而且基址是寄存器,偏移量是寄存器或者是立即数,就使用“基址+偏移量”的方式进行描述;Dtaint使用deref来描述内存访问。比如,LDR R1,[R5,0x4C] ===> R1 = deref(R5 + 0x4C).Fig6 展示了woo和foo两个过程间的数据流。(我认为,deref就是用于标识内存操作)
数据类型推断:Dtaint使用数据布局来表述数据结构,以此来发现多个调用图之间的数据流。数据布局取决于原始的数据类型,包括int, char, int*, char*。通过两种方式识别类型:1)标准的C/C++的函数参数定义;2)通过机器指令进行推断(例如STR/LDR一定是要进行间接寻址内存访问,在 LDR R0,[R4,4]中,R4所代表的变量一定是指针类型;CMP R0,8 中R0一定是int)
定义对:DTaint使用(d,u)来描述变量定义,变量在定义对中以具体值或者符号表达式的方式存在。定义对用来发现指针别名以及矫正数据流。(d--变量的描述,u--变量的类型)
指针别名:两个不同变量指向同一段内存称为指针别名,造成指针别名的方式有两条指令MOVE(寄存器间)和STORE(写入内存)。第一种方式可以在静态分析中进行处理;第二种方式DTaint设计了一个算法,DP是事情的定义对,ALIAS用于存储指针别名,DOP用于存储变量所指向的那端内存的定义。首先,我们将所有deref格式并且类型为指针的变量进行处理,提取其基址和偏移量,存入ALIAS数组中;之后,我们将所有deref格式的变量的基址都存入DOP数组中(所有指的是有的会有双重基址,比如deref(deref(arg0+0x58)+0xEC),两个基址arg0和deref*(arg0+0x58)都存入DOP);最后,将DOP中每个变量的每个基址,与ALIAS数组中的基址进行匹配,如果相等,我们就认为这是一个别名,并且我们将d中的基址替换成alias的,生成一个新的定义,添加到DP数组中。(不得不说这篇论文的符号定义缺失,文中的叙述有些模糊,对于这个算法我的理解是,如果两个变量使用的基址相同,我们就把一个变量的基址替换掉,生成一个新的变量,再保存到变量数组里面,此种操作好的一点是不仅能够处理基本类型指针,还能够涵盖结构体或者类的内存对象)
数据结构相似性:此处主要解决跨进程数据流的问题。DTaint使用三元组(b,o,t)作为数据结构的描述(base,offset,type),S(si)描述具有同一根指针的数据结构,si表示有相同基址的数据,n是s中不同的基址个数。两段数据结构相似有两个规则,一个是基址互相包含,地址相同的变量具有相同的type,通过计算相同的数据在总数中的比例计算形似度。
跨进程数据流:DTaint自底向上的将CFG转换为数据流。DTaint使用use-def链和def-use链后向生成数据流。如果发现定义满足调用约定(啥意思?),DTaint更新callee的定义信息,并且把caller中的定义链更新。同时DTaint也会把未定义的use变量推给caller。(这句话就是,当子函数中对某个变量进行了重新definition(有可能是读网络数据或者memcpy),并且父函数中的定义也会跟着改变,如果子函数中有没有经过定义就使用的变量(可能在父函数中定义)返回个caller进行处理)。跨进程代码,有两种影响数据流的方式:1)callee对指针参数或返回值的修改2)caller实参的定义被修改。第一种方法使用算法2进行修复;第二种方式,就是判断use的变量是否在本地函数中有定义,如果有我们直接回复数据流,如果没有,我们就将其返回给caller,在caller中进行处理。
算法2:对于每个返回值,我们之前都是符号化的,我们首先将这些符号化的返回值替换成callee中计算得到的返回值(如果返回值是一个heap上面的指针,DTaint会识别heap值,通过生成heap使用到allocation的调用链的hash进行辨别);如果一个变量能够到达函数结尾处,并且他是通过传参数传进来的,我们会把callee中符号化的入参改为caller中的参数。
我们将整个过程串一下,在拿到一个bin时,我们最先将其转化为IR语言,并且对每个函数单独进行分析,分析的内容有使用内存地址描述变量,推断函数所使用的参数数量和类型,生成每个函数的DU链和UD链。接下来,我们需要进行指针别名的搜索和匹配,以及数据结构相似度的匹配,检查间接调用还原CFG,最后我们自底向下地生成数据流。
使用binwalk进行固件提取,并且使用SIMUVEX生成IR语言,之后DTaint进行污点分析。DTaint使用静态符号执行分析每个函数的立即数和符号变量,约束表达式以及数据类型。本文实现了三个部分:一个静态符号分析模组(900行python),一个数据流生成模组(2200行python),一个漏洞检测模组(650行python)。
作者在一个64bit 4核IntelCPU以及128G的Ubuntu16.04系统上运行工具测试,发现了8个已知漏洞和13个0day漏洞。在时间消耗上可以看到明显优于Angr。(我们在实际测试中也发现Angr过于笨重以及缓慢的事实)
这是一篇发表在Rank2会议上面的论文,纵观全文,写作思路清晰,文笔流畅,通过阅读文章我们可以理清作者所提出的工具的整体脉络。但是从IDEA的新颖程度上讲,虽然将污点分析应用于嵌入式固件分析,博主认为并不够,因为作者所使用的污点分析技术已经在其他领域广泛使用,而且所涉及的算法并未有大幅提高。从工具的效率方面来看,作者确实做了很多工作,比Angr提高了近百倍的效率,但问题在于进行测试的都是已知的函数。并且实验结果博主有些许疑问,为什么只分析了4个厂商的固件?6000多个只分析6个固件?博主认为作者选择了精心构造的数据集,但不影响它成为一篇好文章。