CVE-2012-0158漏洞分析

by Netfairy - 2015-11-07

实验环境 

操作系统:Vmware Windows xp sp1 

Office 版本:Microsoft Office Word 2003 (11.5604.5606) 

漏洞文件:MSCOMCTL.OCX 6.1.95.45

调试器:Windbg:6.12.0002.633 x86 

前言 

Cve-2012-0158 漏洞出现在 MSCOMCTL.OCX,是微软 Office 系列办公软件在处理 MSCOMCTL 的 ListView 控件的时候由于检查失误,导致攻击者可以通过构造恶意的文档执行任意代码, 本文我将分析这个漏洞的起因。 漏洞验证 我从网上拿到了一个样本:May.doc。首先我在虚拟机用 Word 打开这个样本

image1.png

计算器弹出来,接着 Word 就退出了。但我再一次打开这个样本,发现程序报错。

image2.png

所以在测试前记得先备份,因为下面我们多次要打开这个样本。 

漏洞分析 

打开样本弹出计算器,所以 Shellcode 应该会调用 WinExec 函数。用 Windbg 附加 WINWORD.EXE,bp kernel32!WinExec 下断

image3.png

然后运行,把样本拖进 word。

image4.png

程序断在 WinExec 入口点。这时候计算器还没弹出来,看一下 WinExec 的参数,发现是 a.exe。 我去到这个目录,发现它就是一个计算器。看来 shellcode 应该是把系统的 calc 复制到别的 目录再运行它。 

image5.png

WinExec 的返回地址是 0x00127dee。往上翻翻,没发现 shellcode 从哪开始。Kb 打印堆栈 

ChildEBP RetAddr Args to Child
00127b20 00127dee 0cd5a5b0 00000000 0cd5a5b0 kernel32!WinExec
WARNING: Frame IP not in any known module. Following frames may be wrong.
00127b50 275c8a0a 07d8c008 0cd3b868 0001c000 0x127dee
00127b8c 00127c05 1005c48b c7000001 4d032400 MSCOMCTL!DllGetClassObject+0x41cc6
00000000 00000000 00000000 00000000 00000000 0x127c05
重新载入,在 MSCOMCTL!DllGetClassObject+0x41cc0 下断

image6.png

断在这个 call 的上一句,Kb 发现这一层函数的返回地址是 0x 275e701a,它前面一个 call 是

image7.png

接着两次 p,然后搜索内存 

image8.png

在 0x00127b88 发现万能跳转 0x7ffa4512 和 shellcode。并且在

275c8a05 e863fdffff call MSCOMCTL!DllGetClassObject+0x41a29 (275c876d)
之前这里还没有被 shellcode 覆盖,显然,溢出发生在 

275c8a05 e863fdffff call MSCOMCTL!DllGetClassObject+0x41a29 (275c876d)
接着上面,我单步执行 

image9.png

image10.png

是不是很熟悉呢?,到这里我们知道的信息如下: Shellcode 执行发生在下面 call 返回 

275e7015 e8ad19feff call MSCOMCTL!DllGetClassObject+0x41c83 (275c89c7)
溢出发生在 

275c8a05 e863fdffff call MSCOMCTL!DllGetClassObject+0x41a29 (275c876d)
Shellcode 通过 jmp esp 覆盖返回地址利用。顺便 dump 部分 shellcode 

00127b88 12 45 fa 7f 90 90 90 90 90 90 90 90 8b c4 05 10 .E..............
00127b98 01 00 00 c7 00 24 03 4d 08 e9 5a 00 00 00 6b 65 .....$.M..Z...ke
00127ba8 72 6e 65 6c 33 32 00 df 2d 89 8c 1b 81 7d ef 42 rnel32..-....}.B
00127bb8 9d 85 85 d6 4e 99 59 5a 61 d8 54 93 77 77 21 9d ....N.YZa.T.ww!.
00127bc8 4a 62 68 c3 53 a3 83 6a 6b df 5c 5a 8a 1d 2b 4f Jbh.S..jk.\Z..+O
00127bd8 2c 45 28 81 71 f5 40 01 92 8f 05 ba 36 c1 0a 61 ,E(.q.@.....6..a
00127be8 61 61 61 73 68 65 6c 6c 33 32 00 8b 98 8a 31 61 aaashell32....1a
00127bf8 61 61 61 6f 70 65 6e 00 e8 11 02 00 00 6a ff e8
接下来用 IDA 打开有漏洞的 MSCOMCTL.OCX。定位前面那两个关键 call,先看上层 call 

275e7015 e8ad19feff call MSCOMCTL!DllGetClassObject+0x41c83 (275c89c7)

image11.png

首先分配 20 个字节空间 

275C89CA sub esp, 14h ; 分配 20 字节局部空间
然后调用

275C89DA call sub_275C876D 
这个 call 的功能是从样本读取 dwBytes,这里是 0CH 字节返回到 eax 指向的内存地址。读取 到的 0xC 字节如下所示: 

00127b70 43 6f 62 6a 64 00 00 00 82 82 00 00
接着程序 

image12.png

如果读取的 12 字节开始 4 个字节不等于 0x6A626F43h,则跳转到错误处理。否则执行

image13.png

275C89F3 cmp [ebp+dwBytes], 8 

我这里[ebp+dwBytes]为 0x00008282。显然 0x00008282 大于 8,所以程序转到 

image14.png

这里其实就是我们真正溢出的那个 CALL。我们看这个 call 有三个参数 Sub_275C876D(8 字节局部空间起始地址,lpMem,字节数) 【这里字节数是 0x8282】 跟进这个 call 看看,首先

275c877f 6a04 push 4
275c8781 51 push ecx
275c8782 53 push ebx
275c8783 ff500c call dword ptr [eax+0Ch] ole32!CExposedStream::Read (771ca84b)}
读取 4 个字节到[ebp+var_4],返回值和 0 比较,如果小于 0,读取失败,转到失败处理,否 则

