漏洞概述
CVE-2018-4990是Adobe在2018年5月修复的一个Adobe DC系列PDF阅读器的0day漏洞。该漏洞为双重释放(Double Free)漏洞,攻击者通过一个特殊的JPEG2000图像而触发Acrobat Reader双重释放,再通过JavaScript对于ArrayBuffers灵活的控制来实现任意地址读写。
攻击者可以通过这个漏洞实现对任意两个4字节地址的释放,漏洞触发前用精准的堆喷射巧妙地布局内存,然后触发漏洞,释放可控的的两块大小为0xfff8的相邻堆块。随后,Windows堆分配算法自动将两块空闲的堆块合并成一个大堆块,接着立即重新使用这个大堆块,并利用这个该堆块的读写能力改写一个ArrayBuffer对象的长度为0x66666666,从而实现任意地址读写。
漏洞细节
代码分析
分析漏洞样本,通过PDF流解析工具PdfStreamDumper可以看到pdf文件里面的objects流。其中第1个object流使用了JavaScript来触发并利用漏洞。通过对该段分析可以知道,JavaScript中的dlldata为PDF阅读器漏洞触发后加载运行的载荷,主要用于提权并执行恶意代码,而之后的JavaScript代码用来进行内存布局和漏洞触发。
上面JavaScript代码中通过两个Array实例sprayarr及a1来进行内存控制,这两个Array在这里构造了大量对象,申请了大量的堆空间来实现Spray布局。再对a1的Array中奇数下标的堆空间进行了释放,借助堆分配算法,Windows堆管理器(Windows Heap Manager)会对这些块进行合并,产生一个0x2000大小的空间,JP2Klib在申请漏洞对象时,会从释放的堆块里面直接复用一个。
下面的代码会先从释放的内存空间中重新使用内存。并且,因为空间较大(由于之前的合并),所以需要分配比原来大一倍的空间,每个数组成员分配一个长度为0x20000-0x24的ArrayBuffer。接着遍历sprayarr可以发现其对应的某一个sprayarr的成员长度被修改为了0x20000-0x24(默认的长度为0x10000-0x24),此时通过超长的sprayarr[i1]即可修改相邻的sprayarr[i1+1]对象的len长度属性,从脚本代码中可以看到长度被修改为了0x66666666,最终通过该超长的sprayarr[i1+1]即可实现全内存的读写。
数据结构分析
由于Adobe DC没有符号表,很多结构也没公开只有自己测试和总结。可以利用PdfStreamDumper对pdf分析dump出需要修改的stream流,在修改dump出的stream流,最后替换实现对内嵌的javascript代码的修改。通过添加一些调试代码,以方便下断和调试。
对Array结构进行分析,可以创建一个Array的实例myContent,将该Array中第0个element赋值为0x1a2c3d4f,以便于内存搜索,之后分别将感兴趣的变量赋值到该Array中即可很方便的定位内存进行分析。通过”s -d 0x0 L?0x7fffffff 0x1a2c3d4f”命令可以定位到0x1a2c3d4f,查到附近的内存可以看到myContent结构的实例。可以看到Array结构每个element占8字节,0x1a2c3d4f对应的是值,后面的0xffffff81对应的为element的类型,所以0xffffff81对应的类型为数值,0xffffff87对应的类型为数组。
现在有了sprayarr的地址0x391b3358,查看该地址的值可以看到该地址也是一个结构用来描述sprayarr数组。通过测试发现0x49d50000地址保存了数组值,0x1000为数组的大小。第0个element没有赋值所以为0,后面的element都赋值为数组。
在代码里sprayarr被两次赋值,第一次为Uint32Array,第二次为ArrayBuffer,那么sprayarr被赋的值应该是ArrayBuffer。查看第一个element 0x49f9e420的值,可以看到连续的内存区域用来保存ArrayBuffer的结构信息,每个结构0x98大小,该结构偏移0xc的值0x49d5a018表示ArrayBuffer保存数据的内存区域。
进入到具体的Arraybuffer内存空间查看(0x49d5a018),可以看到在0x49d5a018前面的0xc字节保存了Arraybuffer的实际内存长度ffe8(0x10000-0x24)。
再看看a1的结构,a1的地址为0x391b3380,a1的结构和sprayarr相同都为Array。0x47c01000为a1保存数据的地址,可以看到基数的element已经被释放。
再查看a1[3]所指向的Uint32Array结构,该结构大小为0x58字节,其中0x3f0为结构的大小(252*4),0x39137388描述下一个结构。
在0x39137388的地址又保存的为0x98大小的结构用来描述实际数据的存放地址,在偏移0xc的地址指向实际数据的存放地址。在0x42638c10前面偏移0xc字节保存了Uint32Array的大小0x3f0,这个头大小应该为16字节。
对应的JavaScript脚本,其中a1[i1][249],a1[i1][250]的值在此时分别为0x0d0e0048和0x0d0f0048。
漏洞调试
设置windbg为默认调试器,对AcroRd32.exe进程使用命令开启页堆”gflags /i AcroRd32.exe +ust +hpa”,附加AcroRd32.exe进程后运行poc文件,windbg将暂停到发生crach的地方。通过栈回溯可以看到释放的调用者是JP2KLib!JP2KCopyRect+0xbad6,证明漏洞很可能在该模块里面。在该模块里面又调用了HeapFree函数,很可能是释放空间引发的异常。
对应的代码为如下代码片段。
该片段F5的代码如下。
通过对关键部分进行整理后如下代码,可以看到这个代码从基地址循环并使用变量count作为空闲内存的计数器,变量mem_base是在此循环中开始的内存地址。可以设置断点来监控mem_base,max_count和count的值。
可以通过如下断点来监控mem_base,max_count和count值的变化。可以看到mem_base的地址为0x47560c08,max_count的值为0xff。可以看到在count为0xfd的时候释放了0xd0d0d0d0的地址。
bp JP2KLib!JP2KCopyRect+0xbaea "dd eax+4 l1; g;"// max_count
bp JP2KLib!JP2KCopyRect+0xbac9 "r eax; r ecx; g;"// eax = mem_base,ecx = count
bp JP2KLib!JP2KCopyRect+0xbad0 "r eax; g;"//free addr
再通过!heap -p -a 47560c08查看基地址0x47560c08的信息,可以看到使用的大小为0x3f4,而while循环可以访问到mem_base ~ mem_base+3fc(4*0xff)区间的内存。两者的差值为8个字节3fc - 3f4 = 8,于是可以借助上述while循环越界访问两个4字节地址并释放,来实现任意释放两个地址。攻击者可以通过内存布局(例如堆喷射)提供的任意两个4字节地址,并实现任意释放。
从前面的代码知道攻击者在漏洞触发前利用精心控制大小(0x400)的堆喷射构造大量对象,然后释放其中的一半,借助堆分配算法,JP2Klib在申请漏洞对象时,会从a1释放的堆块里面直接复用一个。JP2Klib释放对象时会触发漏洞多释放2个4字节的地址,在特殊的构造下释放的地址正好是0x0d0e0048和0x0d0f0048,而这两个地址在堆喷下会被sprayarr申请使用,在触发漏洞的情况下两个地址被释放后合并大小正好是0x20000,在随后攻击者通过以下代码立即将上述合并的堆块重新使用。
这段代码会从双重释放的内存空间中回收已经释放的内存。并且因为内存较大(由于之前的合并),所以需要分配比原来大一倍的空间。在sprayarr2被分配为0x20000-24大小的空间后,由于堆的特性sprayarr中之前被释放的elment的长度就会被修改为0x20000。在此之后,攻击者已经回收了释放的内存,他们接下来需要找出sprayarr中的哪个ArrayBuffer的大小增加了一倍。
接着攻击者查找所需的ArrayBuffer之后利用长度为0x20000-24的ArrayBuffer的读写能力去改写对应ArrayBuffer对象的长度,将其改写为0x66666666。然后利用之前构造的sprayarr数组找到长度为0x66666666的“ArrayBuffer”对象,紧接着将其赋值给一个DataView对象借助DataView来实现任意地址读写。