0x00 前言
Windows图形组件DWrite库是用于高质量文本呈现的用户模式动态库,DWrite库存在远程代码执行漏洞。目前已有POC,POC可以实现任意地址写任意内容。
0x01 漏洞信息
漏洞简述
漏洞名称:Windows图形组件远程执行代码漏洞
漏洞编号:CVE-2021-24093
漏洞类型:数组越界
CVSS评分:8.8
基础条件:用户访问网页
组件概述
Microsoft DirectWrite是用于高质量文本呈现的现代Windows API。它的大部分代码位于DWrite.dll用户模式库中。它用作各种广泛使用的桌面程序(例如Windows上的chrome,Firefox和Edge)的字体光栅化程序。
漏洞利用
使用chrome浏览器访问Web页面,渲染引擎进程异常,导致chorme页面崩溃。
漏洞影响
漏洞主要影响Win10的某些版本1909等及Windows Server 2016、2019、2004、20H2等系统。
0x02 漏洞复现
1.环境
Windows 10 1909专业版x64,chrome 86.0.4240.193(64位)
poc.html放在本机,漏洞环境也在本机
2.复现
浏览器渲染引擎崩溃
3.定位
(1) 打开Html页面时会启动多个chrome进程,首先需要定位到哪个是渲染引擎进程。
(2)先关闭chrome浏览器(否则会影响定位结果),使用火绒剑来定位渲染引擎进程。
做如下设置
(3)首先先看看运行崩溃的正常捕获情况:
我们看到id为4780的进程发生过崩溃,盲猜目标就是第六个进程,此时我们尝试不加载ttf文件
此时便没有崩溃进程,利用windbg attach这个进程,输入g运行,chrome加载ttf文件,此时便可以断到引发崩溃的位置。
看看当前寄存器r8:
0x04 漏洞分析
基本信息
漏洞文件:DWrite.dll
漏洞函数:fsg_ExecuteGlyph
漏洞对象:TrueType字体中的”maxp”表
背景知识
TrueType字体通常包含在单个TrueType字体文件中,其后缀为.TTF。TrueType中的所有数据都使用big-endian编码,TTF文件中包含了字体的版本号和几个表。
typedef sturct
{
char tag[4];
ULONG checkSum;
ULONG offset;
ULONG length;
}TableEntry;
typedef struct
{
Fixed sfntversion; //0x10000 for version 1.0
USHORT numTables;
USHORT searchRange;
USHORT entrySelector;
USHORT rangeShift;
TableEntry entries[1];//variable number of TableEntry
}TableDirectory;
TableDirectory结构的最后一个字段是可变长度的tableentry结构的数组,安体中的每个表对应其中一项。TrueType字体中的每个表都保存了不同的逻辑信息—–如图元中数据、字符到图元的映射、字距调整信息等等。
head 字体头 字体的全局信息
cmap 字符代码到图元的映射 把字符代码映射为图元索引
glyf 图元数据 图元轮廓定义以及网格调整指令
maxp 最大需求表 字体中所需内存分配情况的汇总数据
mmtx 水平规格 图元水平规格
loca 位置表索引 把元索引转换为图元的位置
name 命名表 版权说明、字体名、字体族名、风格名等等
hmtx 水平布局 字体水平布局星系:上高、下高、行间距、最大前进宽度、最小左支撑、最小右支撑
kerm 字距调整表 字距调整对的数组
post PostScript信息 所有图元的PostScript FontInfo目录项和PostScript名
PCLT PCL 5数据 HP PCL 5Printer Language 的字体信息:字体数、宽度、x高度、风格、记号集等等
OS/2 OS/2和Windows特有的规格 TrueType字体所需的规格集
TrueType字体是一种非常灵活的数据结构,它可以包含可变数目的图元,每个图元可以有不同数目的控制点,甚至还可以有数量可变的图元指令。最大需求表的目的是告知字体栅格器(rasterizer)对内存的需求,以便 在出来字体前分配合适大小的内存。因为性能对字体栅格器非常重要,像MFC的CAarray那样需要频繁进行数据拷贝操作的动态增长的数据结构不合要求。下面是maxp表的结构。
typedef struct
{
Fixed Version;//0x00010000 for version 1.0. 4bit
USHORT numGlypha; //Number of glyphs in the font .
USHORT maxPoints; //Max points in noncomposite glyph .
RSHORT maxContours; //Max contours in noncomposite glyph.
USHORT maxCompositePoints;//Max points in a composite glyph.
USHORT maxCompositeContours; //Max contours in a composite glyph.
USHORT maxZones;// 1 if not use the twilight zone [Z0],
//or 2 if so use Z0;2 in most cases.
USHORT max TwilightPoints ;/ Maximum points used in Z0.
USHORT maxStorage; //Number of storage area locations.
USHORT maxFunctionDefs; //Number of FDEFs.
USHORT maxStackElements; //Number of depth.
USHORT maxSizeOfInstructions; //Max byte count for glyph inst.
USHORT maxComponentElements; //Max number top components refernced.
USHORT maxComponentDepth; //Max levels of recursion.
}Table_maxp;
详细分析
ttf文件的二进制数据
USHORT == 2 bit
RSHORT == 2 bit
方框的位置是maxp表的tableEntry结构,偏移0x158的是maxp表的内容,numGlypha字段是0x0006 maxPoints字段值为0x0000(距离箭头2偏移0x6),maxContours字段是0x002a,maxCompositePoints字段为0x0003(距离箭头2偏移0xA)。
poc内容:
正常情况下,ttf文件中maxPoints字段值为0x168,maxCompositePoins字段值为0x2352,在poc.ttf文件中将”maxp”结构中maxPoints字段的值改为0,将maxCompositePoins值改为3,当加载并光栅化损坏的”maxp”表的数据时,会导致堆分配缓冲区过小,调用栈如下:
函数DWrite!fsg_ExecuteGlyph崩溃,调用栈如下:
# Child-SP RetAddr Call Site
00 000000ff`debfa170 00007fff`c05a3886 DWrite!fsg_ExecuteGlyph+0x772
01 000000ff`debfa2a0 00007fff`c05a35ab DWrite!fsg_CreateGlyphData+0x12e
02 000000ff`debfa360 00007fff`c05a2d65 DWrite!fsg_GridFit+0xbb
03 000000ff`debfa3f0 00007fff`c05a17fa DWrite!fs__Contour+0x1c1
04 000000ff`debfa4e0 00007fff`c05a15e7 DWrite!TrueTypeRasterizer::Implementation::RasterizeInternal+0xfa
05 000000ff`debfa520 00007fff`c05a1534 DWrite!TrueTypeRasterizer::Implementation::GetBitmapInternal+0x2f
查看漏洞库版本
0:000> lmm DWrite -v
Unknown option '-'
Browse full module list
start end module name
00007fff`c0520000 00007fff`c081e000 DWrite (pdb symbols) C:\ProgramData\Dbg\sym\DWrite.pdb\FCC9572DA72927C3057C3859D67B0DD61\DWrite.pdb
Loaded symbol image file: C:\Windows\SYSTEM32\DWrite.dll
Image path: C:\Windows\SYSTEM32\DWrite.dll
Image name: DWrite.dll
Browse all global symbols functions data
Image was built with /Brepro flag.
Timestamp: 793762E4 (This is a reproducible build file hash, not a timestamp)
CheckSum: 00300C4B
ImageSize: 002FE000
File version: 10.0.18362.356
Product version: 10.0.18362.356
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
Information from resource tables:
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: DWrite
OriginalFilename: DWrite
ProductVersion: 10.0.18362.356
FileVersion: 10.0.18362.356 (WinBuild.160101.0800)
FileDescription: Microsoft DirectX Typography Services
LegalCopyright: © Microsoft Corporation. All rights reserved.
静态分析
用IDA逆向DWrite.dll基本上看不出什么东西…
动态分析
漏洞如何利用
不调试真不知道都在干些什么:断到memset处,第三个参数是0x148
一直运行到崩溃的位置
向无效地址写入内容,自然会引起崩溃,我们注意到在前面的mov r8,qword ptr [rax+8]对r8进行赋值的,我们看看r8地址的内容:
那么猜测一下可能是以下两个问题:
1.由于溢出的原因,导致读取的ttf数据覆盖掉了本来的合法地址
2.由于rsi本身偏移的问题,也就是说rsi的值向后移动了一部分,将后面的ttf的数据作为地址
那么我们就需要关注一下一开始的堆地址以及后面的寄存器中值的变化:
第一个memset:
断在rcx赋值时
第一个堆地址:0x000001e22eba6c50,我们发现在memset的时候堆地址是0x000001e22eba71ec(偏移增加了0x59c)
第二个memset:
第二个堆地址:0x000001e22eba6c50,我们发现memset的时候地址是0x000001e22eba71c0(偏移增加了0x6c0)
赋值非法地址给r8的时候
rsi的值是:0x000001e22eba7310
TTF中的数据覆盖了[rsi+8],在后面的add word ptr [r8+56],ax指令中,ax其实也是可以通过ttf数据控制
也就是说通过
mov r8, qword ptr [rsi+8]
mov rax, qword ptr [rsi+0A8h]
add word ptr [r8+56h], ax
就可以达到任意代码执行的目的,因为指针和指针的值都可以通过ttf数据控制。
但是我们还是没有解决到底是什么原因触发的这个漏洞
触发漏洞的原因
函数分析:fsg_ExecuteGlyph函数对堆块内部的两个整数数组,对应于x坐标和y坐标进行操作,实际上数组的较小,fsg_ExecuteGlyph函数先调用了两次memset将数组清零,如果字体是一个变量且指定了轴值,还会调用TrueTypeRasterizer::Implementation::ApplyOutlineVariation->GlyphOutlineVariationInterpolator::ApplyVariation将字体中的数据赋值到坐标数组,这样就会破坏到后续的结构成员。
那么我们可以看看这个堆块到底是分配了多大的:
计算内存大小的函数调用链为TrueTypeRasterizer::Implementation::Initialize-> fs_NewSfnt ->fsg_WorkSpaceSetOffsets函数fsg_WorkSpaceSetOffsets内部计算需要申请的内存空间大小并将结果传出到fs_NewSfnt中。
计算所需内存的过程可以通过条件断点的方式来调试,附加到调试器后,可以设置如下断点命令
bp DWrite!TrueTypeRasterizer::Implementation::Initialize “r $t0=$t0+1; .printf "Initialize times:%d\n",@$t0;.echo;gc”
我们看到第13次调用TrueTypeRasterizer::Implementation::Initialize函数之后就会进入崩溃
重启下断点:
bp DWrite!TrueTypeRasterizer::Implementation::Initialize “r $t0=$t0+1; .printf "Initialize times:%d\n",@$t0;.echo;.if(@$t0 == 0x0D){}.else{gc}”
重启在调用fs_NewSfnt前下断点
继续运行:
在崩溃函数中下断点,目的是看看操作的空间和分配的空间:
内存起始位置是0000012f8cd56c50,内存块使用地址是0000012f8cd571ec
查看堆块大小以及addr1相对偏移0x59c
我们看看之前赋值非法地址到r8处的值
第一个堆地址相对于堆块起始位置是0x59c+memset的0x148的初始化>0x6c8
也就是说在这个函数中:
0x05 patch
分配堆块更大,x数组和y数组的大小还是0x148字节,ESI对象距离堆块起始地址的距离变大,所以在x数组和y数组的赋值过程中没有覆盖到ESI对象。也就是说addr+0x148<esi对象对应的堆地址