image15.png

[ebp+var_4]和[ebp+dwBytes]即 0x00008282 比较,相等则

image16.png

分配 0x8282 字节空间,接着 

image17.png

读取 0x8282 字节到刚才分配的堆空间。接着

image18.png

我们看到:275C87CB rep movsd 再看看此时的 

EDI=0x127b7c 
ESI=0x20e008 (指向前面读取的 0x8282 字节) 
ECX=0x20a0
而我们看

275e7015 e8ad19feff call MSCOMCTL!DllGetClassObject+0x41c83 (275c89c7)
函数的返回地址是 0x127b88

image20.png

所以这里的意思就是把从样本中读取的的 0x8282 字节复制 0x8280 到 0x127b7c,上层函数 的返回地址是 0x275c8a0a。上上层函数的返回地址在 0x127b88。所以 275C87CB rep movsd 所在的这层函数在溢出后能正常返回,上上层函数在返回时候才跳去 执行 shellcode。分析完成,我们看到其实漏洞发生的根本原因在于 

.text:275C89F3 cmp [ebp+dwBytes], 8
.text:275C89F7 jb loc_275D
比较从样本读取过来的数据时候处理错误,这里应该是大于 8 个字节就转到错误处理,它写 成了小于 8 个字节转到错误处理。而这个字节大小是我们可以控制的,所以当我们传递比如 0x8282,那么程序就会读取样本相应位置可控的 0x8282 字节到堆空间,然后把这 0x8282 字 节复制到 0x127b7c 地址,进而覆盖掉函数的返回地址,执行我们的 shellcode。漏洞原因伪 代码表示如下: 

#include <stdio.h>
int main()
{
	sub_275C89C7();
	return 0;
}

sub_275C89C7()
{
	char temp[20]; //分配 20 字节局部空间
	Read(temp,12); //从样本读取 12 字节到 temp
	if(temp[0]==0x6A626F43)
	{
		if(temp[8]<8) //关键
		{
			exit(-1);
		}
		else
		{
			sub_275C876D(temp[8],lpMen,*temp[8])
		}
	}
	else
	{
		exit(-1);
	}
}

sub_275C876D(temp[8],lpMen,*temp[8])
{
	char val[4];
	if( Read(val,4) <0 ); //继续从样本读取 4 字节到 temp
	{
		exit(-1);
	}
	else
	{
		if(val[0]==*temp[8])
		{
			if( buffer=HeapAlloc(hHeap,0,val[0]) ); //分配 temp[0]字节空间
			{
				Read(buffer,val[0]); //从样本读取 temp[0]字节到临时空间
				memcpy(temp[12],buffer,0x8280); //复制 0x8280 字节到 sub_275C89C7()剩下的 8 字节空间,导致覆盖返回地址
			}
			else
			{
				exit(-1);
			}
		}
		else
		{
			exit(-1);
		}
	}
}
漏洞利用 

此样本是通过用 jmp esp 地址覆盖函数返回地址进行利用的 

image21.png

搜一下样本,发现 shellcode

image22.png

之前看了通过利用异常处理进行的利用。就像试试这个漏洞能不能用此方法。在溢出即将发 生时候

275c87cb f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
我 dump 异常链

0:000> !exchain
0012f908: USER32!_except_handler3+0 (77d3edbf)
 CRT scope 0, func: USER32!UserCallWinProcCheckWow+155 (77d4f0cc)
0012f968: USER32!_except_handler3+0 (77d3edbf)
0012ffb0: WINWORD!wdGetApplicationObject+27bcf8 (30a6c5d4)
0012ffe0: kernel32!_except_handler3+0 (77e74809)
 CRT scope 0, filter: kernel32!BaseProcessStart+29 (77e737c8)
 func: kernel32!BaseProcessStart+3a (77e80128)
Invalid exception stack at ffffffff
此时 EDI=0x00127b7c,复制 33408 字节,最多覆盖到 0x12FDFC,而最近的异常处理在 0x12f908。 理论上可以利用。但是当我修改样本 shellcode 后面的数据时 shellocde 后面的数据在复制前 已经不是原始的的了。所以尽管可以覆盖到 SEH,但数据似乎是不可控的。 

总结 

这是我分析的第一个 CVE,以前都是看书,写写 demo 调试,基本就是 strcpy 那种简单的溢 出。看别人分析的 CVE 又看不懂。自己分析完一个,感觉收获蛮大,了解到其实 CVE 漏洞 也没有那么神秘高深,还有漏洞居然还能这样产生,而不是 strcpy,memcpy 造成溢出,居然 是一个小小的判断失误就导致了漏洞,真是千里之堤溃于蚁穴。这个漏洞是由于 MSCOMCTL.OCX 在解析数据的时候将需要处理的数据拷贝到栈上,而对需要拷贝的数大小未 做正确检查,造成标准的栈溢出漏洞。