可以说,CVE-2012-0158作为经典的Office漏洞,凭借其稳定性和通用性,频繁出现在各类安全分析报告中。今天,我们将深入剖析这一漏洞,从原理到手动构造一个可触发的POC样本,全程逐步讲解。
漏洞原理
关于此漏洞的原理,网络上已有不少分析文章。通常的做法是获取一个真实样本,再定位二进制代码。我们这里不再重复这个流程,而是直接揭示漏洞的核心成因,但后续仍会简要演示调试过程。
该漏洞存在于Office的一个组件——MSCOMCTL.OCX中。多个版本的Office,该模块均存在相同问题。我们的测试环境以Windows XP + Office 2007为例。MSCOMCTL.OCX是Office解析ActiveX控件时调用的动态库。一旦文档中嵌入了按钮、列表、树形控件等ActiveX元素,Office打开文档时就会自动加载该库来解析和显示控件。
从漏洞类型来看,这属于经典的栈缓冲区溢出——准确地说,是栈内存拷贝溢出。当Office解析一个精心构造的控件(以ListView列表控件为例)时,就会发生栈内存的越界拷贝。下面的栈回溯图可以清晰展示整个触发过程:

可以看到,Excel在解析ListView控件时,读取并加载了控件的数据流。加载过程中会调用一个内部函数ReadBytesFromStreamPadded,该函数功能类似于memcpy,根据参数从指定内存拷贝指定大小的数据到目标缓冲区。不过,如果顺着调用链向上追溯就会发现,漏洞的真正元凶并不在这里,而是出在CObj::load函数中。我们来看看IDA反编译出的伪代码:
int __stdcall CObj__Load( int a1, void *lpMem)
{
int result; // eax@1
void *v3; // ebx@1
int v4; // esi@4
int v5; // [sp+Ch] [bp-14h]@1
SIZE_T dwBytes; // [sp+14h] [bp-Ch]@3
int v7; // [sp+18h] [bp-8h]@4
int v8; // [sp+1Ch] [bp-4h]@8
v3 = lpMem;
result = ReadBytesFromStreamPadded(&v5, lpMem, 0xCu ); //第一次正常拷贝,读取数据头
if ( result >= 0 )
{
if ( v5 == 'jboC' && dwBytes >= 8 ) //漏洞触发条件
{
v4 = ReadBytesFromStreamPadded(& v7, v3, dwBytes); //第二次拷贝,此处调用必然越界
if ( v4 >= 0 )
{
if ( ! v7 )
goto LABEL_8;
lpMem = 0;
v4 = ReadBstrFromStreamPadded(( UINT)&lpMem, (int )v3);
if ( v4 >= 0 )
{
CObj__SetKey((BSTR) lpMem);
SysFreeString((BSTR) lpMem);
LABEL_8:
if ( v8 )
v4 = ReadVariantFromStream(( struct tagVARIANT *)(a1 + 20) , (struct IStream *) v3);
return v4;
}
}
return v4;
}
result = 2147549183;
}
return result;
}
简而言之,CObj::Load用于加载CObj对象。它先从数据流中读取0x0c个字节到临时变量v5,然后检查v5前4个字节是否为“Cobj”以确认对象类型,同时检测dwBytes是否大于等于8。关键点在于:dwBytes这个值是在第一次读取那0x0c个字节时一并读入的。从IDA的变量注释可以看到,dwBytes在内存中正好位于v5之后,其值完全可以通过修改原始数据流来控制。
接下来,程序会再次从数据流中读取dwBytes个字节到临时变量v7。但v7的大小只有8个字节(从bp-0x08到bp-0x00)。而dwBytes本身大于8,这就意味着这次拷贝必然会覆盖到ebp,发生越界,从而形成栈溢出。
基于此分析可以推测,正常情况下,控件数据中读出的dwBytes不应大于8。如果大于8,栈拷贝就会异常,该漏洞早就被发现了。更值得注意的是,我在IDA中查看了这个函数的交叉引用,发现其作用很有限——只在加载特定几个控件时被调用。因此,我怀疑这未必是微软的一个“严重失误”,倒更像是故意留下的后门。

