导语:在50天之内,我们在Adobe Reader中找到了50多个新漏洞。平均每天都能发现1个漏洞,这种速度对于人工研究来说是难以实现的。在本文中,我们详细说明了完整的测试过程。同时,向大家介绍一种新颖的方法,能够扩大搜索范围,并改进WinAFL。
一、概述
2017年,是漏洞领域的一个转折点,在当年报告的新增漏洞数量约为14000个,是2016年的两倍。其中的一个可能的原因,是自动漏洞查找工具(也被称为模糊测试工具,Fuzzer)越来越受欢迎。
模糊测试工具的出现并不是新闻,这些工具已经存在了二十多年。但是,目前的模糊测试工具已经长大,他们的能力更强、更容易使用、整体更加成熟。尽管如此,使用模糊测试工具在某种程度上仍然被认为是一种“黑暗艺术”,许多研究人员并不乐于使用模糊测试工具,因为他们认为这些工具非常麻烦。
考虑到上述情况,我们很自然的会产生一些问题:尽管有更多的研究人员正在使用模糊测试工具来发现更多的漏洞,但所有研究人员都会使用模糊测试工具吗?有多少漏洞还静静挂在那里,只等待着第一个研究人员按下“FUZZ”的红色大按钮?
为了找到答案,我们创建了一个最为普通的实验环境。我们使用最常见的Windows模糊测试框架之一——WinAFL,并针对世界最受欢迎的软件产品之一——Adobe Reader进行测试。我们预先设置了50天的测试时限,首先对代码进行逆向工程,寻找潜在的易受攻击的库,然后编写Harness,最后运行模糊测试工具。
测试的结果让我们大开眼界。在50天之内,我们在Adobe Reader中找到了50多个新漏洞。平均而言,每天都能发现1个漏洞,这种速度对于人工研究来说是难以实现的。
在本文中,我们详细说明了完整的测试过程。同时,向大家介绍一种新颖的方法,能够扩大搜索范围,并改进WinAFL。最后,我们还会分享在此过程中得到的经验和体会。
二、背景介绍
2.1 关于WinAFL
AFL是一种引导式的通用模糊测试工具,具有非常可靠的实现和巧妙的启发式方法,已经被证明能够在真实软件中非常成功的发现有效的漏洞。
WInAFL是AFL在Windows上的分支版本,有Ivan Fratric(Google Project Zero)创建和维护。Windows版本使用不同风格的检测方式,使我们能够针对封闭的源二进制文件进行检查。
我们建议各位读者阅读AFL技术论文,其中详细介绍了AFL的工作原理。同时,其中还指出了该工具的缺点,并帮助用户在出现问题时进行调试。
我们发现,WinAFL在查找文件格式错误方面非常有效,特别是针对压缩的二进制格式(图像、视频、压缩包)。
2.2 针对Acrobat Reader DC的攻击
要测试Acrobat Reader DC,最简单的一个起点就是核心可执行文件AcroRd32.exe。这是一个相对较小的AcroRd32.dll的包装器(Wrapper),大小约为30MB。AcroRd32.dll中有很多代码,其中包含PDF对象的解析器,但其中也有很多GUI代码(这部分代码,通常不是我们要寻找漏洞的位置)。
我们知道,WinAFL在二进制格式方面的处理更好,因此我们决定集中精力,只攻击特定的解析器。这里的挑战在于,我们如何找到解析器,并为其编写Harness。在稍后,我们会详细解释什么是Harness。
我们希望一个具有最小依赖关系的二进制格式解析器,我们可以加载这些解析器,而不再需要加载整个Reader进程。
我们探索了Acrobat文件夹中的DLL,发现其中的JP2KLib.dll适合所有类别:
JP2KLib.dll是JPEG2000格式的解析器,它是一种复杂的二进制格式文件(753 KB),并且具有非常重要的导出函数。
我们的研究是在以下版本进行的:
· Acrobat Reader DC 2018.011.20038及更早版本
· JP2KLib.dll 1.2.2.39492版本
2.3 关于目标函数
目标函数(Target Function)是WinAFL中用于描述作为模糊测试过程入口点的函数的术语。该函数在fuzz_iterations循环中被调用,每一次都会改变磁盘上的输入文件。该函数必须:
1、打开输入文件,读取文件,解析输入并关闭文件。
2、能正常返回,不抛出C++异常或调用TerminateProcess。
要寻找到这样的函数,是非常困难的。所以,在针对复杂软件进行模糊测试时,我们通常需要编写Harness。
2.4 关于Harness
Harness是一个小程序,可以触发我们想要模糊测试的功能。Harness中包含将要作为目标函数的函数。以下是WinAFL存储库中gdiplus小型Harness的示例:
Main的第一个参数是一个路径。在函数中,我们会调用Image::Image解析器,这是我们想要模糊的API。需要注意的是,在错误的情况下,我们不会终止进程。在最后,将会释放所有资源。
对于有记录的API,这一过程相对容易。我们可以使用借助官方文档,复制示例代码或编写简单程序。但其中的乐趣在哪里呢?
我们选择的目标是Adobe Reader,这是一个封闭的二进制文件。针对这种类型的目标,要编写Harness,其步骤如下所示:
1、找到我们想要模糊的功能。
2、对其进行逆向工程。
3、编写一个调用反向API的程序。
4、重复上述步骤,直到获得一个功能齐全的完整Harness。
在下一章中,我们会详细描述如何对JP2KLib进行逆向工程,并为其编写了一个Harness。此外,还在下一章中分享了一些经验。如果读者只对模糊测试的方法感兴趣,可以跳过下一章的阅读。
三、编写JP2KLib.dll的Harness
在开始对JP2KLib.dll进行逆向工程之前,我们首先检查了该库是开源的,还是具有公共符号的,这样做会节约大量的时间。但是,在我们的示例中,没有那么幸运。
由于我们希望我们的Harness与Adobe Reader使用JP2KLib的方式尽可能相似,那么要做的第一件事就是找到一个触发我们想要进行模糊测试的行为的PDF文件。这样一来,我们就能够轻松找到程序的相关部分。
实际上,我们找到了大量的PDF文件来测试产品。我们使用了字符串“/JPXDecode”,这是JPEG2000的PDF过滤器。当然,也可以使用Google搜索示例文件,或者使用Acrobat Pro / Phantom PDF生成测试用例。
提示1:阅读器有一个沙箱,有时会进行非常烦人的调试或分类操作,但它可以被禁用。可以参考:https://forums.adobe.com/thread/2110951
提示2:我们打开PageHeap,以便辅助进行逆向工程,因为该功能有助于跟踪分配的位置和大小。
我们从示例中提取了jp2文件,于是便可以将其用于没有PDF包装器的Harness中。该文件将用于Harness的测试输入。
现在,我们有一个最小化的可用示例,接下来使用“sxe ld jp2klib”在JP2KLib.dll的load事件上放置一个断点。当走到断点时,我们在JP2KLib的所有导出函数上放置了一个断点命令。该断点命令记录调用的栈、前几个参数以及返回值:
bm /a jp2klib!* “.echo callstack; k L5; .echo parameters:; dc esp L8; .echo return value: ; pt; ”
我们加载了示例PDF,并得到以下输出:
JP2KLibInitEx是加载JP2KLib后调用的第一个函数。我们注意到,JP2KLibInitEx只接受一个参数,具体如下:
可以看到,它是一个大小为0x20的结构,其中包含指向AcroRd32.dll中函数的指针。当我们遇到未知函数时,我们并不急于对其进行逆向,因为我们不知道它是否会被目标代码所使用。相反,我们将每个地址指向一个唯一的空函数,我们称之为“nopX”(其中X是一个数字)。
我们现在有足够的信息,开始编写我们的Harness框架:
1、从命令行参数获取输入文件。
2、加载JP2KLib.dll。
3、获取指向JP2KLibInitEx的指针,并使用8个nop函数结构来调用它。
我们使用LOAD_FUNC作为方便的宏。我们还有一个用于创建nop函数的NOP(x)宏。
接下来,编译并运行sample.jp2,它可以成功工作!
让我们继续。然后,我们转到下一个函数JP2KGetMemObjEx,该函数不带任何参数,因此我们只需调用它并保存返回值即可。
接下来的函数JP2KDecOptCreate也不带任何参数,我们采用同样的方法,调用并保存返回值。但是,我们注意到JP2KDecOptCreate在内部会调用nop4和nop7,这意味着我们需要实现它们。
接下来,需要了解“nop4”的作用。我们在原始函数指针上放置了一个断点AcroRd32!CTJPEGDecoderRelease+0xa992,并继续执行:
接下来会流向:
经过几个步骤后:
事实证明,nop4是malloc的一个轻便包装器。我们可以在Harness中实现它,并使用“nop4”替换掉它。接下来,再次对nop7重复上述的过程,最终发现它是memset。经过仔细分析,发现nop5和nop6分别是free以及memcpy。
接下来的函数,JP2KDecOptInitToDefaults需要使用一个参数调用。该参数是JP2KDecOptCreate的返回值,所以我们将这个值传递给它。
下一个函数JP2KImageCreate不带参数,因此我们对其进行调用,并保存返回值。
目前,我们的Harness看起来像是这样:
下一个函数是JP2KImageInitDecoderEx,需要5个参数。
我们找到了5个参数中的3个,分别是JP2KImageCreate、JP2KDecOptCreate和JP2KGetMemObjEx的返回值。
在这里,注意到第三个参数指向vtable。于是,采用了与之前相同的技巧,我们创建一个具有相同大小的结构,指向“nop”函数。
第二个参数指向另一个结构,只是这次它似乎不包含函数指针,因此我们决定发送常数值0xbaaddaab。
此时,代码如下所示:
我们跑完了Harness,很快就到达了nop10。在Adobe Reader的相应函数上设置了一个断点,并进入以下调用栈:
在IDA上查看JP2KCodeStm::IsSeekable,具体如下:
通过查看WinDbg,我们可以看到偏移量为0x24的JP2KCodeStm中包含我们的vtable,偏移量0x18的位置包含0xbaaddaab。我们可以看到,JP2KCodeStm::IsSeekable从vtable调用函数传递了0xbaaddaab作为第一个参数,所以它基本上是我们的vtable函数#7的一个精简包装器。
按道理,每个解析器之间都会有一些区别,但通常它们的输入流可能都位于熟悉的文件界面(例如FILE / ifstream)。通常,它是一种抽象底层输入流(网络/文件/内存)的自定义类型。因此,当我们看到如何使用JP2KCodeStm时,一切就都非常清晰了。
回到我们的例子,0xbaaddaab是流对象,vtable函数在流对象上运行。
我们打开IDA,并查看了所有其他JP2KCodeStm::XXX函数。
它们都非常相似,所以我们继续创建自己的文件对象,并实现了所有必要的方法。最终代码如下所示:
我们会对JP2KImageInitDecoderEx的返回值进行检查,从而确保不会出现错误。在我们的案例中,JP2KImageInitDecoderEx成功返回了0。我们尝试了几次,试图正确实现流函数,最终得到了我们想要的返回值。
下一个函数JP2KImageDataCreate不带函数,其返回值传递给JP2KImageGetMaxRes函数。我们对这两个函数进行调用,并继续前进。
接下来,到达JP2KImageDecodeTileInterleave函数,它居然需要7个参数!其中的3个参数分别是JP2KImageCreate、JP2KImageGetMaxRes和JP2KImageDataCreate的返回值。
在xref之后,使用IDA查看AcroRd32内部,我们发现第二个和第六个参数为空。
接下来,就只剩下第四个和第五个参数了。我们认为,这两个参数取决于颜色深度(8/16),所以我们决定使用恒定的深度值来模糊处理。
最终,我们得到了:
最后,我们调用函数JP2KImageDataDestroy、JP2KImageDestroy和JP2KDecOptDestroy来释放我们创建的对象,并避免内存泄漏。当模糊测试的迭代次数(fuzz_iterations)较多时,这一步对于WinAFL来说至关重要。
大功告成,现在我们就有一个可以使用的Harness了。
在最后一次调整中,我们分离了初始化代码,加载JP2KLib并从解析代码中查找函数。这样能够提高性能,因为我们就不必在每次模糊迭代中为初始化过程浪费时间。我们将新的函数称为“fuzzme”,并且将会导出“fuzzme”(可以在exe文件中导出函数),因为它比在二进制文件中找到相关的偏移量要相对简单一些。
在WinAFL测试Harness时,我们发现WinAFL会生成重复的文件。经过深入探究,我们发现Adobe使用了不同于libc中定义的SEEK常量,导致我们混用了SEEK_SET和SEEK_CUR。
由于显而易见的原因,我们决定不公开分享Harness,但是以学习为目的的安全研究人员可以通过 [email protected] 这个邮箱地址与Yoav Alon取得联系。
四、模糊测试的方法论
1、对Harness进行基本测试:稳定性、路径、超时值
2、模糊测试前环境部署
3、初始语料库
4、初始行覆盖范围
5、模糊循环:模糊测试、检查覆盖/崩溃、cmin与重复
6、分类
4.1 对Harness进行基本测试
在开始大规模模糊测试之前,我们首先会进行一些健全性测试,以确保我们没有做无用功。要检查的第一件事,是模糊测试工具是否能够借助我们的Harness实现新的路径,也就是总路径的数量是否能够稳步上升。
如果路径计数为0,或者几乎为0,我们可以检查是否出现了如下问题:
1、目标函数由编译器内联,导致WinAFL错过目标函数的入口,并导致WinAFL终止程序终止(Abort)。
2、如果参数数量(-nargs)不正确,或调用约定(Calling Convention)不是默认值,也可能发生这种情况。
3、超时值:有时超时值太低,也会导致模糊测试工具过快地终止Harness。解决方案就是提高超时值。
我们让模糊测试工具运行几分钟,然后检查模糊测试工具的稳定性。如果稳定性很低(低于80%),那么我们应该尝试去继续调试。Harness的稳定性非常重要,因为它会影响模糊测试工具的精度和性能。
常见的陷阱:
1、检查随机元素。例如,一些哈希表的实现会使用随机数来防止冲突,但这对于保证准确性来说是不利的。我们需要做的,是将随机种子修改为常数值。
2、有时,软件具有某些全局对象的缓存。我们通常只是在调用目标函数之前执行nop,从而减少这一方面的影响。
3、对于Windows 10 x64环境上的32位应用程序,栈对齐并不总是8个字节。这意味着有时memcpy和其他AVX优化代码的行为会有所不同。这种情况,会直接影响覆盖率。一种解决方案就是在Harness中添加代码,从而对齐栈。
如果上述尝试都失败了,我们可以使用DynamoRIO对Harmess进行指令跟踪,并对输出进行差异化处理。
4.2 模糊测试前环境部署
我们要部署的环境是Windows 10 x64的虚拟机,其中包含8-16个内核以及32GB内存。
我们使用ImDisk工具包在RAM磁盘驱动器上进行模糊测试。我们发现,对于某些注重测试速度的目标,将测试用例写入磁盘上是一个性能瓶颈。
在这里,考虑到性能问题,应该禁用Windows Defender,因为WinAFL生成的一些测试用例是由Windows Defender发现的已知漏洞(漏洞利用:Win32/CVE-2010-2889)。
禁用Windows索引服务(Windows Indexing Service),从而提高性能。
此外,还需禁用Windows Update,因为它会干扰模糊测试(重新启动计算机,并替换模糊的DLL)。
我们为Harness进程启动页堆(Page Heap),因为事实证明,启用后可以发现我们原本无法检测到的漏洞。
下面是运行adobe_jp2k工具的示例命令:
afl-fuzz.exe -i R:\jp2k\in -o R:\jp2k\out -t 20000+ -D c:\DynamoRIO-Windows-7.0.0-RC1\bin32 -S Slav02 — -fuzz_iterations 10000 -coverage_module JP2KLib.dll -target_module adobe_jp2k.exe -target_method fuzzme -nargs 1 -covtype edge — adobe_jp2k.exe @@
4.3 初始语料库
一旦我们有了可以使用的Harness,就可以为其创建一个初始语料库,其来源包括:
1、在线语料库(afl语料库、openjpeg-data)。
2、来自开源项目的测试套件。
3、从Google或DuckDuckGo抓取的内容。
4、以往模糊测试项目的语料库。
4.4 语料库最小化
如果使用相同覆盖范围的大量文件,将会影响模糊测试工具的性能。在AFL中,通过使用afl-cmin最小化语料库来解决这一问题。在WinAFL中,有一个名为winafl-cmin.py工具的端口。
我们获取收集到的所有文件,并通过winafl-cmin.py来运行它们,这样就能产生最小的语料库。
我们可以运行winafl-cmin至少两次,并查看是否得到了相同的文件集。如果得到了两个不同的集合,通常就意味着我们的Harness中存在着不确定性。这是我们尝试使用afl-showmap或其他工具进行调查的内容。
成功完成最小化后,我们将文件集保存为初始语料库。
4.5 初始行覆盖范围
现在,我们有一个最小的语料库,我们想看一下行覆盖范围(Line Coverage)。行覆盖表示我们实际执行的汇编指令。为了获得行覆盖,我们针对每个测试用例都使用DynamoRIO:
[dynamoriodir]\bin32\drrun.exe -t drcov -- harness.exe testcase
接下来,使用Lighthouse将结果加载到IDA:
我们注意到初始行覆盖范围,因为这有助于我们评估模糊测试Session的效果。
4.6 模糊循环
接下来的一步非常简单:
1、运行模糊测试工具。
2、检查覆盖范围和崩溃情况。
3、调查覆盖率、cmin,并重复这一过程。
运行模糊测试工具前,并不需要任何特殊的设置,只需在上述配置环境中运行模糊测试工具。
我们有一个具有以下功能的自动工具:
1、所有模糊测试工具的状态(使用winafl-whatsapp.py)
2、每个模糊测试工具的路径图(使用winafl-plot.py)
3、对崩溃进行分类,并生成报告(将在下一节详细阐述)
4、重新启动挂掉的模糊测试工具。
这些任务的自动化是非常重要的,否则模糊测试就是一个单调乏味并且容易出现错误的过程。
我们要每隔几个小时检查一次模糊测试工具的状态,以及随着时间推移的路径变化情况。如果发现图表平稳,我们就会尝试分析覆盖范围。
我们复制所有模糊测试工具的所有队列,并通过cmin运行它们,最后查看IDA中的结果。我们寻找相对较大且覆盖范围非常小的函数,尝试了解与此功能相关的功能,并主动查找会触发此功能的示例。在JP2K中,这并不是很有帮助,但在其他目标中,特别是文本格式中,会非常有效。
这个阶段非常重要。在一次测试中,我们添加了一个样本,经过几个小时的模糊测试后,将行覆盖率上升至1.5%,并成功发现了3个新的安全漏洞。
然后,我们重复这个循环,直到时间用尽,或者没有看到任何覆盖范围的改变。这就意味着我们必须改变目标,或尝试改进Harness。
4.7 分类
一旦出现了一组导致崩溃的测试用例,我们就会手动检查崩溃以及导致每次崩溃的输入内容。很快,我们就改变了策略,因为有很多重复的内容。于是,我们开始使用BugId自动查找重复项,并使其最小化。这一过程同样使用自动工具来完成。
五、成果
通过上述策略,我们最终得以在Adobe Reader和Adobe Pro中找到53个关键漏洞。
我们针对不同的解析器重复了这一过程,例如图像、流解码器和xslt模块,最终发现了如下的CVE:
CVE-2018-4985、CVE-2018-5063、CVE-2018-5064、CVE-2018-5065、CVE-2018-5068、CVE-2018-5069、CVE-2018-5070、CVE-2018-12754、CVE-2018-12755、CVE-2018-12764、CVE-2018-12765、CVE-2018-12766、CVE-2018-12767、CVE-2018-12768、CVE-2018-12848、CVE-2018-12849、CVE-2018-12850、CVE-2018-12840、CVE-2018-15956、CVE-2018-15955、CVE-2018-15954,CVE-2018-15953、CVE-2018-15952、CVE-2018-15938、CVE-2018-15937、CVE-2018-15936、CVE-2018-15935、CVE-2018-15934、CVE-2018-15933、CVE-2018-15932 、CVE-2018-15931、CVE-2018-15930 、CVE-2018-15929、CVE-2018-15928、CVE-2018-15927、CVE-2018-12875、CVE-2018-12874 、CVE-2018-12873、CVE-2018-12872,CVE-2018-12871、CVE-2018-12870、CVE-2018-12869、CVE-2018-12867 、CVE-2018-12866、CVE-2018-12865 、CVE-2018-12864 、CVE-2018-12863、CVE-2018-12862、CVE-2018-12861、CVE-2018-12860、CVE-2018-12859、CVE-2018-12857、CVE-2018-12839、CVE-2018-8464。
我们在jp2k中发现的一个漏洞,实际上在我们发现之前不久已经被Adobe接收,因为这一漏洞疑似已在野外发现被利用。
当然,Adobe Reader是以沙箱模式运行的,而阅读器保护模式(Reader Protected Mode)极大增加了将沙箱内可进行漏洞利用的崩溃转变成攻陷系统的方法的复杂性,这通常需要额外的PE漏洞利用,例如上面所说的野外利用就是如此。
我们非常喜欢WinAFL,并希望该工具能够被大家更广泛地利用。
在使用WinAFL的过程中,我们还遇到了许多漏洞或功能缺失。我们添加了对这些新功能的支持,并对漏洞进行了修复。其中包括:在Windows 10中增加对App验证程序的支持、对工作程序的CPU关联、修复漏洞、增加一些GUI功能。
各位读者可以在这里查看新的提交版本:
Netanel的提交版本:https://github.com/googleprojectzero/winafl/commits?author=netanel01
Yoava的提交版本:https://github.com/googleprojectzero/winafl/commits?author=yoava333