首页
论坛
课程
招聘
[原创]4月13日 Chrome爆出的v8漏洞车祸现场分析
2021-4-20 18:55 7982

[原创]4月13日 Chrome爆出的v8漏洞车祸现场分析

2021-4-20 18:55
7982

0x1: 关于v8引擎

 1.1v8的指针为真实地址+1,这样最后一位为1,据说是为了加快寻址的速度,根据最后一位直接就可以判断该值其代表的是指针还是自然数。

这点在逆向的角度就是优化后生成的指令,

  a)  看到到有 add rX,1 这样的指令片段一般就是代表着开始对指针指向对象的初始化。

  b)  在内存中看到的数值为真实数值*2

 1.2:在比较新版本的v8对一个地址只是存储其低4位,要进行寻址的时候再加上其高八位。

 1.3:v8在多次执行一个函数的时候,会触发JIT(优化)过程,直接生成函数对应的机器码,这也是这篇分析文章的理论基础。

0x2:JIT产生的优化指令分析:

     分析环境为Windows 10 x64

              v8版本8.9.25

             分析工具为x64debug

           https://bbs.pediy.com/thread-267049.htm

     紧跟着前面前面

      poc为:

      const _arr = new Uint32Array([2**31]);
           function foo() {
                 var x = 1; 
                 x = (_arr[0] ^ 0) + 1; 
 
                 x = Math.abs(x); 
                 x -= 2147483647;
                 x = Math.max(x, 0); 
                 x -= 1;//
                 if(x==-1) x = 0; 
                 var corr = new Array(x);
                 arr.shift();
                 var arr = [1.1, 1.2, 1.3];
 
                 return [arr, cor];
            }
     console.log("ready !!");
     for(i=0;i<0x3000;i++)
     {
          foo();
     }
    %SystemBreak();
    var x = foo();
    var corr=x[0];
    var arr=x[1];
    %DebugPrint(corr);
    %DebugPrint(arr);
    console.log("Analyze Over!")

2.1:v8优化后生成指令对有符号数的错误处理分析

2.1.1x64debug打开d8.exe程序加载poc之后,会在运行到在%SystemBreak()时会断下; 在断点返回以后单步运行一段时间可以看到:

00000039000C404C | 49:BA 60EB0C43F77F0000   | mov r10,<d8.Builtins_CompileLazyDeoptimizedCode>    | 7FF7430CEB60:"I嫕Hi                                                 

                               图2.1.1.1                                                                           

分析一下图2.1.1.1的指令逻辑,这里是对是否已经优化做判断,如果已经优化就执行优化后生成的指令,否则则是通过r10跳转到d8.Builtins_CompileLazyDeoptimizedCode ,先执行优化过程。

运行到这里显然已经完成了优化,继续下去执行的是优化后生成的指令。

                      

                                                                              图2.1.1.2

                        

                                                                         图2.1.1.3            

2.1.2:  调试器走到图2.1.1.3所在的位置,这里的指令为:

    mov ecx,edi

    add rcx,r8

    mov ecx,dword ptr ds:[rcx]

这里进行的是v8的数组的取值操作,取完值以后对该值进行+1操作,对应的是poc中的x=(_arr[0] ^ 0)+1js代码

       

                             图2.1.2.1  

                   

                                                                          图2.1.2.2  

走到图2.1.2.1的add rcx,1这句指令后,结果为0x80000001,并将其放在rcx指令中,接下来用一些特殊指令进行处理,如下图所示:

                   

                                                                                 图2.1.2.3 

2.1.3:这些指令对应的是poc中的x = Math.abs(x) js语句,应该是做了些优化;

但是这些指令执行完以后,rcx还是为0x80000001,而后面的指令中使用的是ecx作为参数进行运算。 

也就是说v8优化后产生的这些指令,并未正确处理x = Math.abs(x);的操作。这也是后面一连串错误操作的起始原因。

                  

                                                                          图2.1.3.1 

2.1.4:流程运行到这里直接用rdi寄存器的ecx来进行计算,由上图可以看到,导致运算为0x80000001-0x7fffffff,结果等于2

接下来是指令

   cmp ecx,0

       jl 39000C40CA  

 这里是执行源poc里面的x = Math.max(x, 0)js语句,最后结构返回x=2 。这段指令将结果放入到rdi寄存器中,此时edi表示参数x。               

              

                                                                     图2.1.4.1 

             

                                                                     图2.1.4.2

            

                                                                  图2.1.4.3

 2.1.5: 上图所示,edi进行-1运算(也就是poc js文件里面的参数x),此时edi=1,结果自然是不等于0xFFFFFFFF,最后越过poc 中的x==-1的判断。        对应poc中的js代码if(x==-1) x = 0;

然后在之后的流程里var corr = new Array(x);这语句执行的时候,就会生成一个大小为1的数组

2.2:漏洞数组初始化分析

       

                                                              图2.2.1.1

图2.1.4.1上EIP指向的指令为

cmp rdi,0

       je 39000C419D

