CVE-2016-7595 Apple macOS/iOS CoreText OTL::GPOS::ApplyPairPos 越界访问漏洞分析

前言

2016年12月的Apple安全公告中(macOS公告iOS公告),修复4个由腾讯安全平台部终端安全团队报告的漏洞,其中有2个是字体解析造成的越界访问漏洞,影响 macOS/iOS/watchOS/tvOS等多个平台系统,本文主要分析其中的 CVE-2016-7595 字体漏洞【图1】。



图1

这个漏洞在报给Apple 17天后发布 macOS 10.2.2 测试版补丁,一个半月后发布安全公告和补丁(包括iOS、watchOS和tvOS),难得看见苹果这么积极一次。

###漏洞分析
此次漏洞是通过Fuzzing发现的,直接对比poc与原始文件的数据,可以发现其实就1个字节的差异(0x00 => 0x6C)【图2】:



图2

用ttx命令分析字体格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
╭─riusksk@MacBook  ~/Downloads ‹›
╰─➤$ ttx poc.ttf
Dumping "poc.ttf" to "poc#1.ttx"...
Dumping 'GlyphOrder' table...
Dumping 'head' table...
Dumping 'hhea' table...
Dumping 'maxp' table...
Dumping 'OS/2' table...
Dumping 'hmtx' table...
Dumping 'cmap' table...
Dumping 'fpgm' table...
Dumping 'prep' table...
Dumping 'cvt ' table...
Dumping 'loca' table...
Dumping 'glyf' table...
Dumping 'name' table...
Dumping 'post' table...
Dumping 'gasp' table...
/usr/local/lib/python2.7/site-packages/FontTools/fontTools/ttLib/tables/otTables.py:60: UserWarning: Coverage table has start glyph ID out of range: glyph27713.
warnings.warn("Coverage table has start glyph ID out of range: %s." % start)
An exception occurred during the decompilation of the 'GPOS' table
Dumping 'GPOS' table...
Dumping 'GSUB' table...
Dumping 'DSIG' table...

从上面的提示可以看出,是在解析GPOS表时,通过glyphIDCoverage表索引时导致越界了,其中glyph27713的数值正是0x6C41(27713),也就是上面图1中文件对比的差异值。虽然这是FontTools工具的错误,不代表Apple系统本身,但它跟Apple系统导致崩溃的是同一字节,从这可以直接得到导致崩溃的关键字节是glyphID值。

GPOS表

TrueType/OpenType字体格式中的GPOS表是用于为字体中文本布局及渲染提供glyph位置信息的表,表中各个字体结构如图3所示:



图3

GPOS表主要包含3个子表:ScriptList、FeatureList和LookupList,本次漏洞主要问题在LookupList子表中的PairAdjustmentPositioning中,PairAdjustmentPositioning子表(PairPos)被用于调整两个glyphs彼此之间的位置。

PairPos表下又包含多个PairSet数组,PairSet数组包含Coverage表中每个glyph对应的偏移量,并按Coverage Index来排序。

PairSet下包含PairValueRecord指定每一glyph配对(pair)中的第二个glyph(SecondGlyph)的glyph名和索引值GlyphID(对应【图3】中glyphRefID),同时包含两个ValueRecord值去指定第一个glyph和第二个glyph的位置信息。

导致越界的漏洞正是用于索引的GlyphID(glyphRefID),用ttx解析原有正常字体文件生成的xml文件,如图4所示,index=”65”就是正常GlyphID值 00 41,如果随便给第1字节设置个值都会导致崩溃。



图4

调试