构造触发漏洞的POC
原理已经厘清:Office解析ListView控件时,会调用漏洞函数CObj::Load,该函数在加载CObj对象时,会用我们可控的dwBytes值去拷贝数据到一个只有8字节的临时变量里——关键是,它连基本的校验都没有。为了验证这一分析,我们按图索骥,动手构造一个能触发漏洞的Excel文档。
首先,Excel文档中需要有一个ListView控件,这可以通过开发者工具添加。添加完成后,文档中就有了一个空的ListView对象。

接下来,还需要往该控件中添加ListItems和ListItem子对象,这样才能让Excel程序调用CObj::Load。不过这里有一个难点:ListItem对象无法直接通过Excel界面操作添加——Excel只能通过ListView控件的属性添加列表标题,没有直接添加列表内容的入口。解决办法是使用VBA代码生成。但这样做又会引出另一个问题:如果文档包含宏代码,Excel默认会禁用宏,导致无法解析ListItem对象。
一个简单粗暴的应对方法是:先编写并运行代码,生成初始化好的ListView控件,然后将所有生成代码删除再保存。宏代码会被阻止执行,但已经生成的控件对象不会被阻止解析。

完成这一步后,用十六进制编辑器打开保存好的文档,定位到CObj对象的数据区,将偏移量为8的dwBytes值改成一个大于8的数,理论上就能触发漏洞。不过只修改这一个值,你会发现它并未触发。原因是拷贝函数ReadBytesFromStreamPadded还会再做一次校验——它会从待拷贝的数据头部再读取一个dwBytes值,检查两个值是否相等。因此,我们只需要将对象数据中的那个同名字段也改成同样的大小,就能顺利绕过校验,触发漏洞。

触发漏洞后,由于我们只是用随机数据覆盖了ebp和函数的返回地址,Excel最终会弹出一个经典的“程序错误”提示框——这正是我们想要的效果。

漏洞利用
现在,我们手上有了一个可以稳定触发的栈溢出漏洞。接下来如何利用,就看各位的发挥了。这里我们还是老规矩——弹个计算器,抛砖引玉。
通过前面构造的POC,我们可以通过修改那两个dwBytes的值以及后面的数据,来控制运行栈的内存布局。为了更精准地编排数据,最好边调试边修改,最后将内存中排好的数据拷贝回文档的对应位置。调试阶段,第一目标自然是控制程序流程——拿到EIP的控制权。通常的做法是覆盖函数返回值或者SEH链指针。由于MSCOMCTRL.DLL没有开启GS保护,我们直接用最简单的覆盖函数返回值就能控制EIP。不过,为了让程序顺利走到返回地址,还需要调整数据,满足一些返回条件,引导程序流程绕过那些复杂的指令操作集,避免因栈被破坏而触发异常。

一旦程序顺利抵达返回地址,剩下的就随意发挥了——可以是构造ROP链绕过DEP保护,也可以直接跳转到栈空间执行代码。对于懂漏洞利用的人来说,这些都是基本功。这里有一个额外要求:栈内存上的数据需要有足够的空间来容纳ROP链或者Shellcode。因此,需要将ListView控件的数据规模做大——方法很简单,在添加ListItem时把字符串写长一点即可。

所有必要条件都凑齐后,代码就可以放进栈中执行了。这里使用一个通用的跳转地址,直接跳到栈内存代码上执行。由于XP + Office 2007默认不开启DEP保护,计算器顺利弹出。

至于如何编写ROP链来突破Office 2010及以上版本默认开启的DEP保护,以及更多高级利用技巧,我们会在后续的漏洞分析中继续展开。
总结
通过这一番分析,我们对该漏洞的原理和危害性有了透彻的了解。MSCOMCTL.OCX本身是一个基础动态库,影响面非常广——除了Office整套产品外,SQL Server以及其他第三方应用,只要依赖这个库,都可能被利用。而利用的思路与本文一致,万变不离其宗:构造的“畸形”数据必须能通过漏洞函数的检验流程,最终绕过程序本身的限制,夺取控制权。
