一、前言
这个下溢是当word处理特殊的二进制文档文件时,在复制操作期间越界读取时触发,能够导致winword.exe保护模式下栈的缓冲区溢出问题。
一切听起来是戏剧性的,但是PoC触发只需要越界读取,然而本文将深入分析漏洞细节。
这个漏洞影响Microsoft Word 2007 Service Pack 3, Microsoft Office 2010 Service Pack 2 (32位版本), Microsoft Office 2010 Service Pack 2 (64位版本) 和 Microsoft Office Compatibility Pack Service Pack 3。更多的细节能从SRC-2016-0042获取。本文所有的分析是基于Microsoft Office 2010 专业版的winword.exe(v14.0.4734.1000)。
首先,来看下sample和PoC文件的不同之处。
注意到只有一个字节的不同。接下来看下哪个结构块包含了这个不同。
可以看到不同之处在于OneTableDocumentStream数据域中。Sample文件的值为0x68,而poc文件使用0xfa来触发下溢。
二、触发漏洞
首先,为了调试开启页堆和用户态栈跟踪:
- c:\Program Files\Debugging Tools for Windows (x86)>gflags.exe -i winword.exe +hpa +ust
- Current Registry Settings for winword.exe executable are: 02001000
- ust - Create user mode stack trace database
- hpa - Enable page heap
- c:\Program Files\Debugging Tools for Windows (x86)>
然后运行poc.doc文件导致以下保护模式外异常访问:
- (880.ac4): Access violation - code c0000005 (first chance)
- First chance exceptions are reported before any exception handling.
- This exception may be expected and handled.
- eax=00000000 ebx=00000000 ecx=00000033 edx=00000002 esi=22870ffd edi=002513c4
- eip=744fb40c esp=0024c694 ebp=0024c69c iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
- 0:000> kvn
- # ChildEBP RetAddr Args to Child
- 00 0024c69c 5e3f9b36 002513bf 22870ff8 000000d3 MSVCR90!memmove+0xfc
- WARNING: Stack unwind information not available. Following frames may be wrong.
- 01 0024c6b0 5e413843 22870ff8 002513bf 000000d3 wwlib!DllGetClassObject+0x455a
- 02 0024c744 5e413223 002513ac 002513a0 00004ab8 wwlib!GetAllocCounters+0xcadb
- 03 00251230 5e4131c6 002513ac 002513a0 00004ab8 wwlib!GetAllocCounters+0xc4bb
- 04 00251264 5e45f414 002513ac 002513a0 00004ab8 wwlib!GetAllocCounters+0xc45e
- 05 00251280 5e8da8a7 002513a0 22872fe4 00000000 wwlib!GetAllocCounters+0x586ac
- 06 002512b8 5e89fdcb 04760520 002513a0 ffffffff wwlib!DllGetLCID+0x2d4521
- 07 002567f4 5e66e957 1b132948 04760098 00000000 wwlib!DllGetLCID+0x299a45
- 08 002580e0 5e671d5b 04760098 00258928 00000001 wwlib!DllGetLCID+0x685d1
- 09 00258584 5e671489 04760098 00258928 1b132948 wwlib!DllGetLCID+0x6b9d5
- 0a 0025894c 5e675c10 04760098 00002490 00000000 wwlib!DllGetLCID+0x6b103
- 0b 00258998 5e4a6ad4 04760098 1b132948 0000056e wwlib!DllGetLCID+0x6f88a
- 0c 002589d4 64270be6 22562f10 0000056e 00000000 wwlib!GetAllocCounters+0x9fd6c
- 0d 002589f8 64270ebd 18bea880 18bea998 00258aa8 MSPTLS!FsTransformBbox+0x279b3
- 0e 00258a4c 64270f2c 22798de8 00258d40 00000000 MSPTLS!FsTransformBbox+0x27c8a
- 0f 00258aec 64271196 00258d40 00000000 00000000 MSPTLS!FsTransformBbox+0x27cf9
- 10 00258ca0 6425736a 22798de8 227f0ca0 00000000 MSPTLS!FsTransformBbox+0x27f63
- 11 00258db4 6428aa6f 22826fd0 00000000 00000000 MSPTLS!FsTransformBbox+0xe137
- 12 00258eac 6426fbb9 22798de8 227f0ca0 00000000 MSPTLS!FsTransformBbox+0x4183c
- 13 00259000 6425684e 22798de8 00000000 00000000 MSPTLS!FsTransformBbox+0x26986
三、调查访问的内存
第一步我们要检查在崩溃时访问的内存。
- address 22870ffd found in
- _DPH_HEAP_ROOT @ 61000
- in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
- 227a13a8: 22870fe0 19 - 22870000 2000
- 67be8e89 verifier!AVrfDebugPageHeapAllocate+0x00000229
- 77126206 ntdll!RtlDebugAllocateHeap+0x00000030
- 770ea127 ntdll!RtlpAllocateHeap+0x000000c4
- 770b5950 ntdll!RtlAllocateHeap+0x0000023a
- 5de2d804 mso!Ordinal149+0x000074b0
- 5e6a754d wwlib!DllGetLCID+0x000a11c7
- 5e7debc2 wwlib!DllGetLCID+0x001d883c
- 5e41f313 wwlib!GetAllocCounters+0x000185ab
- 5e41ec32 wwlib!GetAllocCounters+0x00017eca
- 5e41eb57 wwlib!GetAllocCounters+0x00017def
- 5e41e72a wwlib!GetAllocCounters+0x000179c2
- 5e423d89 wwlib!GetAllocCounters+0x0001d021
- 5e6acca5 wwlib!DllGetLCID+0x000a691f
- 5e422aa0 wwlib!GetAllocCounters+0x0001bd38
- 5e43ed59 wwlib!GetAllocCounters+0x00037ff1
- 5e43ec61 wwlib!GetAllocCounters+0x00037ef9
- 5e48f0c3 wwlib!GetAllocCounters+0x0008835b
- 5e48f050 wwlib!GetAllocCounters+0x000882e8
- 5e4a6aba wwlib!GetAllocCounters+0x0009fd52
- 64270be6 MSPTLS!FsTransformBbox+0x000279b3
- 64270ebd MSPTLS!FsTransformBbox+0x00027c8a
- 64270f2c MSPTLS!FsTransformBbox+0x00027cf9
- 64271196 MSPTLS!FsTransformBbox+0x00027f63
- 6425736a MSPTLS!FsTransformBbox+0x0000e137
- 6428aa6f MSPTLS!FsTransformBbox+0x0004183c
- 6426fbb9 MSPTLS!FsTransformBbox+0x00026986
- 6425684e MSPTLS!FsTransformBbox+0x0000d61b
- 6426ad48 MSPTLS!FsTransformBbox+0x00021b15
- 6428573e MSPTLS!FsTransformBbox+0x0003c50b
- 64285910 MSPTLS!FsTransformBbox+0x0003c6dd
- 64285c7b MSPTLS!FsTransformBbox+0x0003ca48
- 6426b17a MSPTLS!FsTransformBbox+0x00021f47
- 0:000> !address @edi
- ProcessParametrs 00069738 in range 00069000 0006a000
- Environment 02b233d8 in range 02b23000 02b24000
- 00160000 : 0023d000 - 00023000
- Type 00020000 MEM_PRIVATE
- Protect 00000004 PAGE_READWRITE
- State 00001000 MEM_COMMIT
- Usage RegionUsageStack
- Pid.Tid 880.ac4
- 0:000> dd @esi
- 22870ffd ???????? ???????? ???????? ????????
- 2287100d ???????? ???????? ???????? ????????
- 2287101d ???????? ???????? ???????? ????????
- 2287102d ???????? ???????? ???????? ????????
- 2287103d ???????? ???????? ???????? ????????
- 2287104d ???????? ???????? ???????? ????????
- 2287105d ???????? ???????? ???????? ????????
- 2287106d ???????? ???????? ???????? ????????
- 0:000> ?@ecx*4
- Evaluate expression: 204 = 000000cc
可以看到越界读取了一个0x19字节的堆内存,试着将另外的204个字节复制到edi中。
正如结果,栈变量在顶上6个栈桢传递,下面的是通过其他变量和偏移动态计算的。没有符号直接加大了跟踪的难度。
四、写内存
如果我们继续从esi读,那么可以假定继续写是安全的。我知道这是一个大的猜测,但是利用ole堆喷射或者得到使用eps的的堆,是有可能控制那个偏移的数据的。但是如何覆写?我们看下目标栈地址:
- 0:000> !py mona do -a 002513c4 -s 0xcc
- Hold on...
- [+] Command used:
- !py mona.py do -a 002513c4 -s 0xcc
- ----------------------------------------------------
- [+] Dumping object at 0x002513c4, 0xcc bytes
- [+] Preparing output file 'dumpobj.txt'
- - (Re)setting logfile dumpobj.txt
- [+] Generating module info table, hang on...
- - Processing modules
- - Done. Let's rock 'n roll.
- >> Object at 0x002513c4 (0xcc bytes):
- Offset Address Contents Info
- ------ ------- -------- -----
- +00 0x002513c4 | 0x00000000
- +04 0x002513c8 | 0x000bd62f
- +08 0x002513cc | 0x00002001
- +0c 0x002513d0 | 0x0000ff00
- +10 0x002513d4 | 0xd63b0000
- +14 0x002513d8 | 0x8001000c
- +18 0x002513dc | 0xff000000
- +1c 0x002513e0 | 0x0100ffff
- +20 0x002513e4 | 0x00000000
- +24 0x002513e8 | 0x00000000
- +28 0x002513ec | 0xffffffff
- +2c 0x002513f0 | 0x00000000
- +30 0x002513f4 | 0x00000000
- +34 0x002513f8 | 0x00000000
- +38 0x002513fc | 0x00000000
- +3c 0x00251400 | 0x00000000
- +40 0x00251404 | 0xff000000
- +44 0x00251408 | 0x00000000
- +48 0x0025140c | 0xff000000
- +4c 0x00251410 | 0x00000000
- +50 0x00251414 | 0xff000000
- +54 0x00251418 | 0x00000c48
- +58 0x0025141c | 0xffffffff
- +5c 0x00251420 | 0x00000000
- +60 0x00251424 | 0xff000000
- +64 0x00251428 | 0x00000000
- +68 0x0025142c | 0xff000000
- +6c 0x00251430 | 0x00000000
- +70 0x00251434 | 0x1b132948 ptr to 0x5e52ee80 : wwlib!GetAllocCounters+0x128118
- +74 0x00251438 | 0xff000000
- +78 0x0025143c | 0x00000000
- +7c 0x00251440 | 0x00000000
- +80 0x00251444 | 0x00000000
- +84 0x00251448 | 0x00000000
- +88 0x0025144c | 0x00000000
- +8c 0x00251450 | 0xff000000
- +90 0x00251454 | 0x00000000
- +94 0x00251458 | 0x00000000
- +98 0x0025145c | 0x00000000
- +9c 0x00251460 | 0x00000000
- +a0 0x00251464 | 0x00000000
- +a4 0x00251468 | 0x00000000
- +a8 0x0025146c | 0x00000000
- +ac 0x00251470 | 0x00000000
- +b0 0x00251474 | 0x00000000
- +b4 0x00251478 | 0x00000000
- +b8 0x0025147c | 0x00000000
- +bc 0x00251480 | 0x00000000
- +c0 0x00251484 | 0x00000000
- +c4 0x00251488 | 0x00000000
- +c8 0x0025148c | 0x00000000
使用mona插件,能够将拷贝剩余大小的栈地址转储,可以看见有个指针指向.text (wwlib!GetAllocCounters+0x128118)。如果没猜错,我们不应该覆写这个值。
因此,我们可能溢出了一个栈缓冲区(可能性不大)。如果我们想命中返回地址,需要知道目的地址+0x1e8处才能出现。好奇之下能够定位到这里:
- ...
- +cc 0x00251490 | 0xff700000
- +d0 0x00251494 | 0x00ffffff
- +d4 0x00251498 | 0x00000000
- +d8 0x0025149c | 0x00000000
- ...
- +1dc 0x002515a0 | 0x1b132be0
- +1e0 0x002515a4 | 0x0000005e
- +1e4 0x002515a8 | 0x002515c4 ptr to self+0x00000200
- +1e8 0x002515ac | 0x5e415bc1 wwlib!GetAllocCounters+0xee59
- [+] This mona.py action took 0:00:01.669000
- 0:000> ub 0x5e415bc1
- wwlib!GetAllocCounters+0xee41:
- 5e415ba9 5e pop esi
- 5e415baa 81fbffffff7f cmp ebx,7FFFFFFFh
- 5e415bb0 0f873e393c00 ja wwlib!DllGetLCID+0x1d316e (5e7d94f4)
- 5e415bb6 8b5508 mov edx,dword ptr [ebp+8]
- 5e415bb9 53 push ebx
- 5e415bba 50 push eax
- 5e415bbb 52 push edx
- 5e415bbc e8b9e9fdff call wwlib+0x457a (5e3f457a)
我们无法看见调用栈,因为栈向上伸展失败了:
- 0:000> ?0x002515ac-@esp
- Evaluate expression: 20248 = 00004f18
接下来的问题是,怎么模拟继续执行?
Bannedit编写了一个很好的插件counterfeit,可以在windbg中看到用VirtualAlloc分配的块并且用标记的数据填充它。我们能继续并替换esi的值,继续复制操作。
- 0:000> !py cf -a 2000 -f
- __ _____ .__ __
- ____ ____ __ __ _____/ |_ ____________/ ____\____ |__|/ |_
- _/ ___\/ _ \| | \/ \ __\/ __ \_ __ \ __\/ __ \| \ __\
- \ \__( <_> ) | / | \ | \ ___/| | \/| | \ ___/| || |
- \___ >____/|____/|___| /__| \___ >__| |__| \___ >__||__|
- \/ \/ \/ \/
- version 1.0 - bannedit
- Allocated memory @ 0x14130000 with RWX permissions.
- Filling memory...
- Finished filling memory.
- 0:000> dd 0x14130000
- 14130000 41414141 41414142 41414143 41414144
- 14130010 41414145 41414146 41414147 41414148
- 14130020 41414149 4141414a 4141414b 4141414c
- 14130030 4141414d 4141414e 4141414f 41414150
- 14130040 41414151 41414152 41414153 41414154
- 14130050 41414155 41414156 41414157 41414158
- 14130060 41414159 4141415a 4141415b 4141415c
- 14130070 4141415d 4141415e 4141415f 41414160
现在我们看到esi位于0x14130000:
- 0:000> g
- (880.ac4): Access violation - code c0000005 (!!! second chance !!!)
- eax=00000000 ebx=00000000 ecx=00000033 edx=00000002 esi=22870ffd edi=002513c4
- eip=744fb40c esp=0024c694 ebp=0024c69c iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
- 0:000> r @esi=0x14130000
- ...
- 0:000> t
- eax=00000000 ebx=00000000 ecx=00000017 edx=00000002 esi=14130070 edi=00251434
- eip=744fb40c esp=0024c694 ebp=0024c69c iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
- 0:000> dd @edi L1
- 00251434 1b132948
- 0:000> dds poi(@edi) L1
- 1b132948 5e52ee80 wwlib!GetAllocCounters+0x128118
- 0:000> u poi(poi(@edi))
- wwlib!GetAllocCounters+0x6e3b0:
- 5e475118 55 push ebp
- 5e475119 8bec mov ebp,esp
- 5e47511b 56 push esi
- 5e47511c 8bf1 mov esi,ecx
- 5e47511e e814000000 call wwlib!GetAllocCounters+0x6e3cf (5e475137)
- 5e475123 f6450801 test byte ptr [ebp+8],1
- 5e475127 7407 je wwlib!GetAllocCounters+0x6e3c8 (5e475130)
- 5e475129 56 push esi
- 0:000> t
- eax=00000000 ebx=00000000 ecx=00000016 edx=00000002 esi=14130074 edi=00251438
- eip=744fb40c esp=0024c694 ebp=0024c69c iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
- 0:000> dds poi(@edi-4) L1
- 4141415d ????????
我们能看到我们覆写数据的指针,指向了来自esi中值对应的函数。因为esi中的数据是标记过的,我们能知道用来覆写指针的偏移。
五、曝光
再次观察调用栈,注意到memmove()的调用。
- 0:000> kvn L2
- # ChildEBP RetAddr Args to Child
- 00 0024c69c 5e3f9b36 002513bf 22870ff8 000000d3 MSVCR90!memmove+0xfc
- WARNING: Stack unwind information not available. Following frames may be wrong.
- 01 0024c6b0 5e413843 22870ff8 002513bf 000000d3 wwlib!DllGetClassObject+0x455a
用Hex-Rays反编译器,可以看到这个函数只是memmove()的一个封装,并在wwlib库中调用。可以重命名sub_316d9b16函数为memmove_wrapper_1。
- int __stdcall memmove_wrapper_1(void *Src, void *Dst, size_t Size)
- {
- int result; // eax@2
- if ( Size > 0x7FFFFFFF )
- result = MSO_1511(1647603307, 0);
- else
- result = (int)memmove(Dst, Src, Size);
- return result;
- }
如果大小大于MAX_INT,一个整形溢出异常被触发。另外也没有合理的校验小雨目的缓冲区的情况。
为了利用,我们得知道memmove()如何访问和被调用。
所以设置一个断点bp wwlib!DllGetClassObject+0x4554 ".printf \"calling memmove(%x, %x, %x);\\n\", poi(@esp), poi(@esp+4), poi(@esp+8); gc"并重新运行PoC。
- calling memmove(271164, 26fb3c, e);
- calling memmove(271172, 26fb4a, f);
- calling memmove(271148, 2266efe0, 3);
- calling memmove(27114b, 2266efe3, 3);
- calling memmove(27114e, 2266efe6, 3);
- calling memmove(271151, 2266efe9, 3);
- calling memmove(271154, 2266efec, 3);
- calling memmove(271157, 2266efef, 4);
- calling memmove(27115b, 2266eff3, 5);
- calling memmove(27122e, 27115b, 5);
- calling memmove(27115b, 2266eff8, d3);
- (5f0.59c): Access violation - code c0000005 (first chance)
- First chance exceptions are reported before any exception handling.
- This exception may be expected and handled.
- eax=00000000 ebx=00000000 ecx=00000033 edx=00000002 esi=2266effd edi=00271160
- eip=744fb40c esp=0026c430 ebp=0026c438 iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
有一系列的源缓冲区是0x2266efXX开头的,并且目的缓冲区是0x002711YY。怀疑这是在一个错误的循环中多次调用memmove()。
我决定分析每个调用来判断是否是独特的。在windbg中执行‘k’命令不能继续分割它,我们已经准备在上述断点减缓执行。我决定用一个轻量的windbg插件来收集返回地址:
- from pykd import *
- mashed = 0
- for frame in getStack():
- mashed += frame.returnOffset
- print "stack hash: 0x%x" % mashed
- 0:000> !py sh
- stack hash: 0x199a6804c9
现在将它添加到我们的断点,换一行并在末尾增加空格,最后重新运行:
- 0:010> bu wwlib!DllGetClassObject+0x4554 ".printf \"calling memmove(%x, %x, %x); \", poi(@esp), poi(@esp+4), poi(@esp+8); !py sh; gc"
- 0:010> g
- ...
- calling memmove(190fa4, 18f97c, e); stack hash: 0x18a96a3a98
- calling memmove(190fb2, 18f98a, f); stack hash: 0x18a96a3a98
- calling memmove(190f88, 49d7fe0, 3); stack hash: 0x1847ab6993
- calling memmove(190f8b, 49d7fe3, 3); stack hash: 0x1847ab6993
- calling memmove(190f8e, 49d7fe6, 3); stack hash: 0x1847ab6993
- calling memmove(190f91, 49d7fe9, 3); stack hash: 0x1847ab6993
- calling memmove(190f94, 49d7fec, 3); stack hash: 0x1847ab6993
- calling memmove(190f97, 49d7fef, 4); stack hash: 0x1847ab6993
- calling memmove(190f9b, 49d7ff3, 5); stack hash: 0x1847ab6993
- calling memmove(19106e, 190f9b, 5); stack hash: 0x1847ad8b4c
- calling memmove(190f9b, 49d7ff8, d3); stack hash: 0x1847ab6993
- (7dc.71c): Access violation - code c0000005 (first chance)
- First chance exceptions are reported before any exception handling.
- This exception may be expected and handled.
- eax=00000000 ebx=00000000 ecx=00000033 edx=00000002 esi=049d7ffd edi=00190fa0
- eip=744fb40c esp=0018c270 ebp=0018c278 iopl=0 nv up ei pl nz ac po nc
- cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210212
- MSVCR90!memmove+0xfc:
- 744fb40c f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
现在可以判断memmove() 在一个循环中被调用,因为在同一个栈哈希值0x1847ab6993。
六、影响
因为不能溢出返回地址和之后在写或复制操作中会访问和用到的一些值,这个漏洞的影响非常小。
Microsoft将此漏洞修补为“Microsoft Office信息泄露漏洞”,这在本文中介绍的上下文中说的通。然而,如果我们能够在溢出覆盖栈中.text中的一个指针,这个漏洞将影响更大。
在sub_316f3232函数中,有525处调用memmove_wrapper_1(),意味着有有多种途径可以触发这个漏洞。
另外在栈中没有一个函数使用了/GS保护,意味着如果返回地址被覆盖,没有系统级别的缓解措施能够缓解它。
七、总结
许多复杂的漏洞在office代码中一直存在,只是难以被发现。甚至更难去调查根因并开发利用,如果微软开放了符号表,将能更好的发现漏洞。