用lldb调试下,崩溃后的地址及栈回溯如下:

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
(lisa)run poc.ttf 
Process 96714 launched
Process 96714 stopped
* thread #1: tid = 0x3f119d, 0x00007fffa7c01491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x3007ddfae)
frame #0: 0x00007fffa7c01491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411
CoreText`OTL::GPOS::ApplyPairPos:
-> 0x7fffa7c01491 <+411>: mov ax, word ptr [r14 + 2*rax + 0xa]
0x7fffa7c01497 <+417>: rol ax, 0x8
0x7fffa7c0149b <+421>: movzx eax, ax
0x7fffa7c0149e <+424>: lea rsi, [r14 + rax]
(lisa)register read rax
rax = 0x00000000ffffffff
(lisa)x $r14+2*$rax+0xa
error: memory read failed for 0x3007c5600

(lisa)bt
* thread #1: tid = 0x3f119d, 0x00007fffa7c01491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x3007ddfae)
* frame #0: 0x00007fffa7c01491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411
frame #1: 0x00007fffa7c05907 CoreText`OTL::GPOS::ApplyLookupSubtable(OTL::Lookup const&, unsigned int, OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 85
frame #2: 0x00007fffa7c02c7b CoreText`OTL::GPOS::ApplyLookupAt(OTL::Lookup const&, TGlyphIterator&) const + 339
frame #3: 0x00007fffa7b68ff4 CoreText`OTL::GPOS::ApplyLookups(TRunGlue&, int, OTL::GlyphLookups&) const + 448
frame #4: 0x00007fffa7b68897 CoreText`TOpenTypePositioningEngine::PositionRuns(SyncState&, KerningStatus&) + 839
frame #5: 0x00007fffa7b66d05 CoreText`TKerningEngine::PositionGlyphs(TLine&, TCharStream const*) + 347
frame #6: 0x00007fffa7bbe59d CoreText`TTypesetter::FinishLayout(std::__1::tuple<TLine const*, TCharStream const*, void const* (*)(__CTRun const*, __CFString const*, void*), void*, std::__1::shared_ptr<TBidiLevelsProvider>*, unsigned int, unsigned char> const&, TLine&, SyncState) + 35
frame #7: 0x00007fffa7b5586d CoreText`TTypesetterAttrString::Initialize(__CFAttributedString const*) + 865
frame #8: 0x00007fffa7b552ea CoreText`CTLineCreateWithAttributedString + 59
frame #9: 0x00007fffb8be086e UIFoundation`__NSStringDrawingEngine + 10669
frame #10: 0x00007fffb8be69ca UIFoundation`-[NSAttributedString(NSExtendedStringDrawing) boundingRectWithSize:options:context:] + 605
frame #11: 0x00007fffb8bdcc43 UIFoundation`-[NSAttributedString(NSStringDrawing) size] + 59
frame #12: 0x0000000100047d70 Font Book`___lldb_unnamed_symbol1053$$Font Book + 368
frame #13: 0x00000001000476e9 Font Book`___lldb_unnamed_symbol1052$$Font Book + 89
frame #14: 0x00000001000475b0 Font Book`___lldb_unnamed_symbol1049$$Font Book + 774
frame #15: 0x000000010006a2d2 Font Book`___lldb_unnamed_symbol1860$$Font Book + 110
frame #16: 0x000000010005d4ee Font Book`___lldb_unnamed_symbol1545$$Font Book + 2651
frame #17: 0x00007fffa46b2451 AppKit`-[NSApplication _doOpenFile:ok:tryTemp:] + 253
frame #18: 0x00007fffa427f789 AppKit`-[NSApplication finishLaunching] + 1624
frame #19: 0x00007fffa427ed2a AppKit`-[NSApplication run] + 267
frame #20: 0x00007fffa4249a8a AppKit`NSApplicationMain + 1237
frame #21: 0x0000000100001527 Font Book`___lldb_unnamed_symbol1$$Font Book + 11
frame #22: 0x00007fffbb632255 libdyld.dylib`start + 1
frame #23: 0x00007fffbb632255 libdyld.dylib`start + 1

[r14 + 2*rax + 0xa]索引错误,很典型的数组越界指令。

通过设置条件断点去记录获取的GlyphID以及后面的将其传参给OTL::Coverage::SearchFmt2Binary函数后返回值,可以发现最后当GlyphID=0x55(85)时返回值0,最后触发崩溃,所以样本中的GlyphID只要>=0x55都会导致崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lisa) p "GlyphID" $eax
(unsigned int) $95 = 85
(lisa) c
Process 25648 resuming
Command #2 'c' continued the target.
(lisa) p "SearchFmt2Binary参数2" $esi
(unsigned int) $96 = 85
(lisa) c
Process 25648 resuming
Command #2 'c' continued the target.
(lisa) p "返回值" $eax
(unsigned int) $97 = 0
(lisa) c
Process 25648 resuming
Command #2 'c' continued the target.
Process 25648 stopped
* thread #1: tid = 0x43fc6, 0x00007fffcb443491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x3007bc6fe)
frame #0: 0x00007fffcb443491 CoreText`OTL::GPOS::ApplyPairPos(OTL::LookupSubtable const*, TGlyphIterator&, OTL::Coverage const&) const + 411
CoreText`OTL::GPOS::ApplyPairPos:
-> 0x7fffcb443491 <+411>: mov ax, word ptr [r14 + 2*rax + 0xa]
0x7fffcb443497 <+417>: rol ax, 0x8
0x7fffcb44349b <+421>: movzx eax, ax
0x7fffcb44349e <+424>: lea rsi, [r14 + rax]

返回值为0时,经dec减1后为0xFFFFFFFF,以此为索引值,最后导致越界访问。

1
2
dec     eax     ; eax=0xFFFFFFFF
mov ax, [r14+rax*2+0Ah] ; 越界访问,导致崩溃

漏洞修复

苹果已经发布安全补丁,macOS用户可升级到10.12.2,iOS用户可升级到10.2。

对补丁进行比对,可以发现在漏洞函数OTL::GPOS::ApplyPairPos 中添加了判断【图5】,获取到的GlyphID值传递给OTL::Coverage::SearchFmt2Binary函数,当查找失败时会返回0,因此只要添加判断返回值是否为0,为0则直接跳走返回。



图5

处理流程

  1. 2016-10-23 通过邮件提交给Apple
  2. 2016-11-09 Apple确认漏洞,并在 macOS Sierra 10.12.2 beta 测试版中修复
  3. 2016-12-09 分配CVE号:CVE-2016-7595
  4. 2016-12-14 Apple发布安全公告,并推送补丁