深入解析CVE-2022-32792:WebKit B3ReduceStrength越界写入漏洞

本文详细分析了CVE-2022-32792漏洞,该漏洞源于WebKit的B3ReduceStrength阶段在符号扩展操作中的范围计算错误,导致溢出检查被错误移除,从而可能引发越界写入。文章涵盖了JIT编译、强度减少优化、漏洞原理及利用步骤。

背景

最近,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对象。除了rangeForIntRange类还有另外2个静态函数:

  • rangeForMask - 当value->opcode() == BitAnd时调用
  • rangeForZShr - 当value->opcode() == ZShr时调用

Value包含以下字段,以操作a = b + c为例:

  • opcode - 用于生成此值的DFG操作码。在上面的示例中,aAdd作为其操作码。
  • child - 用于生成此值的其他Valuea有2个子项,bc
  • type - 对于IntRange,仅用于指示值是Int32还是Int64。

除了ValuerangeFor还接受一个可选参数timeToLive

1
IntRange rangeFor(Value* value, unsigned timeToLive = 5)

递归

这是对给定Value的子项递归应用rangeFor的深度。对于除Const32Const64BitAnd之外的所有操作码,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 = 0x1337a = b + 0x1337时看到)

1
2
3
4
5
case Const32:
case Const64: {
    int64_t intValue = value->asInt();
    return IntRange(intValue, intValue);
}

其次,当ValueBitAnd操作码时。(例如,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

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计