第一次分析 Word 的漏洞, 错误地方还请各位师傅指正!
样本是一个 RTF 文件, 360 发布的信息说到该样本在 Shellcode 执行后会释放 DLL 文件, 先打开 Procmon, 然后打开 RTF 文件, 打开后 word 奔溃, 在 Procmon 中也没有发现释放 DLL, 说明 Shellcode 可能没有成功执行. 再次启动 Word, 附加 Windbg, 打开样本文件, Word 奔溃在如下地方
分析奔溃点上面几条指令可知, eax 值依赖于上面那个 call 的返回值, 重新开始, 对该 call 下断.
断下后跟进该函数, 分析可知, 该函数的返回值等于调用该函数时的 edx * [eax + 8] + [eax + 0ch] + eax. 函数返回后继续跟, 跟到 call 后第二条 mov 时, 可以看到从 eax + 44h 取出的值是 0x088888ec, 这时 db 看下 eax 的内存
可以看到, 0x088888ec 似乎是存在于一段字符串中, 而且经过多次调试可以发现, 这个 0x088888ec 是固定的, 并不是随便的一个值, 而且是一直和字符串一起出现的. 这里可以猜想, 该值是在样本中故意指定的.
从 RTF 文件搜 0x088888ec 或 "Lincer" 发现搜不到. 通过看一些 RTF 样本分析文章可知, RTF 样本一般会包含一些嵌入的对象, 这里用 oletools 中的 rtfobj 看下样本
可以看到, 里面存在 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 函数转换一下试试
执行后可以看到, 这 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 的高亮我们知道来自于该奔溃函数的第一参数. 我们在奔溃函数下断点运行, 发现该函数会被断下多次, 这里如果没有明显的条件用于判断在哪次中断时发生奔溃, 可以用下面的条件断点统计下该奔溃函数的调用次数, 看看是在第几次发生的奔溃, 然后再下相应的条件断点
这里如果是调试的样本文件, 会在第 106 此时发生奔溃, 如果是前面自己写的 POC 的话, 会在第 5 此时奔溃.
接下来我们跟踪一次不奔溃时执行该函数的流程, 在 IDA 中用一个颜色把路径标出来, 然后再跟一次奔溃时的流程, 用另一个颜色把不同的路径标出来. 最后我们知道主要的不同在下面这个地方(IDA 加载的基址是 0x31240000)
当不奔溃时, ecx 为 0, 这里没跳, 而奔溃时, ecx 为 1, 进而走向奔溃点. 这里 ecx 来自 [esi + 224h], 动态调试可以知道, 不奔溃时这里的值的最低字节为 0, 而奔溃时这里的值的最低字节为 2, 右移一位后和 1 位与为 1. 通过 IDA 我们知道 esi 等于第一参数, 所以该值也可以做为我们条件断点的条件.
通过跟踪函数流程可以知道, 奔溃函数的第二个参数 +18h 处是一个 Unicode 字符串指针, +1ch 处是字符个数. 字符串的内容是文档的 XML 中的一些标签名, 比如 "shapedefaults" 和 "idmap". 下断点时, 通过输出该字符串可以知道现在处理的是那个标签. 这里使用条件断点, 输出一下该函数的第一个参数 +224h 处的值和这个 Unicode 字符串. 以下是打开 POC 时的输出
从输出可以看到, 在处理 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 标签了. 继续运行, 接着内存写入断点触发
这里 db 看下写入值的数据(减 4 是因为访问断点是在访问后才断下), 正是奔溃时的数据. 继续运行, 又断在了上级函数, 输出 "idmap", 再运行就会断在奔溃函数, 输出同样是 "idmap". 再运行就奔溃了. 到这里, 刚开始我以为是因为 w:font 标签的问题, 在处理该标签时破坏了对象的数据, 在处理接下来的标签时访问了被破坏的内存导致的问题. 但是后来再看触发漏洞的 XML 时, 发现 w:font 标签没有对应的关闭标签, 就想着加上关闭标签看看. 加上后再打开, 发现不奔溃了, 这里又试了不加关闭标签, 修改后面的 idmap 标签为其它标签, 发现以 o: 开头的一些标签同样导致奔溃, 比如 o:object 和 o:FieldCodes. 这说明问题的根本并不是因为处理 w:font 标签破坏了内存, 也后面和 idmap 标签没关系, 而是和 w:font 没有添加关闭标签有关.
接下来分别调试有关闭标签的和没关闭标签的. 首先在奔溃函数下断, 到处理 OLEObject 时, 对上级函数下断, 对上面计算的地址 +44h 下写入断点. 执行后可以发现, 在有关闭标签时, 当奔溃函数处理 idmap 时, 在下面位置
触发了写入断点, 这里重新写入了正常的值, 这里的 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.
当执行到
时, 有关闭标签的情况下, 处理 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 了.
APT 攻击利器-Word 漏洞 CVE-2016-7193 原理揭秘
CVE-2015-1641 Word 利用样本分析
结合一个野外样本构造一个cve-2016-7193弹计算器的利用
Spraying the heap in seconds using ActiveX controls in Microsoft Office
Bypassing Windows ASLR in Microsoft Office using ActiveX controls
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课