也就是对x是否为0进行了判断                                     

根据v8指针的特点,以及调试分析可以判断,这里执行的流程是进行数组对象的初始化。

2.2.1: 初始化r8-1指向的数组。(这里将其定义为array1

 add r8,1

    lea r9d,qword ptr ds:[rdi+rdi]//rdi=x

    mov r12,qword ptr ds:[r13+D0]

    mov dword ptr ds:[r8-1],r12d //初始化array1数组的map

 mov dword ptr ds:[r8+3],r9d//初始化array1数组的大小,内存值为2*x

 xor r9,r9

    lea r14,qword ptr ds:[r9+1]                      

    mov r15,qword ptr ds:[r13+98]                        

    mov dword ptr ds:[r8+r9*4+7],r15d   /*这里进行了初始化,也就是该数组第一个元素为[r8+7],在[array1+8] */                

    mov r9,r14                                         

    cmp r9,rdi                                          

    jb 99000C4180 

     

                                                             图2.2.1.2

2.2.2:  图2.2.1.2EIP前面的一段指令是初始化r9-1指向的数组。(这里将其定义为array2)

  add r9,1

  mov r14d,824394D

  mov dword ptr ds:[r9-1],r14d//初始化array2数组的map

  mov r14,qword ptr ds:[r13+158]

  mov dword ptr ds:[r9+3],r14d//初始化array2数组的prototype

  lea  r15d, qword ptr ds:[rdi+rdi]

  mov dword ptr ds:[r9+7],r8d//初始化array2数组的大小,内存值为2*x

  mov [r9+7],r8d//初始化array2数组的elements,让其指向array1

        

                                                                图2.2.1.1、R8、R9寄存器此时指向的内存

          

                                                     图2.2.2.2、array1指向的内存

           

                    图2.2.2.3、array2指向的内存

2.2.3:此时array1array2两者内存关系如下图所示:

             

                       图2.2.3.4、array1和array2的内存关系图

这内存关系在后面的漏洞利用比较重要。如果改变了sizeofarray1的值,就对后面的内存进行读写,我们在poc中的js对corr 进行索引,其实是索引element1开始的内存,也就是实际上控制这里的array1数组。corr[0]索引到的值为element1corr[1] 索引到的值array2map),corr[2] 索引为到的值为sizeofarray2由此继续下去。

2.3:数值越界的直接原因

2.3.1:接下执行的是对应的是poc中的corr.shift();这句的流程:

           

                                                                       图2.3.1.1

图2.3.1.1这里的指令为:mov dword ptr ds:[r9+B],FFFFFFFE;

接下来的指令是直接将0xFFFFFFFE这个硬编码放入了array2存放数组大小的内存位置中, v8 引擎对array2索引的时候,会把0xFFFFFFFE当成array2数组的大小,从而将array2视为一个0xFFFFFFFE/2大小的数组。(不过经过调试这个内存的数值并不是v8用来索引poc中js数组corr的数值,只要该值不为0,就会去索引实际控制内存的数组array1)

2.3.2:紧接着会执行指令

   mov rdi,qword ptr ds:[r13+98]                      

   mov dword ptr ds:[r8+3],edi                         

这两句指令是对原来array1的数组大小的内存位置,也就是[array1+4]的值进行填充,填充的值为[r13+98]指向的值,这里是数字0x08042429v8引擎会在后面把这个填进来的数字解释为array1的大小。(这是造成这漏洞数组corr可以越界读写的直接原因)

          

                                                                         图2.3.2.1

对这一段指令总结:

         l  v8执行生成和执行poc 优化后的corr.shift() js指令时,并没有对array2指向array1的关系进行改变。

   l  在对array2数组处理时,原来存放数组大小的位置改为了数字0xFFFFFFFE,但这个数值v8会认为其是合法的数组数值,因此我们poc中的js语句中还是用corr数组还是可以继续索引到array1数组,array1因为优化的原因,把代表着array1长度的内存值修改为0x08042429(这个数值根据调试环境而定,但是一定是个比2大很多的值)。而v8解释器会把这个值解释为array1数组的大小也就是pocjscorr数组的大小,从而造成了corr数组可以越界读写

 0x3:浮点数组生成验证越界读写

这一段是开始其实算是漏洞利用的部分,和我要分析的东西没太大关联,在这里我是用来验证越界读取的位置,不过从逆向分析的角度来看这一段加进来还是很有必要的,毕竟浮点数的初始化的特征非常明显,可以帮助你很快定位到优化后生成的指令:

   

                              图3.1.1.1

3.1.1:图3.1.1.1的流程是在array2数组的内存之后生成一个浮点数组,在这个浮点数组生成后,整个内存结构就变成如下图所示:

     

                                                                           图3.1.1.2

3.1.2:最后生成内存数据排列如图3.1.2.1图所示:

             

                                                                                           图3.1.2.1

