背景
最近,ZDI发布了关于Safari越界写入漏洞的公告,该漏洞由Manfred Paul在Pwn2Own中利用。我们决定研究补丁并尝试利用它。
补丁相当简单:它创建了一个新函数(IntRange::sExt
),用于在应用符号扩展操作后决定整数范围(在rangeFor
中)。在此之前,程序假设应用符号扩展后范围保持不变。这是不正确的,可能导致错误地移除溢出/下溢检查。
补丁提交如下:
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
From 6983e76741a1bad811783ceac0959ff9953c175d Mon Sep 17 00:00:00 2001
From: Mark Lam <[email protected]>
Date: Fri, 20 May 2022 18:33:04 +0000
Subject: [PATCH] Refine B3ReduceStrength's range for sign extension
operations. https://bugs.webkit.org/show_bug.cgi?id=240720
<rdar://problem/93536782>
Reviewed by Yusuke Suzuki and Keith Miller.
* Source/JavaScriptCore/b3/B3ReduceStrength.cpp:
Canonical link: https://commits.webkit.org/250808@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@294563 268f45cc-cd09-0410-ab3c-d52691b4dbfc
---
Source/JavaScriptCore/b3/B3ReduceStrength.cpp | 61 ++++++++++++++++++-
1 file changed, 59 insertions(+), 2 deletions(-)
diff --git a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
index f30a68587876..32bcf3d81415 100644
--- a/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
+++ b/Source/JavaScriptCore/b3/B3ReduceStrength.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015-2020 Apple Inc. All rights reserved.
+ * Copyright (C) 2015-2022 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -388,6 +388,61 @@ class IntRange {
}
}
+ template<typename T>
+ IntRange sExt()
+ {
+ ASSERT(m_min >= INT32_MIN);
+ ASSERT(m_max <= INT32_MAX);
+ int64_t typeMin = std::numeric_limits<T>::min();
+ int64_t typeMax = std::numeric_limits<T>::max();
+ auto min = m_min;
+ auto max = m_max;
+
+ if (typeMin <= min && min <= typeMax
+ && typeMin <= max && max <= typeMax)
+ return IntRange(min, max);
+
+ // Given type T with N bits, signed extension will turn bit N-1 as
+ // a sign bit. If bits N-1 upwards are identical for both min and max,
+ // then we're guaranteed that even after the sign extension, min and
+ // max will still be in increasing order.
+ //
+ // For example, when T is int8_t, the space of numbers from highest to
+ // lowest are as follows (in binary bits):
+ //
+ // highest 0 111 1111 ^
+ // ... |
+ // 1 0 000 0001 | top segment
+ // 0 0 000 0000 v
+ //
+ // -1 1 111 1111 ^
+ // -2 1 111 1110 | bottom segment
+ // ... |
+ // lowest 1 000 0000 v
+ //
+ // Note that if we exclude the sign bit, the range is made up of 2 segments
+ // of contiguous increasing numbers. If min and max are both in the same
+ // segment before the sign extension, then min and max will continue to be
+ // in a contiguous segment after the sign extension. Only when min and max
+ // spans across more than 1 of these segments, will min and max no longer
+ // be guaranteed to be in a contiguous range after the sign extension.
+ //
+ // Hence, we can check if bits N-1 and up are identical for the range min
+ // and max. If so, then the new min and max can be be computed by simply
+ // applying sign extension to their original values.
+
+ constexpr unsigned numberOfBits = countOfBits<T>;
+ constexpr int64_t segmentMask = (1ll << (numberOfBits - 1)) - 1;
+ constexpr int64_t topBitsMask = ~segmentMask;
+ int64_t minTopBits = topBitsMask & min;
+ int64_t maxTopBits = topBitsMask & max;
+
+ if (minTopBits == maxTopBits)
+ return IntRange(static_cast<int64_t>(static_cast<T>(min)), static_cast<int64_t>(static_cast<T>(max)));
+
+ return top<T>();
+ }
+
IntRange zExt32()
{
ASSERT(m_min >= INT32_MIN);
@@ -2765,9 +2820,11 @@ class ReduceStrength {
rangeFor(value->child(1), timeToLive - 1), value->type());
case SExt8:
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int8_t>();
case SExt16:
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int16_t>();
case SExt32:
- return rangeFor(value->child(0), timeToLive - 1);
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int32_t>();
|
背景
在深入探讨漏洞之前,我们将描述相关的术语、概念和涉及的代码。
JavaScriptCore(JSC)中的即时(JIT)编译
JavaScriptCore(JSC)有4层执行:
- 解释器(无JIT)
- 基线JIT(非常简単,只是将某些字节码一对一映射到汇编)
- DFG JIT(数据流图)
- FTL JIT(超光速)
JIT使用了多个IR,从高级到低级列出如下:
- DFG IR(顾名思义,由DFG JIT使用)
- DFG SSA IR(DFG转换为SSA形式以允许更多教科书式优化,用于FTL)
- B3(称为BareBones Backend,比DFG更低级,丢弃所有JS语义以允许更多优化,用于FTL)
- Air(汇编IR,非常接近汇编,用于FTL)
此补丁应用于FTL管道中的B3优化阶段之一,即“Reduce Strength”阶段。
如果您对DFG和FTL JIT的详细工作原理感兴趣,可以阅读Filip Pizlo在WebKit博客上的文章“Speculation in JavaScriptCore”。如果您像我一样阅读速度慢,可能需要4-5天才能读完。
由于此漏洞发生在B3中,您可能想了解更多:
强度减少
直接从维基百科复制:
在编译器构造中,强度减少是一种编译器优化,其中昂贵的操作被替换为等效但更便宜的操作。强度减少的经典示例将循环内的“强”乘法转换为“弱”加法——这经常发生在数组寻址中。(Cooper, Simpson & Vick 1995, p. 1)
强度减少的示例包括:
- 将循环内的乘法替换为加法
- 将循环内的幂运算替换为乘法
在b3/B3ReduceStrength.cpp中进行了许多强度减少优化,代码近3k行。例如:
1
2
3
4
5
6
7
8
9
10
11
12
|
// Turn this: Add(value, zero)
// Into an Identity.
//
// Addition is subtle with doubles. Zero is not the neutral value, negative zero is:
// 0 + 0 = 0
// 0 + -0 = 0
// -0 + 0 = 0
// -0 + -0 = -0
if (m_value->child(1)->isInt(0) || m_value->child(1)->isNegativeZero()) {
replaceWithIdentity(m_value->child(0));
break;
}
|
更复杂的示例:
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
|
case Sub:
// Turn this: Sub(BitXor(BitAnd(value, mask1), mask2), mask2)
// Into this: SShr(Shl(value, amount), amount)
// Conditions:
// 1. mask1 = (1 << width) - 1
// 2. mask2 = 1 << (width - 1)
// 3. amount = datasize - width
// 4. 0 < width < datasize
if (m_value->child(0)->opcode() == BitXor
&& m_value->child(0)->child(0)->opcode() == BitAnd
&& m_value->child(0)->child(0)->child(1)->hasInt()
&& m_value->child(0)->child(1)->hasInt()
&& m_value->child(1)->hasInt()) {
uint64_t mask1 = m_value->child(0)->child(0)->child(1)->asInt();
uint64_t mask2 = m_value->child(0)->child(1)->asInt();
uint64_t mask3 = m_value->child(1)->asInt();
uint64_t width = WTF::bitCount(mask1);
uint64_t datasize = m_value->child(0)->child(0)->type() == Int64 ? 64 : 32;
bool isValidMask1 = mask1 && !(mask1 & (mask1 + 1)) && width < datasize;
bool isValidMask2 = mask2 == mask3 && ((mask2 << 1) - 1) == mask1;
if (isValidMask1 && isValidMask2) {
Value* amount = m_insertionSet.insert<Const32Value>(m_index, m_value->origin(), datasize - width);
Value* shlValue = m_insertionSet.insert<Value>(m_index, Shl, m_value->origin(), m_value->child(0)->child(0)->child(0), amount);
replaceWithNew<Value>(SShr, m_value->origin(), shlValue, amount);
break;
}
}
|
总之,这里的一切并不复杂。目标是减少算术操作的使用。
SExt
在代码中,SExt用作符号扩展的简写。此程序中有3种SExt变体:SExt8、SExt16、SExt32。它们都对值执行符号扩展到64位。后面的数字是原始值的位宽。
例如,SExt8将0xfa扩展为0xfffffffffffffffa,SExt16将0xf7b2扩展为0xfffffffffffff7b2,同样的想法适用于SExt32。
代码理解
现在,是时候查看补丁中涉及的代码了。
rangeFor
新创建的sExt
函数在rangeFor
中被调用。
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
|
IntRange rangeFor(Value* value, unsigned timeToLive = 5)
{
if (!timeToLive)
return IntRange::top(value->type());
switch (value->opcode()) {
case Const32:
case Const64: {
int64_t intValue = value->asInt();
return IntRange(intValue, intValue);
}
case BitAnd:
if (value->child(1)->hasInt())
return IntRange::rangeForMask(value->child(1)->asInt(), value->type());
break;
...
case Add:
return rangeFor(value->child(0), timeToLive - 1).add(
rangeFor(value->child(1), timeToLive - 1), value->type());
...
case SExt8:
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int8_t>();
case SExt16:
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int16_t>();
case SExt32:
- return rangeFor(value->child(0), timeToLive - 1);
+ return rangeFor(value->child(0), timeToLive - 1).sExt<int32_t>();
}
|
rangeFor
返回一个IntRange
。它的行为有点像构造函数,从Value
创建IntRange
对象。除了rangeFor
,IntRange
类还有另外2个静态函数:
rangeForMask
- 当value->opcode() == BitAnd
时调用
rangeForZShr
- 当value->opcode() == ZShr
时调用
Value
包含以下字段,以操作a = b + c
为例:
opcode
- 用于生成此值的DFG操作码。在上面的示例中,a
有Add
作为其操作码。
child
- 用于生成此值的其他Value
。a
有2个子项,b
和c
。
type
- 对于IntRange
,仅用于指示值是Int32还是Int64。
除了Value
,rangeFor
还接受一个可选参数timeToLive
。
1
|
IntRange rangeFor(Value* value, unsigned timeToLive = 5)
|
递归
这是对给定Value
的子项递归应用rangeFor
的深度。对于除Const32
、Const64
和BitAnd
之外的所有操作码,rangeFor
将递归调用自身,例如:
1
2
3
4
5
6
7
8
9
|
case Add:
return rangeFor(value->child(0), timeToLive - 1).add(
rangeFor(value->child(1), timeToLive - 1), value->type());
case Sub:
return rangeFor(value->child(0), timeToLive - 1).sub(
rangeFor(value->child(1), timeToLive - 1), value->type());
case Mul:
return rangeFor(value->child(0), timeToLive - 1).mul(
rangeFor(value->child(1), timeToLive - 1), value->type());
|
当timeToLive
为0时停止:
1
2
|
if (!timeToLive)
return IntRange::top(value->type());
|
top
根据Value
的位宽返回完整的整数范围。在抽象解释中,此完整范围称为top。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
template<typename T>
static IntRange top()
{
return IntRange(std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
}
static IntRange top(Type type)
{
switch (type.kind()) {
case Int32:
return top<int32_t>();
case Int64:
return top<int64_t>();
default:
RELEASE_ASSERT_NOT_REACHED();
return IntRange();
}
}
|
在这里我们看到,在“评估”范围5层深之后,函数放弃并只返回完整的整数范围。
或者,如果它在switch-case块中看到不支持的操作码,它也将返回top。
1
2
3
4
5
|
default:
break;
}
return IntRange::top(value->type());
|
这在存在Phi值的情况下是可能的。例如:
1
2
3
4
5
|
let a = 10;
if (...) {
a = 20;
}
let b = a * 2;
|
在这种情况下,b
将有Mul
作为其操作码,和2个子项:
Phi(Const32(10), Const32(20))
Const32(2)
第一个子项将被赋予top范围,尽管我们知道它是[10, 20]。这在编写漏洞利用时出现,但只是一个小问题。
基本情况
像每个递归函数一样,必须有一些基本情况。这里有2个。
首先,如果Value
是常量。(例如,当a = 0x1337
或a = b + 0x1337
时看到)
1
2
3
4
5
|
case Const32:
case Const64: {
int64_t intValue = value->asInt();
return IntRange(intValue, intValue);
}
|
其次,当Value
有BitAnd
操作码时。(例如,a = b & 0xfb
)
1
2
3
4
|
case BitAnd:
if (value->child(1)->hasInt())
return IntRange::rangeForMask(value->child(1)->asInt(), value->type());
break;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
template<typename T>
static IntRange rangeForMask(T mask)
{
if (!(mask + 1))
return top<T>();
if (mask < 0)
return IntRange(INT_MIN & mask, mask & INT_MAX);
return IntRange(0, mask);
}
static IntRange rangeForMask(int64_t mask, Type type)
{
switch (type.kind()) {
case Int32:
return rangeForMask<int32_t>(static_cast<int32_t>(mask));
case Int64:
return rangeForMask<int64_t>(mask);
default:
RELEASE_ASSERT_NOT_REACHED();
return IntRange();
}
}
|
简而言之,上面的BitAnd
代码取其第二个操作数,并将其作为掩码应用于IntRange
的最小和最大值。以a = b & 0x2ffff
为例。如果b
在范围[0x1337, 0x31337]中。应用&操作后,新范围是[0x1337, 0x21337],因为掩码0x2ffff应用于2个值0x1337和0x31337