查看原文
其他

CVE-2017-11826 样本分析

2017-10-27 污师 看雪学院


第一次分析 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.

<w:body >

    <w:shapeDefaults >

        <o:OLEObject >

            <w:font w:name="LincerCharChar[...]font:batang"><o:idmap/>

        </o:OLEObject>

    </w:shapeDefaults>

</w:body>


在这里我们发现奔溃时的字符串 "Lincer CharChar",但是发现 "CharChar" 和 "font" 之间的字节(也就是上面的 [...],因为原字节不可显示所以代替)并不是 0x088888ec。注意到这里的字符串是 ASCII 形式的。而奔溃时是 Unicode 的,怀疑是编码转换的问题,这里可以把 "CharChar" 和 "font" 之间的字节复制出来,使用 MultiByteToWideChar 函数转换一下试试

#include <windows.h>

#include <map>

 

int main()

{

    WCHAR wideChars[0x100];

    SecureZeroMemory(wideChars, sizeof(wideChars));

 

    std::map<UINT, PCSTR> 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] 49 31055 49 15287 0 0 4148 0 0:00:07 0:00:03 0:00:04 4147+ 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 了。





本文由看雪论坛 污师 原创

转载请注明来自看雪社区

热门阅读


点击阅读原文/read,

更多干货等着你~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存