3.1.3图3.1.2.1的位置关系可以看到,corr发生了越界读写的话,corr[8]如果合法会读到到了浮点数0x3FF199999999999A的前四个字节0x3FF19999v8把这值当 成是地址解析,所以会在前面加上external_pointer,在这调试环境下是0x003900000000,最后读取出来就corr[8]会变成了0x00393ff19999,如下图所示
    

                              图3.1.3.1

3.1.4:这里可以看到,此时原来的array1大小区域变为0x08042429,v8把他把他当成数组的大小就会解释为0x08042429/2==0x04021214,也就是十进制数     67244564。(这个值和该数组第一个元素的尾部四位是一样的,也就是在内存中为同样的值而表示array2的大小的内存值为0xFFFFFFFE,在这里打印出corr.length的时候解释为-1,但是在实际判断中却被先当成有符号数-2,然后除2为-1,也就是0xFFFFFFFF(十进制数为4294967295),然后作为合法的素组大小索引,这里用Ubuntu上的v8运行可以直接看到

         

                                                                             图3.1.4.1

0x4:写在最后 

   就像这标题说的,这是4月13日Chrome爆出的v8优化漏洞的车祸现场分析,什么抛砖引玉一类的客套话就不说了,这里只是解析了优化后的JS代码到底发生了什么,但是为什么会有这场车祸,比如这些车子出事前经过了几个红绿灯,驾驶过来的先后顺序和方向是什么,分别在什么时间节点出了发的车,又是如何控制道路障碍让他们最后相撞,单单靠逆向这段JIT后生成的指令是远远不够的,还要搞明白这段JIT生成为什么会生成这样的指令,也就是了解优化的本身。不过搞明白这些确能够帮我们整理更多的漏洞信息,以便后面分析能彻底的了解这个Chrome漏洞的完整面貌。

   参考文档

         https://twitter.com/r4j0x00/status/1381643526010597380

         https://chromereleases.googleblog.com/2021/03/stable-channel-update-for-desktop_30.html  

         https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html

         https://xz.aliyun.com/t/9014

         https://www.anquanke.com/post/id/207483

 

  

 

 


    第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

    最后于 2021-4-21 10:08 被苏啊树编辑 ,原因:
    收藏
    点赞2
    打赏
    分享
    最新回复 (8)
    雪    币: 12189
    活跃值: 活跃值 (8234)
    能力值: (RANK:600 )
    在线值:
    发帖
    回帖
    粉丝
    有毒 活跃值 9 2021-4-20 19:09
    2
    0
    师傅的文章重新编排一下吧,图片没上来
    雪    币: 2742
    活跃值: 活跃值 (1152)
    能力值: ( LV7,RANK:110 )
    在线值:
    发帖
    回帖
    粉丝
    苏啊树 活跃值 2 2021-4-20 20:18
    3
    0
    有毒 师傅的文章重新编排一下吧,图片没上来
    版主费心了,因为没怎么在看雪发过贴,就先直接从Word上直接粘贴,然后编辑.....
    雪    币: 12189
    活跃值: 活跃值 (8234)
    能力值: (RANK:600 )
    在线值:
    发帖
    回帖
    粉丝
    有毒 活跃值 9 2021-4-21 09:00
    4
    0
    苏啊树 版主费心了,因为没怎么在看雪发过贴,就先直接从Word上直接粘贴,然后编辑.....
    表示理解~期待师傅下一篇文章啊
    雪    币: 440
    活跃值: 活跃值 (44)
    能力值: (RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    同志们好啊 活跃值 2021-4-22 14:00
    5
    0
    如果直接能在浏览器里面写c++就好了.
    雪    币: 2742
    活跃值: 活跃值 (1152)
    能力值: ( LV7,RANK:110 )
    在线值:
    发帖
    回帖
    粉丝
    苏啊树 活跃值 2 2021-4-22 15:35
    6
    0
    同志们好啊 如果直接能在浏览器里面写c++就好了.

    用C++可以用WASM转化,只能写一些图像算法布局啥的,不允许你调用标准C++库和系统函数这些......

    最后于 2021-4-22 15:39 被苏啊树编辑 ,原因:
    雪    币: 47
    活跃值: 活跃值 (276)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    saloyun 活跃值 2021-4-23 11:00
    7
    0
    厉害!
    雪    币: 440
    活跃值: 活跃值 (44)
    能力值: (RANK:0 )
    在线值:
    发帖
    回帖
    粉丝
    同志们好啊 活跃值 2021-4-27 08:39
    8
    0
    苏啊树 同志们好啊 如果直接能在浏览器里面写c++就好了. 用C++可以用WASM转化,只能写一些图像算法布局啥的,不允许你调用标准C++库和系统函数这 ...
    有个yc++浏览器,可能可以实现在浏览器里面运行c++.
    这里:http://www.ycbro.com/
    雪    币: 39
    活跃值: 活跃值 (21)
    能力值: ( LV2,RANK:10 )
    在线值:
    发帖
    回帖
    粉丝
    zjjjia 活跃值 2021-6-5 14:05
    9
    0
    游客
    登录 | 注册 方可回帖
    返回