一、题目介绍
随着软件产业和软件工程的不断发展,软件包含的程序文件、动态库越来越多,相互之间的依赖关系越来越普遍。
其中一旦某个动态库出现漏洞出现漏洞,都会导致大批量的软件受到攻击。本赛题数据包含387个绿色软件的目标软件集,给定24个CVE。我们需要通过分析目标软件集和给定的24个CVE,识别出包含漏洞的软件模块,并给出引入CVE漏洞的开源组件名称及其版本号。
二、解题思路
2.1 总体思路
我们首先获取绿色软件目录下所有以sys、dll、exe结尾的PE文件,同时根据题目给出的CVE确定影响的组件版本范围;然后根据PE文件名检测是否属于组件本身,如果是组件本身则通过lief库获取PE文件中的resource表获取到文件的版本,将版本标准化NVD上对应的版本并确定CVE后就可以得到答案;另外就是我们发现很多PE文件把组件静态编译进了PE文件,在PE文件的export table中会存在该组件的一些函数,例如获取版本信息的函数,根据导出表中获取版本的函数名结合符号执行或者借助lief库静态查找就可以获取到函数中的版本号;通过对组件的源码进行分析,我们也发现可以通过关键字符串获取到组件的版本号或者对应的CVE;最后就是我们基于CCS上的一篇论文训练了一个二进制代码相似性检测的模型。
2.2 根据资源表获取版本
首先是根据PE文件名确定组件本身,我们根据观察到的现象预定义一个映射表如下,如果文件名中包含ssl关键字,我们则认定该文件就是openssl组件本身,然后通过PE文件的资源表获取其版本信息。
文件名关键字 | 组件名 | |
---|---|---|
ssl | openssl | |
curl | libcurl | |
sqlite | sqlite | |
png | libpng | |
tiff | libtiff | |
xml2 | libxml2 | |
libcrypto | openssl | |
jpeg | libjpeg | |
zlib | zlib |
通过对PE文件的结构进行分析,我们发现PE文件中的Optional Header中存在DataDirectory数据结构,里面存储了资源表的地址,在资源表中存储了文件的版本信息,通过lief库对PE文件的资源表进行解析,可以直接获取到用32个字节存储的版本信息,进行转码和标准化后就可以得到NVD上对应的版本信息。
2.3 根据导出表获取版本
导出表就是该PE文件对外暴露的函数,如果一个PE文件存在某个组件特有的函数,则该PE文件要么是组件本身,要么是通过静态编译将该组件链接进了PE文件中,所以我们只要确定组件和组件特有的函数对应关系,就可以根据导出表确定该组件。通过对组件的分析,我们发现不同的组件会存在不同的获取版本信息的函数,对应关系如下:
组件名 | 获取版本的函数 |
---|---|
sqlite | SQLITE_VERSION_NUMBER或sqlite3_libversion_number |
zlib | zlib_version |
openssl | OpenSSL_version_num |
libcurl | curl_version_info |
libpng | png_access_version_number |
libxml2 | __xmlParserVersion |
另外获取版本的函数一般都比较简单,大多只有一条mov指令和跳转指令。我们主要采用两个方法去获取函数中的版本信息,第一种是通过静态的lief库获取该函数的字节码,并根据版本号的特征获取版本号;第二种是借助符号执行,在函数中每个block的开始处设置为符号执行的起点,block的结束处设置为终点,通过模拟运行后,版本信息会存在寄存器中,获取寄存器中的值就可以获取到版本号。
2.4 根据关键字符串获取版本或CVE
2.4.1 zlib组件
在分析CVE-2005-2096漏洞的时候,涉及到zlib/inftrees.c中的bug,我们在该文件下发现了一个静态常量数组inflate_table,在比较不同版本的inftrees.c时我们发现,当version >= 1.2.0后,每个zlib版本的这个lext数组的最后两位数都是独一无二的,1.2.0版本以前这个数组数值是一定的。我们收集了所有zlib版本大于1.2.0的该数组最后两位数,形成了一个映射表,比如下图最后两位数是[192, 79],对应的zlib版本是1.2.9。该静态常量数组很有可能在含有zlib组件的PE文件中被静态写入成二进制代码,所以我们只要搜索该常量数组就可以获取zlib的版本号以及是否包含CVE。
另外我们拿到了1.1.3以前的zlib的静态常量数组,通过搜索该数组,我们可以通过排除法,确定zlib的版本范围,从而确定是否包含CVE-2002-0059(zlib 1.0.0-1.1.3)
v1.0.5~v1.1.3:
local const uInt cplext[31] = { /* Extra bits for literal codes 257..285 */
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112}; /* 112==invalid */
v0.71~1.0:
local uInt cplext[] = { /* Extra bits for literal codes 257..285 */
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 192, 192}; /* 192==invalid */
v1.0以后,都去除了这个数组:
{0,1,2,4,5,7,8,10,11,12,16,22,23,26}
最后就是携带版本信息的关键字符串”Copyright”,组件源码都会带有版本信息的字符串,在编译时有可能会把该字符串编译进去。我们发现在PE文件中存在字符串inflate 1.1.4 Copyright 1995-2002 Mark Adler就可以确定版本信息。首先通过搜索人名Mark Adler可以确定该字符串是否存在,然后提取前面的版本信息。
2.4.2 openssl
CVE-2021-23840是openssl的一个CVE,为了修复该CVE,代码添加了一个抛出ERROR的define:OUTPUT_WOULD_OVERFLOW,我们注意到openssl每过一定版本,都会新增一些抛出ERROR的define,所以我们通过在PE文件中查找新增define的字符串来确定该文件是否包含某个CVE。可以确定的CVE有CVE-2020-7043和CVE-2021-23840。
2.4.3 libjpeg
含有该组件的文件可能包含以下字符串,我们可以通过版本信息或者年份来确定libjpeg的版本,从而映射CVE 。由于libjpeg的版本更迭比较慢,所以可以通过年份来确定版本,所以我们搜集并形成了一个年份对应版本的映射表。通过年份或者直接的版本信息,我们可以拿到libjpeg的版本,因为8b对应的年份是重复的,当遇到2010年时,我们就会在前后寻找是否带有版本信息,如果有,会取出来作为libjpeg的版本。
2.4.4 libpng
通过包含版本信息的Copyriht之后的作者信息以及日期,可以确定libpng的版本或者CVE。例如libpng1.6.35含有以下字符串,通过搜索该字符串中包含的作者信息以及日期,可以确定libpng的版本号以及版本号所处的范围,进而确定CVE。
2.4.5 sqlite
我们发现sqlite中有一个manifest.uuid,文件,每个版本都会有一个唯一的id,根据该id我们就可以确认版本号,从而确定CVE。
三、探索与尝试
3.1 总体思路
我们参考了CCS 2016的文章《Scalable Graph-based Bug Search for Firmware Images》。这篇文章针对IOT设备中的组件,实现在不同编译平台或者版本下的二进制相似性检测。
该文章首先提取二进制文件中每个函数的ACFG(Attributed Control Flow Graph),然后对其进行encoding构建一个漏洞库。当输入一个函数时,采用相同的方式提取ACFG并encoding,然后计算两个编码后向量的欧式距离,如果少于某个threshold,则认为两个函数是同一个函数。
3.2 提取ACFG
ACFG(Attributed Control Flow Graph)是CFG的属性集合,其包含了两种类型的特征,统计特征和结构特征。统计特征描述了基本块内的局部特征,结构特征捕获了基本块在CFG中的位置特征。我们基于IDA Pro的IDAPython提取了每个文件内所有函数的ACFG。
3.3 实验结果
我们搜集了题目中所有CVE的组件的部分版本的源代码,然后都使用VS2015进行编译,得到相应的PE文件。然后使用IDA PRO7.0的脚本完成每个PE文件的ACFG提取,结果如下:
使用我们的本地服务器进行训练,得到一个特征库。然后我们使用其他版本含CVE的组件文件对模型做测试,随机选择了50个函数进行搜索,结果如下:
每个函数输出10个库中最相似的函数,如果完全一致则distance为0,这样就可以知道被测函数是与库中哪个函数相似,从而可以通过函数的标识确定是哪个组件,包含哪个CVE。结果显示50个函数都能搜索到distance小于0.1的相似函数,从而判断其组件和CVE。
四、展望与总结
对于通过关键字符串获取组件的版本或者CVE,目前我们主要采用人工观察GitHub上源码的变化来确定关键字,后续可以通过NLP技术来发现源码的变化。此外也可以通过收集更多的训练数据,构建更全面的漏洞库,更好的实现二进制代码的相似性检测。