第一次分析 Word 的漏洞, 错误地方还请各位师傅指正!
测试环境
Windows7 SP1 x86
Microsoft Office 2007
wwlid.dll 12.0.4518.1014
还原 POC
样本是一个RTF 文件, 360 发布的信息说到该样本在 Shellcode 执行后会释放 DLL 文件, 先打开 Procmon, 然后打开 RTF 文件, 打开后 word 奔溃, 在 Procmon 中也没有发现释放 DLL, 说明 Shellcode 可能没有成功执行. 再次启动 Word, 附加 Windbg, 打开样本文件, Word 奔溃在如下地方:
6a2b3076 8bb6140b0000 mov esi,dword ptr [esi+0B14h]
6a2b307c 8b06 mov eax,dword ptr [esi]
6a2b307e 8b10 mov edx,dword ptr [eax]
6a2b3080 4a dec edx
6a2b3081 4a dec edx
6a2b3082 8bce mov ecx,esi
6a2b3084 e8176dc3ff call wwlib!DllGetClassObject+0x5156 (69ee9da0)
6a2b3089 8b4044 mov eax,dword ptr [eax+44h]
6a2b308c 8b4044 mov eax,dword ptr [eax+44h]
6a2b308f 8b4f44 mov ecx,dword ptr [edi+44h]
6a2b3092 894144 mov dword ptr [ecx+44h],eax
6a2b3095 8b4744 mov eax,dword ptr [edi+44h]
6a2b3098 8b4044 mov eax,dword ptr [eax+44h]
6a2b309b 8b08 mov ecx,dword ptr [eax] ds:0023:088888ec=????????
6a2b309d 50 push eax
6a2b309e ff5104 call dword ptr [ecx+4]
分析奔溃点上面几条指令可知, eax 值依赖于上面那个 call 的返回值, 重新开始, 对该 call 下断.
断下后跟进该函数,
分析可知, 该函数的返回值等于调用该函数时的 edx * [eax + 8] + [eax + 0ch] + eax. 函数返回后继续跟,
跟到 call 后第二条 mov 时, 可以看到从 eax + 44h 取出的值是 0x088888ec, 这时 db 看下 eax 的内存
0:000> db eax
04591e00 5f 04 00 00 00 00 00 00-00 00 00 00 00 00 00 00 _...............
04591e10 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
04591e20 00 00 00 00 00 00 00 00-4c 00 69 00 6e 00 63 00 ........L.i.n.c.
04591e30 65 00 72 00 43 00 68 00-61 00 72 00 43 00 68 00 e.r.C.h.a.r.C.h.
04591e40 61 00 72 00 ec 88 88 08-66 00 6f 00 6e 00 74 00 a.r.....f.o.n.t.
04591e50 1a ff 62 00 61 00 74 00-61 00 6e 00 67 00 00 00 ..b.a.t.a.n.g...
04591e60 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
04591e70 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
可以看到, 0x088888ec 似乎是存在于一段字符串中, 而且经过多次调试可以发现, 这个 0x088888ec 是固定的, 并不是随便的一个值, 而且是一直和字符串一起出现的. 这里可以猜想, 该值是在样本中故意指定的.
从 RTF 文件搜 0x088888ec 或 "Lincer" 发现搜不到. 通过看一些 RTF 样本分析文章可知, RTF 样本一般会包含一些嵌入的对象, 这里用 oletools 中的 rtfobj 看下样本
---+----------+-------------------------------+-------------------------------
id |index |OLE Object |OLE Package
---+----------+-------------------------------+-------------------------------
0 |0003972Dh |format_id: 1 (Linked) |Not an OLE Package
| |class name: '' |
| |data size: N/A |
---+----------+-------------------------------+-------------------------------
1 |00039807h |format_id: 2 (Embedded) |Not an OLE Package
| |class name: 'Word.Document.12' |
| |data size: 53248 |
---+----------+-------------------------------+-------------------------------
2 |000538E9h |format_id: 2 (Embedded) |Not an OLE Package
| |class name: 'Word.Document.12' |
| |data size: 14336 |
---+----------+-------------------------------+-------------------------------
可以看到, 里面存在 3 个嵌入的对象, 第一个对象通过在 RTF 文件搜 "objdata" 发现一个 CLSID
D5DE8D20-5BB8-11D1-A1E3-00A0C90F2731, 从注册表 HKEY_CLASSES_ROOT\CLSID
下可以知道该 CLSID 代表 msvbvm60.dll, 通过参考中的第 5 篇文章可以知道, 这个是加载 msvbvm60.dll 来绕过
ASLR 的.
接着看两个 Word 对象, 用 rtfobj -s all 把它们提取出来, 用 7z 解压, 再解压里面的 Package.
首先来看第一个DOC, 解压后, 在 word 目录中发现 activeX 目录, 里面有 40 个 activeX*.xml 和一个 activeX1.bin, 看过分析文章可以知道, 这是用来堆喷的. activeX1.bin 中就是喷射的内容, 这里会喷射 40 个 activeX, 这 40 个 activeX 在加载时一般是连续分配的. 正常情况下, 每插入一个 activeX 对象, 就会生成一个 activeX*.xml 和 activeX*.bin, activeX*.xml 对应哪个 bin 文件由 _rels 目录下的 activeX*.xml.rels 指定. 这里把所有 activeX*.xml.rels 都修改为指向 activeX1.bin, 所以只有一个 bin 文件, 不过效果是相同的. 通过查看 word 目录下的 document.xml 文件, 可以知道该样本应该是插入了 40 个 Image 对象.
接着看第二个 DOC, 查看 word 目录中的 document.xml.
在这里我们发现奔溃时的字符串 "Lincer CharChar", 但是发现 "CharChar" 和 "font" 之间的字节(也就是上面的
[...], 因为原字节不可显示所以代替)并不是 0x088888ec. 注意到这里的字符串是 ASCII 形式的, 而奔溃时是 Unicode的, 怀疑是编码转换的问题, 这里可以把 "CharChar" 和 "font" 之间的字节复制出来, 使用 MultiByteToWideChar 函数转换一下试试
#include
#include
int main()
{
WCHAR wideChars[0x100];
SecureZeroMemory(wideChars, sizeof(wideChars));
std::map codePages = {
{CP_ACP, "CP_ACP"},
{CP_OEMCP, "CP_OEMCP"},
{CP_THREAD_ACP, "CP_THREAD_ACP"},
{CP_SYMBOL, "CP_SYMBOL"},
{CP_UTF7, "CP_UTF7"},
{CP_UTF8, "CP_UTF8"}
};
for(auto codePage = codePages.cbegin(); codePage != codePages.cend(); ++codePage)
{
MultiByteToWideChar(codePage->first, 0, "\xe8\xa3\xac\xe0\xa2\x88", -1, wideChars, 0x100);
printf("%s: %08x\n", codePage->second, *(PULONG32)wideChars);
}
return 0;
}
执行后可以看到, 这 6 字节用 UTF8 编码后就是 0x088888ec. 这里也可以在奔溃函数下断,
在奔溃发生的前一次断下(如何判断见下文), 此时 s -u 1000 L?70000000 "Lincer" 搜一下是否已经生成 Unicode字符串, 如果有了, 就再在前一次断下, 如果没找到, 此时用命令 s -a 1000 L?70000000 搜 ASCII 字符串, 对找到的所有地址下 ba r1 访问断点, 要编码转换肯定是要访问原数据的, 所以这样可以找到处理的地方.
这里猜测这个DOC 就是触发漏洞的文件, 我们单独打开该文件, 发现并不会奔溃. 这里可以把 document.xml 中 w:body 里面的内容复制出来, 然后新建一个 DOCX 文件, 用 7z 打开, 用刚复制出来的内容替换 word\document.xml 中 w:body 的内容. 再次打开 DOCX, 可以看到奔溃在同一个地方.
现在我们可以大致了解样本的流程,
它在 RTF 中嵌入了 3 个 OLE 对象, 第一个用来加载 msvbvn60.dll 来绕过 ASLR, 第二个用来堆喷,
第三个用来触发漏洞. 在奔溃点我们可知道, 下面有个虚函数调用, 从 eax 取出虚表而后调用虚函数. 这里 eax 是可控的, 通过堆喷布局
eax 所指地址, 最后执行 shellcode. 我们用 x32dbg 加载 Word, 在奔溃点下个断点, 打开样本后断下,
在内存布局标签可以看到连续的 40 个 2MB 内存空间, 样本的堆喷是成功的, 但是堆是从比 0x088888ec 高的地址开始分配的, 0x088888ec 并没有被布局, 所以导致奔溃.
漏洞分析
接着继续分析漏洞是怎么造成的。
前面我们分析可以知道, 奔溃点上面 call 的返回值主要来自于 esi, 而 esi 通过 IDA
的高亮我们知道来自于该奔溃函数的第一参数. 我们在奔溃函数下断点运行, 发现该函数会被断下多次, 这里如果没有明显的条件用于判断在哪次中断时发生奔溃, 可以用下面的条件断点统计下该奔溃函数的调用次数, 看看是在第几次发生的奔溃,
然后再下相应的条件断点
// 使用 $t 伪寄存器之前先清 0
r $t0 = 0
// 统计次数
bp wwlib+000861d4 "r $t0=$t0+1; .printf\"Count: %d\\n\", $t0; g;"
r $t0 = 0
// 输出第 1, 2, 106 次的 esp, 并在第 106 次时中断
bp wwlib+000861d4 "r $t0=$t0+1; .printf\"Count: %d\\n\", $t0; .if ($t0 == 1 or $t0 == 2) {dd esp; g;} .elsif ($t0 == 0n106) {dd esp;} .else {g;}"
这里如果是调试的样本文件, 会在第 106 此时发生奔溃, 如果是前面自己写的 POC 的话, 会在第 5 此时奔溃.
接下来我们跟踪一次不奔溃时执行该函数的流程, 在 IDA 中用一个颜色把路径标出来, 然后再跟一次奔溃时的流程, 用另一个颜色把不同的路径标出来. 最后我们知道主要的不同在下面这个地方(IDA 加载的基址是 0x31240000)
.text:312C63F3 lea eax, [esi+224h]
.text:312C63F9 mov ecx, [eax]
.text:312C63FB xor edx, edx
.text:312C63FD shr ecx, 1
.text:312C63FF inc edx
.text:312C6400 and ecx, edx
.text:312C6402 jnz short loc_312C640E
.text:312C6404 cmp [ebp+var_14], 0
.text:312C6408 jnz loc_31FEC471
.text:312C640E
.text:312C640E loc_312C640E: ; CODE XREF: sub_312C61D4+22Ej
.text:312C640E test ecx, ecx
.text:312C6410 jnz loc_3161306A
当不奔溃时, ecx 为 0, 这里没跳, 而奔溃时, ecx 为 1, 进而走向奔溃点. 这里 ecx 来自 [esi + 224h],动态调试可以知道, 不奔溃时这里的值的最低字节为 0, 而奔溃时这里的值的最低字节为 2,右移一位后和 1 位与为 1. 通过 IDA 我们知道 esi 等于第一参数, 所以该值也可以做为我们条件断点的条件。
通过跟踪函数流程可以知道,奔溃函数的第二个参数 +18h 处是一个 Unicode 字符串指针, +1ch 处是字符个数. 字符串的内容是文档的 XML 中的一些标签名, 比如 "shapedefaults" 和 "idmap". 下断点时, 通过输出该字符串可以知道现在处理的是那个标签.
这里使用条件断点, 输出一下该函数的第一个参数 +224h 处的值和这个 Unicode 字符串. 以下是打开 POC 时的输出
bp wwlib+000861d4 "dd poi(esp + 4) + 224 L1; du poi(poi(esp + 8) + 18) Lpoi(poi(esp + 8) + 1c); g;"
03dbc224 00000000
05220c0e "shapedefaults"
03dbc224 00000000
05220c0e "shapelayout"
03dbc224 00000000
05220c28 "idmap"
03dbc224 00018000
051cba8a "OLEObject"
03dbc224 00018002
051cbaac "idmap"
从输出可以看到, 在处理 OLEObject 标签时, 值是 0x18000, 处理 idmap 时, 值就变成了 0x18002,
而且在调试时可以发现, 奔溃函数的第一参数在每次调用时都是相同的, 所以我们可以在处理 OLEObject 时断下, 对第一个参数 +224h
处下 ba w4 断点. 中断后分析得知, 正是在处理 OLEObject 时对该位置的值位或了一个 2, 又因每次处理时第一个参数是不变的,
从而再处理接下来的标签时, 该处的值是 0x18002. 不过观察输出, 里面并没有出现 POC 中的 w:font 标签.
这里我们可以回溯到奔溃函数的上级函数下断, 看看能不能断到 font. 通过 IDA 的 F5 可以知道, 奔溃函数的第一个参数等于
poi(参数 1 + 0xb10), 第二个参数等于第二个参数. 所以在这个函数头下断时, 依然可以输出第二个参数中的字符串. 测试后可以知道,
在上级函数是可以断到 font 的, 只不过在处理 font 时没有进入奔溃函数, 而是由别的函数处理.
接下来继续看奔溃点处,
这里取出了 0x0888880e, 我们看看该数据是由谁写入内存的. 前面我们分析过, 取出 0x088888ec 的地址是根据一个函数的返回值得到的. 该函数的返回值等于调用该函数时的 edx * [eax + 8] + [eax + 0ch] + eax,
这里的 edx 为 poi(poi(poi(参数 1 + b14))). 调试可以知道, 在奔溃时, edx 等于 4, +8 和 +0ch
处的值是 4ch 和 10h(多次调试可以知道, 这两个值不管处理哪个标签时都是固定的). eax 等于 poi(poi(参数 1 + 0xb14)). 这里在奔溃函数下断, 处理 OLEObject 时停下, 根据上面的表达式手工计算出奔溃时计算的地址, 然后 dd 查看计算的地址 + 44h, 发现此时这里全是 0, 所以这里对计算的地址 + 44h 下 ba w4 断点.
运行后, 首先断在上级函数, 输出的字符串是 "font", 说明开始处理 font 标签了. 继续运行, 接着内存写入断点触发
MSVCR80!memcpy+0x5a:
6da7500a f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0:000> db poi(esi - 4)
031bc200 61 04 00 00 00 00 00 00-00 00 00 00 00 00 00 00 a...............
031bc210 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
031bc220 00 00 00 00 00 00 00 00-4c 00 69 00 6e 00 63 00 ........L.i.n.c.
031bc230 65 00 72 00 43 00 68 00-61 00 72 00 43 00 68 00 e.r.C.h.a.r.C.h.
031bc240 61 00 72 00 ec 88 88 08-66 00 6f 00 6e 00 74 00 a.r.....f.o.n.t.
031bc250 31 00 32 00 62 00 61 00-74 00 61 00 6e 00 67 00 1.2.b.a.t.a.n.g.
031bc260 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
031bc270 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
这里 db 看下写入值的数据(减 4 是因为访问断点是在访问后才断下), 正是奔溃时的数据. 继续运行, 又断在了上级函数, 输出 "idmap", 再运行就会断在奔溃函数, 输出同样是 "idmap". 再运行就奔溃了. 到这里, 刚开始我以为是因为 w:font 标签的问题, 在处理该标签时破坏了对象的数据, 在处理接下来的标签时访问了被破坏的内存导致的问题. 但是后来再看触发漏洞的 XML 时, 发现 w:font 标签没有对应的关闭标签, 就想着加上关闭标签看看. 加上后再打开, 发现不奔溃了, 这里又试了不加关闭标签, 修改后面的 idmap 标签为其它标签, 发现以 o: 开头的一些标签同样导致奔溃, 比如 o:object 和 o:FieldCodes.
这说明问题的根本并不是因为处理 w:font 标签破坏了内存, 也后面和 idmap 标签没关系, 而是和 w:font 没有添加关闭标签有关.
接下来分别调试有关闭标签的和没关闭标签的. 首先在奔溃函数下断, 到处理 OLEObject 时, 对上级函数下断, 对上面计算的地址 +44h 下写入断点. 执行后可以发现, 在有关闭标签时, 当奔溃函数处理 idmap 时, 在下面位置
.text:312C63DD push esi
.text:312C63DE call sub_3127F3FB
.text:312C63E3 mov edi, eax
.text:312C63E5 mov [edi+44h], ebx
触发了写入断点, 这里重新写入了正常的值, 这里的 call 同样是返回一个计算的地址, 计算方法和奔溃点的 call 是一样的,
都是表达式 edx * [eax + 8] + [eax + 0ch] + eax 的结果(寄存器不同, 逻辑一样). 只是这里 edx 等于 poi(poi(poi(第一个参数 + b14))) - 1, 而奔溃点是 poi(poi(poi(第一个参数 + b14))) - 2.
在有关闭标签时, 当处理到 idmap 时, poi(poi(poi(第一个参数 + b14))) 的值是 5, 而没关闭标签时, poi(poi(poi(第一个参数 + b14))) 的值是 6.
接下来先在奔溃函数下断,
当断下后再在上级函数下断, 可以少断很多次. 当上级函数处理 OLEObject 时, 查看 poi(poi(poi(esp + 4) +
b14)) 处的值并对该位置下内存写入断点. 经过多次调试可以发现, 当上级函数准备处理 OLEObject 时, 查看该值是 3,
处理过程中将该值修改为 4, 处理 font 的过程中将该值修改为 5, 这里看下 document.xml 就可发现, 该值应该是标签的的嵌套层次(POC 的 document.xml 中 OLEObject 就是嵌套的第 4 个标签, 从 ducument 标签开始). 当有关闭标签时, 处理 font 标签时先增到 5, 然后再减 1, 处理 idmap 时再修改为 5, 最后进入奔溃函数.
当没有关闭标签时, 处理 font 增到 5, 而后并没有减 1 操作, 到处理 idmap 时为 6.
当执行到
.text:312C63DD push esi
.text:312C63DE call sub_3127F3FB
.text:312C63E3 mov edi, eax
.text:312C63E5 mov [edi+44h], ebx
时, 有关闭标签的情况下, 处理 OLEObject 时为 4, 这里把正常对象设置到 4 - 1 后计算的位置, 到处理 idmap 时为 5, 在奔溃点处计算时是 5 - 2, 获取的正是 OLEObject 设置的值. 而没关闭标签的情况下, 处理 font 时为 5, 处理 font 用了其它函数, 不过也是修改 5 - 1 后计算的位置. 到处理 idmap 时为 6, 在奔溃点用 6 - 2 计算, 这里正是 font 设置的值. 由于 font 和 OLEObject 设置的对象内存布局并不同, 导致在奔溃点获取一个对象时, 获取了 font 的 name 属性中的数据.
漏洞利用
漏洞的利用流程在前面还原 POC 时基本已经摸清了, 就是通过堆喷布局内存, 然后在触发漏洞的文件中指定一个地址, 最后根据该指定地址执行 Shellcode. 这里就不传 EXP 了.
本文由看雪论坛 污师 原创 转载请注明来自看雪社区